diff --git a/Barotrauma/BarotraumaClient/ClientCode.projitems b/Barotrauma/BarotraumaClient/ClientCode.projitems index 8b57735b9..7c88d6ccd 100644 --- a/Barotrauma/BarotraumaClient/ClientCode.projitems +++ b/Barotrauma/BarotraumaClient/ClientCode.projitems @@ -40,6 +40,7 @@ + @@ -165,6 +166,10 @@ + + + + diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index a4375f0b3..dced51971 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -93,6 +93,9 @@ ..\..\Libraries\NuGet\nVLC.3.0.0\lib\net40\nVLC.dll + + ..\..\Libraries\NuGet\NVorbis.0.8.6\lib\net35\NVorbis.dll + ..\..\Libraries\NuGet\OpenTK.3.0.1\lib\net20\OpenTK.dll diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index d21001bd7..2018c3506 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -1,4 +1,4 @@ - + ReleaseMac @@ -91,6 +91,9 @@ ..\..\Libraries\NuGet\nVLC.3.0.0\lib\net40\nVLC.dll + + ..\..\Libraries\NuGet\NVorbis.0.8.6\lib\net35\NVorbis.dll + ..\..\Libraries\NuGet\OpenTK.3.0.1\lib\net20\OpenTK.dll @@ -316,8 +319,8 @@ - - Icon.bmp + + Icon.bmp diff --git a/Barotrauma/BarotraumaClient/Properties/AssemblyInfo.cs b/Barotrauma/BarotraumaClient/Properties/AssemblyInfo.cs index 6509147bc..b55a2d5c1 100644 --- a/Barotrauma/BarotraumaClient/Properties/AssemblyInfo.cs +++ b/Barotrauma/BarotraumaClient/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.9.200.0")] -[assembly: AssemblyFileVersion("0.9.1.0")] +[assembly: AssemblyVersion("0.9.2.1")] +[assembly: AssemblyFileVersion("0.9.2.1")] diff --git a/Barotrauma/BarotraumaClient/Source/Camera.cs b/Barotrauma/BarotraumaClient/Source/Camera.cs index fe79671b4..29bc5fdc9 100644 --- a/Barotrauma/BarotraumaClient/Source/Camera.cs +++ b/Barotrauma/BarotraumaClient/Source/Camera.cs @@ -185,9 +185,9 @@ namespace Barotrauma position += amount; } - public void ClientWrite(NetOutgoingMessage msg) + public void ClientWrite(IWriteMessage msg) { - if (Character.Controlled != null && !Character.Controlled.IsDead) return; + if (Character.Controlled != null && !Character.Controlled.IsDead) { return; } msg.Write((byte)ClientNetObject.SPECTATING_POS); msg.Write(position.X); diff --git a/Barotrauma/BarotraumaClient/Source/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/Source/Characters/CharacterHUD.cs index 004c074a0..c28135e75 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/CharacterHUD.cs @@ -138,7 +138,7 @@ namespace Barotrauma brokenItemsCheckTimer = 1.0f; foreach (Item item in Item.ItemList) { - if (!item.Repairables.Any(r => item.Condition < r.ShowRepairUIThreshold)) { continue; } + if (!item.Repairables.Any(r => item.ConditionPercentage < r.ShowRepairUIThreshold)) { continue; } if (Submarine.VisibleEntities != null && !Submarine.VisibleEntities.Contains(item)) { continue; } Vector2 diff = item.WorldPosition - character.WorldPosition; @@ -210,7 +210,7 @@ namespace Barotrauma GUI.DrawString(spriteBatch, textPos, focusName, nameColor, Color.Black * 0.7f, 2); textPos.Y += offset.Y; - if (character.FocusedCharacter.CanInventoryBeAccessed) + if (character.FocusedCharacter.CanBeDragged) { GUI.DrawString(spriteBatch, textPos, GetCachedHudText("GrabHint", GameMain.Config.KeyBind(InputType.Grab).ToString()), Color.LightGreen, Color.Black, 2, GUI.SmallFont); diff --git a/Barotrauma/BarotraumaClient/Source/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/Source/Characters/CharacterInfo.cs index f678e8b3a..1b51261ea 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/CharacterInfo.cs @@ -1,5 +1,5 @@ using Barotrauma.Extensions; -using Lidgren.Network; +using Barotrauma.Networking; using System; using System.Linq; using System.Collections.Generic; @@ -53,7 +53,7 @@ namespace Barotrauma if (personalityTrait != null && TextManager.Language == "English") { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), - TextManager.AddPunctuation(':', TextManager.Get("PersonalityTrait"), personalityTrait.Name), font: font); + TextManager.AddPunctuation(':', TextManager.Get("PersonalityTrait"), TextManager.Get("personalitytrait." + personalityTrait.Name.Replace(" ", ""))), font: font); } //spacing @@ -220,7 +220,7 @@ namespace Barotrauma } - public static CharacterInfo ClientRead(string configPath, NetBuffer inc) + public static CharacterInfo ClientRead(string configPath, IReadMessage inc) { ushort infoID = inc.ReadUInt16(); string newName = inc.ReadString(); diff --git a/Barotrauma/BarotraumaClient/Source/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/Source/Characters/CharacterNetworking.cs index 5ad1a4063..a4927a4b7 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/CharacterNetworking.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Linq; @@ -22,12 +21,12 @@ namespace Barotrauma } //freeze AI characters if more than 1 seconds have passed since last update from the server - if (lastRecvPositionUpdateTime < NetTime.Now - 1.0f) + if (lastRecvPositionUpdateTime < Lidgren.Network.NetTime.Now - 1.0f) { AnimController.Frozen = true; memState.Clear(); //hide after 2 seconds - if (lastRecvPositionUpdateTime < NetTime.Now - 2.0f) + if (lastRecvPositionUpdateTime < Lidgren.Network.NetTime.Now - 2.0f) { Enabled = false; return; @@ -99,22 +98,22 @@ namespace Barotrauma } } - public virtual void ClientWrite(NetBuffer msg, object[] extraData = null) + public virtual void ClientWrite(IWriteMessage msg, object[] extraData = null) { if (extraData != null) { switch ((NetEntityEvent.Type)extraData[0]) { case NetEntityEvent.Type.InventoryState: - msg.WriteRangedInteger(0, 3, 0); + msg.WriteRangedIntegerDeprecated(0, 3, 0); Inventory.ClientWrite(msg, extraData); break; case NetEntityEvent.Type.Treatment: - msg.WriteRangedInteger(0, 3, 1); + msg.WriteRangedIntegerDeprecated(0, 3, 1); msg.Write(AnimController.Anim == AnimController.Animation.CPR); break; case NetEntityEvent.Type.Status: - msg.WriteRangedInteger(0, 3, 2); + msg.WriteRangedIntegerDeprecated(0, 3, 2); break; } } @@ -132,7 +131,7 @@ namespace Barotrauma msg.Write(inputCount); for (int i = 0; i < inputCount; i++) { - msg.WriteRangedInteger(0, (int)InputNetFlags.MaxVal, (int)memInput[i].states); + msg.WriteRangedIntegerDeprecated(0, (int)InputNetFlags.MaxVal, (int)memInput[i].states); msg.Write(memInput[i].intAim); if (memInput[i].states.HasFlag(InputNetFlags.Select) || memInput[i].states.HasFlag(InputNetFlags.Deselect) || @@ -147,14 +146,14 @@ namespace Barotrauma msg.WritePadBits(); } - public virtual void ClientRead(ServerNetObject type, NetBuffer msg, float sendingTime) + public virtual void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { switch (type) { case ServerNetObject.ENTITY_POSITION: bool facingRight = AnimController.Dir > 0.0f; - lastRecvPositionUpdateTime = (float)NetTime.Now; + lastRecvPositionUpdateTime = (float)Lidgren.Network.NetTime.Now; AnimController.Frozen = false; Enabled = true; @@ -222,8 +221,8 @@ namespace Barotrauma } Vector2 pos = new Vector2( - msg.ReadFloat(), - msg.ReadFloat()); + msg.ReadSingle(), + msg.ReadSingle()); float MaxVel = NetConfig.MaxPhysicsBodyVelocity; Vector2 linearVelocity = new Vector2( msg.ReadRangedSingle(-MaxVel, MaxVel, 12), @@ -235,7 +234,7 @@ namespace Barotrauma float? angularVelocity = null; if (!fixedRotation) { - rotation = msg.ReadFloat(); + rotation = msg.ReadSingle(); float MaxAngularVel = NetConfig.MaxPhysicsBodyAngularVelocity; angularVelocity = msg.ReadRangedSingle(-MaxAngularVel, MaxAngularVel, 8); angularVelocity = NetConfig.Quantize(angularVelocity.Value, -MaxAngularVel, MaxAngularVel, 8); @@ -336,7 +335,7 @@ namespace Barotrauma } } - public static Character ReadSpawnData(NetBuffer inc, bool spawn = true) + public static Character ReadSpawnData(IReadMessage inc, bool spawn = true) { DebugConsole.NewMessage("READING CHARACTER SPAWN DATA", Color.Cyan); @@ -347,7 +346,7 @@ namespace Barotrauma string speciesName = inc.ReadString(); string seed = inc.ReadString(); - Vector2 position = new Vector2(inc.ReadFloat(), inc.ReadFloat()); + Vector2 position = new Vector2(inc.ReadSingle(), inc.ReadSingle()); bool enabled = inc.ReadBoolean(); @@ -414,8 +413,17 @@ namespace Barotrauma return character; } + + private void ReadTraitorStatus(IReadMessage msg) + { + IsTraitor = msg.ReadBoolean(); + if (IsTraitor) + { + TraitorCurrentObjective = msg.ReadString(); + } + } - private void ReadStatus(NetBuffer msg) + private void ReadStatus(IReadMessage msg) { bool isDead = msg.ReadBoolean(); if (isDead) @@ -450,7 +458,7 @@ namespace Barotrauma else { if (IsDead) Revive(); - + CharacterHealth.ClientRead(msg); } } diff --git a/Barotrauma/BarotraumaClient/Source/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/Source/Characters/Health/CharacterHealth.cs index 131923312..110bc3c49 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/Health/CharacterHealth.cs @@ -1,6 +1,5 @@ using Barotrauma.Items.Components; using Barotrauma.Networking; -using Lidgren.Network; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; @@ -1375,7 +1374,7 @@ namespace Barotrauma (int)(limbHealth.HighlightArea.Height * scale)); } - public void ClientRead(NetBuffer inc) + public void ClientRead(IReadMessage inc) { List> newAfflictions = new List>(); diff --git a/Barotrauma/BarotraumaClient/Source/Characters/Limb.cs b/Barotrauma/BarotraumaClient/Source/Characters/Limb.cs index af335ceb0..082d577b7 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/Limb.cs @@ -441,7 +441,7 @@ namespace Barotrauma float herpesStrength = character.CharacterHealth.GetAfflictionStrength("spaceherpes"); if (herpesStrength > 0.0f) { - DrawWearable(HerpesSprite, depthStep, spriteBatch, color * (herpesStrength / 100.0f), spriteEffect); + DrawWearable(HerpesSprite, depthStep, spriteBatch, color * Math.Min(herpesStrength / 10.0f, 1.0f), spriteEffect); depthStep += 0.000001f; } } diff --git a/Barotrauma/BarotraumaClient/Source/DebugConsole.cs b/Barotrauma/BarotraumaClient/Source/DebugConsole.cs index 429bb51f2..bf118207c 100644 --- a/Barotrauma/BarotraumaClient/Source/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/Source/DebugConsole.cs @@ -199,6 +199,7 @@ namespace Barotrauma return client.HasPermission(ClientPermissions.Kick); case "ban": case "banip": + case "banendpoint": return client.HasPermission(ClientPermissions.Ban); case "unban": case "unbanip": @@ -317,7 +318,13 @@ namespace Barotrauma private static void AssignRelayToServer(string names, bool relay) { - commands.First(c => c.names.Intersect(names.Split('|')).Count() > 0).RelayToServer = relay; + Command command = commands.Find(c => c.names.Intersect(names.Split('|')).Count() > 0); + if (command == null) + { + DebugConsole.Log("Could not assign to relay to server: " + names); + return; + } + command.RelayToServer = relay; } private static void InitProjectSpecific() @@ -358,16 +365,23 @@ namespace Barotrauma } })); - commands.Add(new Command("startclient", "", (string[] args) => + commands.Add(new Command("startlidgrenclient", "", (string[] args) => { if (args.Length == 0) return; if (GameMain.Client == null) { - GameMain.Client = new GameClient("Name", args[0]); + GameMain.Client = new GameClient("Name", args[0], 0); } })); + commands.Add(new Command("startsteamp2pclient", "", (string[] args) => + { + if (GameMain.Client == null) + { + GameMain.Client = new GameClient("Name", null, 76561198977850505); //this is juan's alt account, feel free to abuse this one + } + })); commands.Add(new Command("enablecheats", "enablecheats: Enables cheat commands and disables Steam achievements during this play session.", (string[] args) => { @@ -465,10 +479,16 @@ namespace Barotrauma commands.Add(new Command("clientlist", "", (string[] args) => { })); AssignRelayToServer("clientlist", true); + commands.Add(new Command("say", "", (string[] args) => { })); + AssignRelayToServer("say", true); + commands.Add(new Command("msg", "", (string[] args) => { })); + AssignRelayToServer("msg", true); commands.Add(new Command("setmaxplayers|maxplayers", "", (string[] args) => { })); AssignRelayToServer("setmaxplayers", true); commands.Add(new Command("setpassword|password", "", (string[] args) => { })); AssignRelayToServer("setpassword", true); + commands.Add(new Command("traitorlist", "", (string[] args) => { })); + AssignRelayToServer("traitorlist", true); AssignOnExecute("control", (string[] args) => { @@ -551,6 +571,7 @@ namespace Barotrauma if (args.Length < 3) { ThrowError("Not enough arguments provided! At least three required."); + return; } if (!byte.TryParse(args[0], out byte r)) { @@ -952,7 +973,36 @@ namespace Barotrauma } }, isCheat: false)); + commands.Add(new Command("setentityproperties", "setentityproperties [property name] [value]: Sets the value of some property on all selected items/structures in the sub editor.", (string[] args) => + { + if (args.Length != 2 || Screen.Selected != GameMain.SubEditorScreen) { return; } + foreach (MapEntity me in MapEntity.SelectedList) + { + if (me is ISerializableEntity serializableEntity) + { + if (serializableEntity.SerializableProperties == null) + { + continue; + } + if (!serializableEntity.SerializableProperties.TryGetValue(args[0].ToLowerInvariant(), out SerializableProperty property)) + { + NewMessage("Property \"" + args[0] + "\" not found in the entity \"" + me.ToString() + "\".", Color.Orange); + continue; + } + if (!property.TrySetValue(me, args[1])) + { + NewMessage("Failed to set the value of \"" + args[0] + "\" to \"" + args[1] + "\" on the entity \"" + me.ToString() + "\".", Color.Orange); + } + } + } + })); + #if DEBUG + commands.Add(new Command("printreceivertransfers", "", (string[] args) => + { + GameMain.Client.PrintReceiverTransters(); + })); + commands.Add(new Command("checkmissingloca", "", (string[] args) => { foreach (MapEntityPrefab me in MapEntityPrefab.List) @@ -1201,6 +1251,8 @@ namespace Barotrauma { GameMain.Config.MusicVolume = 0.5f; GameMain.Config.SoundVolume = 0.5f; + GameMain.Config.DynamicRangeCompressionEnabled = true; + GameMain.Config.VoipAttenuationEnabled = true; NewMessage("Music and sound volume set to 0.5", Color.Green); GameMain.Config.GraphicsWidth = 0; @@ -1416,11 +1468,11 @@ namespace Barotrauma ); AssignOnClientExecute( - "banip", + "banendpoint|banip", (string[] args) => { if (GameMain.Client == null || args.Length == 0) return; - ShowQuestionPrompt("Reason for banning the ip \"" + args[0] + "\"?", (reason) => + ShowQuestionPrompt("Reason for banning the endpoint \"" + args[0] + "\"?", (reason) => { ShowQuestionPrompt("Enter the duration of the ban (leave empty to ban permanently, or use the format \"[days] d [hours] h\")", (duration) => { @@ -1436,7 +1488,7 @@ namespace Barotrauma } GameMain.Client.SendConsoleCommand( - "banip " + + "banendpoint " + args[0] + " " + (banDuration.HasValue ? banDuration.Value.TotalSeconds.ToString() : "0") + " " + reason); @@ -1779,6 +1831,21 @@ namespace Barotrauma character.ReloadHead(id, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); } }, isCheat: true)); + + commands.Add(new Command("spawnsub", "spawnsub [subname]: Spawn a submarine at the position of the cursor", (string[] args) => + { + try + { + Submarine spawnedSub = Submarine.Load(args[0], false); + spawnedSub.SetPosition(GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition)); + } + catch (Exception e) + { + string errorMsg = "Failed to spawn a submarine. Arguments: \"" + string.Join(" ", args) + "\"."; + ThrowError(errorMsg, e); + GameAnalyticsManager.AddErrorEventOnce("DebugConsole.SpawnSubmarine:Error", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg + '\n' + e.Message + '\n' + e.StackTrace); + } + }, isCheat: true)); } private static void ReloadWearables(Character character, int variant = 0) diff --git a/Barotrauma/BarotraumaClient/Source/Events/Missions/Mission.cs b/Barotrauma/BarotraumaClient/Source/Events/Missions/Mission.cs index 0174c2246..1ca52a559 100644 --- a/Barotrauma/BarotraumaClient/Source/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaClient/Source/Events/Missions/Mission.cs @@ -4,16 +4,13 @@ namespace Barotrauma { partial class Mission { - public void ShowMessage(int index) + partial void ShowMessageProjSpecific(int index) { if (index >= Headers.Count && index >= Messages.Count) return; string header = index < Headers.Count ? Headers[index] : ""; string message = index < Messages.Count ? Messages[index] : ""; - //TODO: reimplement - //GameServer.Log(TextManager.Get("MissionInfo") + ": " + header + " - " + message, ServerLog.MessageType.ServerMessage); - new GUIMessageBox(header, message); } } diff --git a/Barotrauma/BarotraumaClient/Source/Events/Missions/MissionMode.cs b/Barotrauma/BarotraumaClient/Source/Events/Missions/MissionMode.cs index bdff63c82..759e34247 100644 --- a/Barotrauma/BarotraumaClient/Source/Events/Missions/MissionMode.cs +++ b/Barotrauma/BarotraumaClient/Source/Events/Missions/MissionMode.cs @@ -5,19 +5,14 @@ namespace Barotrauma { partial class MissionMode : GameMode { - public override void MsgBox() + public override void ShowStartMessage() { if (mission == null) return; - var missionMsg = new GUIMessageBox(mission.Name, mission.Description, new Vector2(0.25f, 0.0f), new Point(400, 200)) + new GUIMessageBox(mission.Name, mission.Description, new Vector2(0.25f, 0.0f), new Point(400, 200)) { UserData = "missionstartmessage" }; - -#if SERVER - Networking.GameServer.Log(TextManager.Get("Mission") + ": " + mission.Name, Networking.ServerLog.MessageType.ServerMessage); - Networking.GameServer.Log(mission.Description, Networking.ServerLog.MessageType.ServerMessage); -#endif } } } diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUI.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUI.cs index 4585a78d4..b9a37894c 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUI.cs @@ -310,6 +310,16 @@ namespace Barotrauma "Sounds (Ctrl+S to hide): ", Color.White, Color.Black * 0.5f, 0, SmallFont); y += 15; + DrawString(spriteBatch, new Vector2(500, y), + "Current playback amplitude: " + GameMain.SoundManager.PlaybackAmplitude.ToString(), Color.White, Color.Black * 0.5f, 0, SmallFont); + + y += 15; + + DrawString(spriteBatch, new Vector2(500, y), + "Compressed dynamic range gain: " + GameMain.SoundManager.CompressionDynamicRangeGain.ToString(), Color.White, Color.Black * 0.5f, 0, SmallFont); + + y += 15; + DrawString(spriteBatch, new Vector2(500, y), "Loaded sounds: " + GameMain.SoundManager.LoadedSoundCount + " (" + GameMain.SoundManager.UniqueLoadedSoundCount + " unique)", Color.White, Color.Black * 0.5f, 0, SmallFont); y += 15; @@ -343,18 +353,21 @@ namespace Barotrauma soundStr += " (looping)"; clr = Color.Yellow; } - if (playingSoundChannel.IsStream) { soundStr += " (streaming)"; clr = Color.Lime; } - if (!playingSoundChannel.IsPlaying) { soundStr += " (stopped)"; clr *= 0.5f; } + else if (playingSoundChannel.Muffled) + { + soundStr += " (muffled)"; + clr = Color.Lerp(clr, Color.LightGray, 0.5f); + } } DrawString(spriteBatch, new Vector2(500, y), soundStr, clr, Color.Black * 0.5f, 0, SmallFont); @@ -640,6 +653,12 @@ namespace Barotrauma return MouseOn; } + public static bool HasSizeChanged(Point referenceResolution, float referenceUIScale, float referenceHUDScale) + { + return GameMain.GraphicsWidth != referenceResolution.X || GameMain.GraphicsHeight != referenceResolution.Y || + referenceUIScale != Inventory.UIScale || referenceHUDScale != Scale; + } + public static void Update(float deltaTime) { HandlePersistingElements(deltaTime); @@ -1461,6 +1480,9 @@ namespace Barotrauma if (pauseMenuOpen) { + Inventory.draggingItem = null; + Inventory.DraggingInventory = null; + PauseMenu = new GUIFrame(new RectTransform(Vector2.One, Canvas), style: null, color: Color.Black * 0.5f); var pauseMenuInner = new GUIFrame(new RectTransform(new Vector2(0.13f, 0.35f), PauseMenu.RectTransform, Anchor.Center) { MinSize = new Point(200, 300) }); diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUIComponent.cs index 265331a13..8bfa3c290 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUIComponent.cs @@ -67,6 +67,21 @@ namespace Barotrauma } // TODO: refactor? + + public GUIComponent FindChild(Func predicate, bool recursive = false) + { + var matchingChild = Children.FirstOrDefault(predicate); + if (recursive && matchingChild == null) + { + foreach (GUIComponent child in Children) + { + matchingChild = child.FindChild(predicate, recursive); + if (matchingChild != null) return matchingChild; + } + } + + return matchingChild; + } public GUIComponent FindChild(object userData, bool recursive = false) { var matchingChild = Children.FirstOrDefault(c => c.userData == userData); diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUIDropDown.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUIDropDown.cs index c2f8351bc..4ff2c0079 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUIDropDown.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUIDropDown.cs @@ -1,11 +1,13 @@ -using Microsoft.Xna.Framework; +using EventInput; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; using System.Collections.Generic; using System.Linq; namespace Barotrauma { - public class GUIDropDown : GUIComponent + public class GUIDropDown : GUIComponent, IKeyboardSubscriber { public delegate bool OnSelectedHandler(GUIComponent selected, object obj = null); public OnSelectedHandler OnSelected; @@ -42,10 +44,22 @@ namespace Barotrauma set { button.Enabled = value; } } - public GUIComponent Selected + public GUIComponent SelectedComponent { get { return listBox.SelectedComponent; } } + + public bool Selected + { + get + { + return Dropped; + } + set + { + Dropped = value; + } + } public GUIListBox ListBox { @@ -69,6 +83,30 @@ namespace Barotrauma } } + public void ReceiveTextInput(char inputChar) + { + GUI.KeyboardDispatcher.Subscriber = null; + } + public void ReceiveTextInput(string text) { } + public void ReceiveCommandInput(char command) { } + + public void ReceiveSpecialInput(Keys key) + { + switch (key) + { + case Keys.Up: + case Keys.Down: + listBox.ReceiveSpecialInput(key); + GUI.KeyboardDispatcher.Subscriber = this; + break; + case Keys.Enter: + case Keys.Space: + case Keys.Escape: + GUI.KeyboardDispatcher.Subscriber = null; + break; + } + } + private List selectedDataMultiple = new List(); public IEnumerable SelectedDataMultiple { @@ -287,6 +325,12 @@ namespace Barotrauma { OnDropped?.Invoke(this, userData); listBox.UpdateScrollBarSize(); + + GUI.KeyboardDispatcher.Subscriber = this; + } + else if (GUI.KeyboardDispatcher.Subscriber == this) + { + GUI.KeyboardDispatcher.Subscriber = null; } return true; } @@ -343,6 +387,10 @@ namespace Barotrauma if (!listBoxRect.Contains(PlayerInput.MousePosition) && !button.Rect.Contains(PlayerInput.MousePosition)) { Dropped = false; + if (GUI.KeyboardDispatcher.Subscriber == this) + { + GUI.KeyboardDispatcher.Subscriber = null; + } } } } diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUIListBox.cs index a8fd0fc02..13066a72e 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUIListBox.cs @@ -651,7 +651,9 @@ namespace Barotrauma case Keys.Up: SelectPrevious(); break; - default: + case Keys.Enter: + case Keys.Space: + case Keys.Escape: GUI.KeyboardDispatcher.Subscriber = null; break; } diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUITextBlock.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUITextBlock.cs index f6699c4f7..07d3459e3 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUITextBlock.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUITextBlock.cs @@ -26,6 +26,7 @@ namespace Barotrauma public TextGetterHandler TextGetter; public bool Wrap; + private bool playerInput; public bool RoundToNearestPixel = true; @@ -193,7 +194,7 @@ namespace Barotrauma /// If the rectT height is set 0, the height is calculated from the text. /// public GUITextBlock(RectTransform rectT, string text, Color? textColor = null, ScalableFont font = null, - Alignment textAlignment = Alignment.Left, bool wrap = false, string style = "", Color? color = null) + Alignment textAlignment = Alignment.Left, bool wrap = false, string style = "", Color? color = null, bool playerInput = false) : base(style, rectT) { if (color.HasValue) @@ -208,6 +209,7 @@ namespace Barotrauma this.textAlignment = textAlignment; this.Wrap = wrap; this.Text = text ?? ""; + this.playerInput = playerInput; if (rectT.Rect.Height == 0 && !string.IsNullOrEmpty(text)) { CalculateHeightFromText(); @@ -254,7 +256,7 @@ namespace Barotrauma if (Wrap && rect.Width > 0) { - wrappedText = ToolBox.WrapText(text, rect.Width - padding.X - padding.Z, Font, textScale); + wrappedText = ToolBox.WrapText(text, rect.Width - padding.X - padding.Z, Font, textScale, playerInput); TextSize = MeasureText(wrappedText); } else if (OverflowClip) diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUITextBox.cs index 92bd2fc12..186be6505 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUITextBox.cs @@ -199,6 +199,12 @@ namespace Barotrauma } } + public Vector4 Padding + { + get { return textBlock.Padding; } + set { textBlock.Padding = value; } + } + // TODO: should this be defined in the stylesheet? public Color SelectionColor { get; set; } = Color.White * 0.25f; @@ -229,7 +235,7 @@ namespace Barotrauma this.color = color ?? Color.White; frame = new GUIFrame(new RectTransform(Vector2.One, rectT, Anchor.Center), style, color); GUI.Style.Apply(frame, style == "" ? "GUITextBox" : style); - textBlock = new GUITextBlock(new RectTransform(Vector2.One, frame.RectTransform, Anchor.Center), text, textColor, font, textAlignment, wrap); + textBlock = new GUITextBlock(new RectTransform(Vector2.One, frame.RectTransform, Anchor.Center), text, textColor, font, textAlignment, wrap, playerInput: true); GUI.Style.Apply(textBlock, "", this); CaretEnabled = true; caretPosDirty = true; @@ -308,6 +314,7 @@ namespace Barotrauma protected List> GetAllPositions() { + float halfHeight = Font.MeasureString("T").Y * 0.5f; string textDrawn = Censor ? textBlock.CensoredText : textBlock.WrappedText; var positions = new List>(); if (textDrawn.Contains("\n")) @@ -323,9 +330,9 @@ namespace Barotrauma for (int j = 0; j <= line.Length; j++) { Vector2 lineTextSize = Font.MeasureString(line.Substring(0, j)); - Vector2 indexPos = new Vector2(lineTextSize.X + textBlock.Padding.X, totalTextHeight + textBlock.Padding.Y); + Vector2 indexPos = new Vector2(lineTextSize.X + textBlock.Padding.X, totalTextHeight + textBlock.Padding.Y - halfHeight); //DebugConsole.NewMessage($"index: {index}, pos: {indexPos}", Color.AliceBlue); - positions.Add(new Tuple(textBlock.Rect.Location.ToVector2() + indexPos, index + j)); + positions.Add(new Tuple(indexPos, index + j)); } index = totalIndex; } @@ -336,9 +343,9 @@ namespace Barotrauma for (int i = 0; i <= textBlock.Text.Length; i++) { Vector2 textSize = Font.MeasureString(textDrawn.Substring(0, i)); - Vector2 indexPos = new Vector2(textSize.X + textBlock.Padding.X, textSize.Y + textBlock.Padding.Y) + textBlock.TextPos - textBlock.Origin; + Vector2 indexPos = new Vector2(textSize.X + textBlock.Padding.X, textSize.Y + textBlock.Padding.Y - halfHeight) + textBlock.TextPos - textBlock.Origin; //DebugConsole.NewMessage($"index: {i}, pos: {indexPos}", Color.WhiteSmoke); - positions.Add(new Tuple(textBlock.Rect.Location.ToVector2() + indexPos, i)); + positions.Add(new Tuple(indexPos, i)); } } return positions; @@ -346,10 +353,64 @@ namespace Barotrauma public int GetCaretIndexFromScreenPos(Vector2 pos) { - var positions = GetAllPositions().OrderBy(p => Vector2.DistanceSquared(p.Item1, pos)); - var posIndex = positions.FirstOrDefault(); + return GetCaretIndexFromLocalPos(pos - textBlock.Rect.Location.ToVector2()); + } + + public int GetCaretIndexFromLocalPos(Vector2 pos) + { + var positions = GetAllPositions(); + if (positions.Count==0) { return 0; } + float halfHeight = Font.MeasureString("T").Y * 0.5f; + + var currPosition = positions[0]; + + for (int i=1;i 3.0f) + { + continue; + } + else + { + diffY = Math.Abs(p1.Item1.Y - pos.Y); + if (diffY < halfHeight) + { + //we are on this line, select the nearest character + float diffX = Math.Abs(p1.Item1.X - pos.X) - Math.Abs(p2.Item1.X - pos.X); + if (diffX < -1.0f) + { + currPosition = p1; continue; + } + else + { + continue; + } + } + else + { + //we are on a different line, preserve order + if (p1.Item2 < p2.Item2) + { + if (p1.Item1.Y > pos.Y) { currPosition = p1; } + } + else if (p1.Item2 > p2.Item2) + { + if (p1.Item1.Y < pos.Y) { currPosition = p1; } + } + continue; + } + } + } //GUI.AddMessage($"index: {posIndex.Item2}, pos: {posIndex.Item1}", Color.WhiteSmoke); - return posIndex != null ? posIndex.Item2 : textBlock.Text.Length; + return currPosition != null ? currPosition.Item2 : textBlock.Text.Length; } public void Select() @@ -674,9 +735,9 @@ namespace Barotrauma { InitSelectionStart(); } - float lineHeight = Font.MeasureString(Text).Y; - int newIndex = GetCaretIndexFromScreenPos(new Vector2(CaretScreenPos.X, CaretScreenPos.Y - lineHeight / 2)); - CaretIndex = newIndex != CaretIndex ? newIndex : 0; + float lineHeight = Font.MeasureString("T").Y; + int newIndex = GetCaretIndexFromLocalPos(new Vector2(caretPos.X, caretPos.Y-lineHeight)); + CaretIndex = newIndex; caretTimer = 0; HandleSelection(); break; @@ -685,9 +746,9 @@ namespace Barotrauma { InitSelectionStart(); } - lineHeight = Font.MeasureString(Text).Y; - newIndex = GetCaretIndexFromScreenPos(new Vector2(CaretScreenPos.X, CaretScreenPos.Y + lineHeight * 2)); - CaretIndex = newIndex != CaretIndex ? newIndex : Text.Length; + lineHeight = Font.MeasureString("T").Y; + newIndex = GetCaretIndexFromLocalPos(new Vector2(caretPos.X, caretPos.Y+lineHeight)); + CaretIndex = newIndex; caretTimer = 0; HandleSelection(); break; diff --git a/Barotrauma/BarotraumaClient/Source/GUI/Widget.cs b/Barotrauma/BarotraumaClient/Source/GUI/Widget.cs index 9fe9ca365..9cd3abb84 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/Widget.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/Widget.cs @@ -114,10 +114,13 @@ namespace Barotrauma if (IsMouseOver || (!RequireMouseOn && selectedWidgets.Contains(this) && PlayerInput.LeftButtonHeld())) { Hovered?.Invoke(); - if ((multiselect && !selectedWidgets.Contains(this)) || selectedWidgets.None()) + if (RequireMouseOn || PlayerInput.LeftButtonDown()) { - selectedWidgets.Add(this); - Selected?.Invoke(); + if ((multiselect && !selectedWidgets.Contains(this)) || selectedWidgets.None()) + { + selectedWidgets.Add(this); + Selected?.Invoke(); + } } } else if (selectedWidgets.Contains(this)) diff --git a/Barotrauma/BarotraumaClient/Source/GameMain.cs b/Barotrauma/BarotraumaClient/Source/GameMain.cs index 13fdede06..655e8f95f 100644 --- a/Barotrauma/BarotraumaClient/Source/GameMain.cs +++ b/Barotrauma/BarotraumaClient/Source/GameMain.cs @@ -101,6 +101,10 @@ namespace Barotrauma private GameTime fixedTime; + public string ConnectName; + public string ConnectEndpoint; + public UInt64 ConnectLobby; + private static SpriteBatch spriteBatch; private Viewport defaultViewport; @@ -175,6 +179,12 @@ namespace Barotrauma ConsoleArguments = args; + ConnectName = null; + ConnectEndpoint = null; + ConnectLobby = 0; + + ToolBox.ParseConnectCommand(ConsoleArguments, out ConnectName, out ConnectEndpoint, out ConnectLobby); + GUI.KeyboardDispatcher = new EventInput.KeyboardDispatcher(Window); PerformanceCounter = new PerformanceCounter(); @@ -355,13 +365,13 @@ namespace Barotrauma } SoundManager = new Sounds.SoundManager(); - SoundManager.SetCategoryGainMultiplier("default", Config.SoundVolume); - SoundManager.SetCategoryGainMultiplier("ui", Config.SoundVolume); - SoundManager.SetCategoryGainMultiplier("waterambience", Config.SoundVolume); - SoundManager.SetCategoryGainMultiplier("music", Config.MusicVolume); - SoundManager.SetCategoryGainMultiplier("voip", Config.VoiceChatVolume * 20.0f); + SoundManager.SetCategoryGainMultiplier("default", Config.SoundVolume, 0); + SoundManager.SetCategoryGainMultiplier("ui", Config.SoundVolume, 0); + SoundManager.SetCategoryGainMultiplier("waterambience", Config.SoundVolume, 0); + SoundManager.SetCategoryGainMultiplier("music", Config.MusicVolume, 0); + SoundManager.SetCategoryGainMultiplier("voip", Config.VoiceChatVolume * 20.0f, 0); - if (ConsoleArguments.Contains("skipintro")) { + if (ConsoleArguments.Contains("-skipintro")) { Config.EnableSplashScreen = false; } @@ -390,7 +400,7 @@ namespace Barotrauma { if (SteamManager.AutoUpdateWorkshopItems()) { - ContentPackage.LoadAll(ContentPackage.Folder); + ContentPackage.LoadAll(); Config.ReloadContentPackages(); } } @@ -494,7 +504,12 @@ namespace Barotrauma if (SteamManager.USE_STEAM) { - SteamWorkshopScreen = new SteamWorkshopScreen(); + SteamWorkshopScreen = new SteamWorkshopScreen(); + if (SteamManager.IsInitialized) + { + SteamManager.Instance.Friends.OnInvitedToGame += OnInvitedToGame; + SteamManager.Instance.Lobby.OnLobbyJoinRequested += OnLobbyJoinRequested; + } } SubEditorScreen = new SubEditorScreen(); @@ -594,6 +609,18 @@ namespace Barotrauma } } + public void OnInvitedToGame(Facepunch.Steamworks.SteamFriend friend, string connectCommand) + { + ToolBox.ParseConnectCommand(connectCommand.Split(' '), out ConnectName, out ConnectEndpoint, out ConnectLobby); + + DebugConsole.NewMessage(ConnectName+", "+ConnectEndpoint,Color.Yellow); + } + + public void OnLobbyJoinRequested(UInt64 lobbyId) + { + SteamManager.JoinLobby(lobbyId, true); + } + /// /// Allows the game to run logic such as updating the world, /// checking for collisions, gathering input, and playing audio. @@ -622,7 +649,7 @@ namespace Barotrauma { if (WindowActive || !Config.MuteOnFocusLost) { - SoundManager.ListenerGain = 1.0f; + SoundManager.ListenerGain = SoundManager.CompressionDynamicRangeGain; } else { @@ -674,6 +701,40 @@ namespace Barotrauma } else if (hasLoaded) { + if (ConnectLobby != 0) + { + if (Client != null) + { + Client.Disconnect(); + Client = null; + + GameMain.MainMenuScreen.Select(); + } + Steam.SteamManager.JoinLobby(ConnectLobby, true); + + ConnectLobby = 0; + ConnectEndpoint = null; + ConnectName = null; + } + else if (!string.IsNullOrWhiteSpace(ConnectEndpoint)) + { + if (Client != null) + { + Client.Disconnect(); + Client = null; + + GameMain.MainMenuScreen.Select(); + } + UInt64 serverSteamId = SteamManager.SteamIDStringToUInt64(ConnectEndpoint); + Client = new GameClient(SteamManager.GetUsername(), + serverSteamId != 0 ? null : ConnectEndpoint, + serverSteamId, + string.IsNullOrWhiteSpace(ConnectName) ? ConnectEndpoint : ConnectName); + ConnectLobby = 0; + ConnectEndpoint = null; + ConnectName = null; + } + SoundPlayer.Update((float)Timing.Step); if (PlayerInput.KeyHit(Keys.Escape) && WindowActive) @@ -754,6 +815,8 @@ namespace Barotrauma SteamManager.Update((float)Timing.Step); + SoundManager?.Update(); + Timing.Accumulator -= Timing.Step; sw.Stop(); diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/Source/GameSession/CrewManager.cs index ab40ee75f..ab0b3e4a2 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/CrewManager.cs @@ -1251,6 +1251,7 @@ namespace Barotrauma if (!hoverArea.Contains(PlayerInput.MousePosition) || PlayerInput.RightButtonClicked()) { orderTargetFrame = null; + OrderOptionButtons.Clear(); } } } @@ -1361,7 +1362,9 @@ namespace Barotrauma { reportButtonFrame.Visible = true; - var reportButtonParent = ChatBox ?? GameMain.Client.ChatBox; + var reportButtonParent = ChatBox ?? GameMain.Client?.ChatBox; + if (reportButtonParent == null) { return; } + reportButtonFrame.RectTransform.AbsoluteOffset = new Point( Math.Min(reportButtonParent.GUIFrame.Rect.X, reportButtonParent.ToggleButton.Rect.X) - reportButtonFrame.Rect.Width - (int)(10 * GUI.Scale), reportButtonParent.GUIFrame.Rect.Y); diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/CampaignMode.cs new file mode 100644 index 000000000..4fed4eec9 --- /dev/null +++ b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/CampaignMode.cs @@ -0,0 +1,18 @@ +using Microsoft.Xna.Framework; +using Barotrauma.Networking; + +namespace Barotrauma +{ + abstract partial class CampaignMode : GameMode + { + public override void ShowStartMessage() + { + if (Mission == null) return; + + new GUIMessageBox(Mission.Name, Mission.Description, new Vector2(0.25f, 0.0f), new Point(400, 200)) + { + UserData = "missionstartmessage" + }; + } + } +} diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/MultiPlayerCampaign.cs index 6be3bacb1..6cca42753 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/MultiPlayerCampaign.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -10,6 +9,8 @@ namespace Barotrauma { partial class MultiPlayerCampaign : CampaignMode { + public bool SuppressStateSending = false; + private UInt16 startWatchmanID, endWatchmanID; public static GUIComponent StartCampaignSetup( IEnumerable submarines, IEnumerable saveFiles) @@ -129,7 +130,7 @@ namespace Barotrauma } } - public void ClientWrite(NetBuffer msg) + public void ClientWrite(IWriteMessage msg) { System.Diagnostics.Debug.Assert(map.Locations.Count < UInt16.MaxValue); @@ -147,7 +148,7 @@ namespace Barotrauma } //static because we may need to instantiate the campaign if it hasn't been done yet - public static void ClientRead(NetBuffer msg) + public static void ClientRead(IReadMessage msg) { byte campaignID = msg.ReadByte(); UInt16 updateID = msg.ReadUInt16(); @@ -213,6 +214,8 @@ namespace Barotrauma if (NetIdUtils.IdMoreRecent(updateID, campaign.lastUpdateID)) { + campaign.SuppressStateSending = true; + campaign.Map.SetLocation(currentLocIndex == UInt16.MaxValue ? -1 : currentLocIndex); campaign.Map.SelectLocation(selectedLocIndex == UInt16.MaxValue ? -1 : selectedLocIndex); campaign.Map.SelectMission(selectedMissionIndex); @@ -236,6 +239,8 @@ namespace Barotrauma } campaign.lastUpdateID = updateID; + + campaign.SuppressStateSending = false; } } diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/CaptainTutorial.cs b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/CaptainTutorial.cs index f45ec0fc9..e8829789b 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/CaptainTutorial.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/CaptainTutorial.cs @@ -218,7 +218,7 @@ namespace Barotrauma.Tutorials } } yield return null; - } while (!captain_sonar.IsActive); + } while (captain_sonar.CurrentMode != Sonar.Mode.Active); do { yield return null; } while (Vector2.Distance(Submarine.MainSub.WorldPosition, Level.Loaded.EndPosition) > 4000f); RemoveCompletedObjective(segments[5]); yield return new WaitForSeconds(4f, false); diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/MechanicTutorial.cs b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/MechanicTutorial.cs index 4b04bd01e..cd20b3d14 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/MechanicTutorial.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/MechanicTutorial.cs @@ -73,6 +73,7 @@ namespace Barotrauma.Tutorials private Character mechanic; private Sprite mechanic_repairIcon; private Color mechanic_repairIconColor; + private Sprite mechanic_weldIcon; public MechanicTutorial(XElement element) : base(element) { @@ -97,6 +98,7 @@ namespace Barotrauma.Tutorials var repairOrder = Order.PrefabList.Find(order => order.AITag == "repairsystems"); mechanic_repairIcon = repairOrder.SymbolSprite; mechanic_repairIconColor = repairOrder.Color; + mechanic_weldIcon = new Sprite("Content/UI/IconAtlas.png", new Rectangle(1, 256, 127, 127), new Vector2(0.5f, 0.5f)); // Other tutorial items tutorial_securityFinalDoorLight = Item.ItemList.Find(i => i.HasTag("tutorial_securityfinaldoorlight")).GetComponent(); @@ -309,7 +311,7 @@ namespace Barotrauma.Tutorials yield return null; } while (!mechanic.HasEquippedItem("divingmask") || !mechanic.HasEquippedItem("weldingtool")); // Wait until equipped SetDoorAccess(mechanic_secondDoor, mechanic_secondDoorLight, true); - mechanic.AddActiveObjectiveEntity(mechanic_brokenWall_1, mechanic_repairIcon, mechanic_repairIconColor); + mechanic.AddActiveObjectiveEntity(mechanic_brokenWall_1, mechanic_weldIcon, mechanic_repairIconColor); do { yield return null; } while (WallHasDamagedSections(mechanic_brokenWall_1)); // Highlight until repaired mechanic.RemoveActiveObjectiveEntity(mechanic_brokenWall_1); RemoveCompletedObjective(segments[2]); @@ -443,10 +445,10 @@ namespace Barotrauma.Tutorials { HighlightInventorySlot(mechanic_fabricator.OutputContainer.Inventory, "extinguisher", highlightColor, .5f, .5f, 0f); - for (int i = 0; i < mechanic.Inventory.slots.Length; i++) + /*for (int i = 0; i < mechanic.Inventory.slots.Length; i++) { if (mechanic.Inventory.Items[i] == null) HighlightInventorySlot(mechanic.Inventory, i, highlightColor, .5f, .5f, 0f); - } + }*/ } else if (mechanic_fabricator.InputContainer.Inventory.FindItemByIdentifier("aluminium") != null && mechanic_fabricator.InputContainer.Inventory.FindItemByIdentifier("sodium") != null && !mechanic_fabricator.IsActive) { @@ -521,7 +523,7 @@ namespace Barotrauma.Tutorials SetDoorAccess(tutorial_mechanicFinalDoor, tutorial_mechanicFinalDoorLight, true); // Room 7 - mechanic.AddActiveObjectiveEntity(mechanic_brokenWall_2, mechanic_repairIcon, mechanic_repairIconColor); + mechanic.AddActiveObjectiveEntity(mechanic_brokenWall_2, mechanic_weldIcon, mechanic_repairIconColor); do { yield return null; } while (WallHasDamagedSections(mechanic_brokenWall_2)); mechanic.RemoveActiveObjectiveEntity(mechanic_brokenWall_2); TriggerTutorialSegment(9, GameMain.Config.KeyBind(InputType.Use)); // Repairing machinery (pump) diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/OfficerTutorial.cs b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/OfficerTutorial.cs index 3b8b62909..ce0cb9f88 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/OfficerTutorial.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/OfficerTutorial.cs @@ -305,10 +305,17 @@ namespace Barotrauma.Tutorials do { float distance = Vector2.Distance(officer_coilgunPeriscope.WorldPosition, officer_hammerhead.WorldPosition); + if (distance > originalDistance * 1.5f) + { + // Don't let the Hammerhead go too far. + officer_hammerhead.TeleportTo(officer_hammerheadSpawnPos + new Vector2(0, -1000)); + } if (distance > originalDistance) { - // Don't let the Hammerhead go too far from the periscope. - officer_hammerhead.TeleportTo(officer_hammerheadSpawnPos); + // Ensure that the Hammerhead targets the player + officer_hammerhead.AIController.SelectTarget(officer.AiTarget); + var ai = officer_hammerhead.AIController as EnemyAIController; + ai.sight = 2.0f; } yield return null; } diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/GameSession.cs b/Barotrauma/BarotraumaClient/Source/GameSession/GameSession.cs index b9d2f5b69..81b5040f3 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/GameSession.cs @@ -110,21 +110,33 @@ namespace Barotrauma { infoFrameContent.ClearChildren(); - var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), infoFrameContent.RectTransform)) + var isTraitor = GameMain.Client?.Character?.IsTraitor ?? false; + + var missionFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, isTraitor ? 0.95f : 0.45f), infoFrameContent.RectTransform)) { RelativeSpacing = 0.05f }; - if (Mission == null) + if (Mission != null) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), paddedFrame.RectTransform, Anchor.TopCenter), TextManager.Get("NoMission")); - return; + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionFrame.RectTransform), Mission.Name, font: GUI.LargeFont); + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionFrame.RectTransform), TextManager.GetWithVariable("MissionReward", "[reward]", Mission.Reward.ToString())); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionFrame.RectTransform), Mission.Description, wrap: true); + } + else + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionFrame.RectTransform, Anchor.TopCenter), TextManager.Get("NoMission"), font: GUI.LargeFont); + } + if (isTraitor) + { + var traitorFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.45f), infoFrameContent.RectTransform, Anchor.BottomLeft)) + { + RelativeSpacing = 0.05f + }; + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), traitorFrame.RectTransform), TextManager.Get("Traitors"), font: GUI.LargeFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), traitorFrame.RectTransform), GameMain.Client.Character.TraitorCurrentObjective, wrap: true); } - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), paddedFrame.RectTransform), Mission.Name, font: GUI.LargeFont); - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), paddedFrame.RectTransform), TextManager.GetWithVariable("MissionReward", "[reward]", Mission.Reward.ToString())); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), Mission.Description, wrap: true); } public void AddToGUIUpdateList() diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/Source/GameSession/RoundSummary.cs index 823e0c6dc..518288032 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/RoundSummary.cs @@ -57,10 +57,11 @@ namespace Barotrauma var infoText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoTextBox.Content.RectTransform), summaryText, wrap: true); + GUIComponent endText = null; if (!string.IsNullOrWhiteSpace(endMessage)) { - var endText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoTextBox.Content.RectTransform), - endMessage, wrap: true); + endText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoTextBox.Content.RectTransform), + TextManager.GetServerMessage(endMessage), wrap: true); } //don't show the mission info if the mission was not completed and there's no localized "mission failed" text available @@ -70,7 +71,9 @@ namespace Barotrauma if (!string.IsNullOrEmpty(message)) { //spacing - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), infoTextBox.Content.RectTransform), style: null); + var spacingTransform = new RectTransform(new Vector2(1.0f, 0.1f), infoTextBox.Content.RectTransform); + + new GUIFrame(spacingTransform, style: null); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoTextBox.Content.RectTransform), TextManager.AddPunctuation(':', TextManager.Get("Mission"), GameMain.GameSession.Mission.Name), @@ -86,12 +89,7 @@ namespace Barotrauma } } } - - foreach (GUIComponent child in infoTextBox.Content.Children) - { - child.CanBeFocused = false; - } - + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), TextManager.Get("RoundSummaryCrewStatus"), font: GUI.LargeFont); @@ -166,6 +164,16 @@ namespace Barotrauma UserData = "buttonarea" }; + paddedFrame.Recalculate(); + foreach (GUIComponent child in infoTextBox.Content.Children) + { + child.CanBeFocused = false; + if (child is GUITextBlock textBlock) + { + textBlock.CalculateHeightFromText(); + } + } + return frame; } } diff --git a/Barotrauma/BarotraumaClient/Source/GameSettings.cs b/Barotrauma/BarotraumaClient/Source/GameSettings.cs index f6939378c..e2515f27d 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSettings.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSettings.cs @@ -73,11 +73,23 @@ namespace Barotrauma public void CreateSettingsFrame(Tab selectedTab = Tab.Graphics) { - settingsFrame = new GUIFrame(new RectTransform(new Vector2(0.8f, 0.8f), GUI.Canvas, Anchor.Center)); + RectTransform settingsHolder = null; + + if (Screen.Selected == GameMain.MainMenuScreen) + { + settingsFrame = new GUIFrame(new RectTransform(new Vector2(0.8f, 0.8f), GUI.Canvas, Anchor.Center)); + settingsHolder = settingsFrame.RectTransform; + } + else + { + settingsFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas), style: null, color: Color.Black * 0.5f); + var settingsFrameContent = new GUIFrame(new RectTransform(new Vector2(0.8f, 0.8f), settingsFrame.RectTransform, Anchor.Center)); + settingsHolder = settingsFrameContent.RectTransform; + } Vector2 tickBoxScale = Vector2.One * 0.05f; - var settingsFramePadding = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), settingsFrame.RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.05f) }) { RelativeSpacing = 0.01f, IsHorizontal = true }; + var settingsFramePadding = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), settingsHolder, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.05f) }) { RelativeSpacing = 0.01f, IsHorizontal = true }; /// General tab -------------------------------------------------------------- @@ -104,6 +116,10 @@ namespace Barotrauma OnSelected = SelectContentPackage, Selected = SelectedContentPackages.Contains(contentPackage) }; + if (contentPackage.CorePackage) + { + tickBox.TextColor = Color.White; + } if (!contentPackage.IsCompatible()) { tickBox.TextColor = Color.Red; @@ -251,11 +267,11 @@ namespace Barotrauma Selected = VSyncEnabled }; - //TODO: remove hardcoded texts after the texts have been added to localization + GUITickBox pauseOnFocusLostBox = new GUITickBox(new RectTransform(tickBoxScale, leftColumn.RectTransform, scaleBasis: ScaleBasis.BothHeight), - TextManager.Get("PauseOnFocusLost", returnNull: true) ?? "Pause on focus lost"); + TextManager.Get("PauseOnFocusLost")); pauseOnFocusLostBox.Selected = PauseOnFocusLost; - pauseOnFocusLostBox.ToolTip = TextManager.Get("PauseOnFocusLostToolTip", returnNull: true) ?? "Pauses the game when its window is not in focus. Note that the game won't be paused when a multiplayer session is active."; + pauseOnFocusLostBox.ToolTip = TextManager.Get("PauseOnFocusLostToolTip"); pauseOnFocusLostBox.OnSelected = (tickBox) => { PauseOnFocusLost = tickBox.Selected; @@ -377,12 +393,12 @@ namespace Barotrauma /// Audio tab ---------------------------------------------------------------- - var audioSliders = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), tabs[(int)Tab.Audio].RectTransform, Anchor.TopCenter) + var audioSliders = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.3f), tabs[(int)Tab.Audio].RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.02f) }) { RelativeSpacing = 0.01f }; - GUITextBlock soundVolumeText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), audioSliders.RectTransform), TextManager.Get("SoundVolume")); - GUIScrollBar soundScrollBar = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.05f), audioSliders.RectTransform), + GUITextBlock soundVolumeText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), audioSliders.RectTransform), TextManager.Get("SoundVolume")); + GUIScrollBar soundScrollBar = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.15f), audioSliders.RectTransform), barSize: 0.05f) { UserData = soundVolumeText, @@ -397,8 +413,8 @@ namespace Barotrauma }; soundScrollBar.OnMoved(soundScrollBar, soundScrollBar.BarScroll); - GUITextBlock musicVolumeText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), audioSliders.RectTransform), TextManager.Get("MusicVolume")); - GUIScrollBar musicScrollBar = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.05f), audioSliders.RectTransform), + GUITextBlock musicVolumeText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), audioSliders.RectTransform), TextManager.Get("MusicVolume")); + GUIScrollBar musicScrollBar = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.15f), audioSliders.RectTransform), barSize: 0.05f) { UserData = musicVolumeText, @@ -413,8 +429,8 @@ namespace Barotrauma }; musicScrollBar.OnMoved(musicScrollBar, musicScrollBar.BarScroll); - GUITextBlock voiceChatVolumeText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), audioSliders.RectTransform), TextManager.Get("VoiceChatVolume")); - GUIScrollBar voiceChatScrollBar = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.05f), audioSliders.RectTransform), + GUITextBlock voiceChatVolumeText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), audioSliders.RectTransform), TextManager.Get("VoiceChatVolume")); + GUIScrollBar voiceChatScrollBar = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.15f), audioSliders.RectTransform), barSize: 0.05f) { UserData = voiceChatVolumeText, @@ -429,7 +445,17 @@ namespace Barotrauma }; voiceChatScrollBar.OnMoved(voiceChatScrollBar, voiceChatScrollBar.BarScroll); - GUITickBox muteOnFocusLostBox = new GUITickBox(new RectTransform(tickBoxScale, audioSliders.RectTransform, scaleBasis: ScaleBasis.BothHeight), TextManager.Get("MuteOnFocusLost")); + var leftTickBoxes = new GUILayoutGroup(new RectTransform(new Vector2(0.28f, 0.06f), tabs[(int)Tab.Audio].RectTransform, Anchor.TopLeft) + { RelativeOffset = new Vector2(0.02f, 0.32f) }) + { RelativeSpacing = 0.01f }; + var centerTickBoxes = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 0.06f), tabs[(int)Tab.Audio].RectTransform, Anchor.TopLeft) + { RelativeOffset = new Vector2(0.3f, 0.32f) }) + { RelativeSpacing = 0.01f }; + var rightTickBoxes = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 0.06f), tabs[(int)Tab.Audio].RectTransform, Anchor.TopRight) + { RelativeOffset = new Vector2(0.02f, 0.32f) }) + { RelativeSpacing = 0.01f }; + + GUITickBox muteOnFocusLostBox = new GUITickBox(new RectTransform(tickBoxScale / 0.06f, leftTickBoxes.RectTransform, scaleBasis: ScaleBasis.BothHeight), TextManager.Get("MuteOnFocusLost")); muteOnFocusLostBox.Selected = MuteOnFocusLost; muteOnFocusLostBox.ToolTip = TextManager.Get("MuteOnFocusLostToolTip"); muteOnFocusLostBox.OnSelected = (tickBox) => @@ -439,7 +465,31 @@ namespace Barotrauma return true; }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), audioSliders.RectTransform), TextManager.Get("VoiceChat")); + GUITickBox dynamicRangeCompressionTickBox = new GUITickBox(new RectTransform(tickBoxScale / 0.06f, centerTickBoxes.RectTransform, scaleBasis: ScaleBasis.BothHeight), TextManager.Get("DynamicRangeCompression")); + dynamicRangeCompressionTickBox.Selected = DynamicRangeCompressionEnabled; + dynamicRangeCompressionTickBox.ToolTip = TextManager.Get("DynamicRangeCompressionToolTip"); + dynamicRangeCompressionTickBox.OnSelected = (tickBox) => + { + DynamicRangeCompressionEnabled = tickBox.Selected; + UnsavedSettings = true; + return true; + }; + + GUITickBox voipAttenuationTickBox = new GUITickBox(new RectTransform(tickBoxScale / 0.06f, rightTickBoxes.RectTransform, scaleBasis: ScaleBasis.BothHeight), TextManager.Get("VoipAttenuation")); + voipAttenuationTickBox.Selected = VoipAttenuationEnabled; + voipAttenuationTickBox.ToolTip = TextManager.Get("VoipAttenuationToolTip"); + voipAttenuationTickBox.OnSelected = (tickBox) => + { + VoipAttenuationEnabled = tickBox.Selected; + UnsavedSettings = true; + return true; + }; + + var voipSettings = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.4f), tabs[(int)Tab.Audio].RectTransform, Anchor.TopCenter) + { RelativeOffset = new Vector2(0.0f, 0.38f) }) + { RelativeSpacing = 0.01f }; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), voipSettings.RectTransform), TextManager.Get("VoiceChat")); IList deviceNames = Alc.GetStringList((IntPtr)null, Alc.CaptureDeviceSpecifier); foreach (string name in deviceNames) @@ -447,7 +497,7 @@ namespace Barotrauma DebugConsole.NewMessage(name + " " + name.Length.ToString(), Color.Lime); } - GUITickBox directionalVoiceChat = new GUITickBox(new RectTransform(tickBoxScale, audioSliders.RectTransform, scaleBasis: ScaleBasis.BothHeight), TextManager.Get("DirectionalVoiceChat")); + GUITickBox directionalVoiceChat = new GUITickBox(new RectTransform(tickBoxScale / 0.4f, voipSettings.RectTransform, scaleBasis: ScaleBasis.BothHeight), TextManager.Get("DirectionalVoiceChat")); directionalVoiceChat.Selected = UseDirectionalVoiceChat; directionalVoiceChat.ToolTip = TextManager.Get("DirectionalVoiceChatToolTip"); directionalVoiceChat.OnSelected = (tickBox) => @@ -466,7 +516,7 @@ namespace Barotrauma VoiceSetting = VoiceMode.Disabled; } #if (!OSX) - var deviceList = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.05f), audioSliders.RectTransform), TrimAudioDeviceName(VoiceCaptureDevice), deviceNames.Count); + var deviceList = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.15f), voipSettings.RectTransform), TrimAudioDeviceName(VoiceCaptureDevice), deviceNames.Count); if (deviceNames?.Count > 0) { foreach (string name in deviceNames) @@ -490,7 +540,7 @@ namespace Barotrauma } #else - var defaultDeviceGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), audioSliders.RectTransform), true, Anchor.CenterLeft); + var defaultDeviceGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.3f), voipSettings.RectTransform), true, Anchor.CenterLeft); var currentDeviceTextBlock = new GUITextBlock(new RectTransform(new Vector2(.7f, 0.75f), null), TextManager.AddPunctuation(':', TextManager.Get("CurrentDevice"), TrimAudioDeviceName(VoiceCaptureDevice))) { @@ -526,13 +576,12 @@ namespace Barotrauma currentDeviceTextBlock.RectTransform.Parent = defaultDeviceGroup.RectTransform; #endif - //var radioButtonFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.12f), audioSliders.RectTransform)); GUIRadioButtonGroup voiceMode = new GUIRadioButtonGroup(); for (int i = 0; i < 3; i++) { string langStr = "VoiceMode." + ((VoiceMode)i).ToString(); - var tick = new GUITickBox(new RectTransform(tickBoxScale, audioSliders.RectTransform, scaleBasis: ScaleBasis.BothHeight), TextManager.Get(langStr)) + var tick = new GUITickBox(new RectTransform(tickBoxScale / 0.4f, voipSettings.RectTransform, scaleBasis: ScaleBasis.BothHeight), TextManager.Get(langStr)) { ToolTip = TextManager.Get(langStr + "ToolTip") }; @@ -540,8 +589,8 @@ namespace Barotrauma voiceMode.AddRadioButton((VoiceMode)i, tick); } - var micVolumeText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), audioSliders.RectTransform), TextManager.Get("MicrophoneVolume")); - var micVolumeSlider = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.05f), audioSliders.RectTransform), + var micVolumeText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), voipSettings.RectTransform), TextManager.Get("MicrophoneVolume")); + var micVolumeSlider = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.15f), voipSettings.RectTransform), barSize: 0.05f) { UserData = micVolumeText, @@ -559,7 +608,7 @@ namespace Barotrauma micVolumeSlider.OnMoved(micVolumeSlider, micVolumeSlider.BarScroll); - var extraVoiceSettingsContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), audioSliders.RectTransform, Anchor.BottomCenter), style: null); + var extraVoiceSettingsContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.6f), voipSettings.RectTransform, Anchor.BottomCenter), style: null); var voiceInputContainer = new GUILayoutGroup(new RectTransform(Vector2.One, extraVoiceSettingsContainer.RectTransform, Anchor.BottomCenter)); new GUITextBlock(new RectTransform(new Vector2(0.6f, 0.25f), voiceInputContainer.RectTransform), TextManager.Get("InputType.Voice")); diff --git a/Barotrauma/BarotraumaClient/Source/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/Source/Items/CharacterInventory.cs index 0acf7811a..37d8138eb 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/CharacterInventory.cs @@ -107,6 +107,17 @@ namespace Barotrauma SetSlotPositions(layout); } + protected override ItemInventory GetActiveEquippedSubInventory(int slotIndex) + { + var item = Items[slotIndex]; + if (item == null) return null; + + var container = item.GetComponent(); + if (container == null || !container.KeepOpenWhenEquipped || !character.HasEquippedItem(container.Item)) return null; + + return container.Inventory; + } + protected override void PutItem(Item item, int i, Character user, bool removeItem = true, bool createNetworkEvent = true) { base.PutItem(item, i, user, removeItem, createNetworkEvent); @@ -456,7 +467,7 @@ namespace Barotrauma } } } - + List hideSubInventories = new List(); foreach (var highlightedSubInventorySlot in highlightedSubInventorySlots) { @@ -465,6 +476,8 @@ namespace Barotrauma UpdateSubInventory(deltaTime, highlightedSubInventorySlot.SlotIndex, cam); } + if (!highlightedSubInventorySlot.Inventory.IsInventoryHoverAvailable(character, null)) continue; + Rectangle hoverArea = GetSubInventoryHoverArea(highlightedSubInventorySlot); if (highlightedSubInventorySlot.Inventory?.slots == null || (!hoverArea.Contains(PlayerInput.MousePosition))) { @@ -476,35 +489,16 @@ namespace Barotrauma } } - if (doubleClickedItem != null) - { - QuickUseItem(doubleClickedItem, true, true, true); - } - //activate the subinventory of the currently selected slot if (selectedSlot?.ParentInventory == this) { var subInventory = GetSubInventory(selectedSlot.SlotIndex); - if (subInventory != null) + if (subInventory != null && subInventory.IsInventoryHoverAvailable(character, null)) { selectedSlot.Inventory = subInventory; if (!highlightedSubInventorySlots.Any(s => s.Inventory == subInventory)) { - var slot = selectedSlot; - highlightedSubInventorySlots.Add(selectedSlot); - UpdateSubInventory(deltaTime, selectedSlot.SlotIndex, cam); - - //hide previously opened subinventories if this one overlaps with them - Rectangle hoverArea = GetSubInventoryHoverArea(slot); - foreach (SlotReference highlightedSubInventorySlot in highlightedSubInventorySlots) - { - if (highlightedSubInventorySlot == slot) continue; - if (hoverArea.Intersects(GetSubInventoryHoverArea(highlightedSubInventorySlot))) - { - hideSubInventories.Add(highlightedSubInventorySlot); - highlightedSubInventorySlot.Inventory.HideTimer = 0.0f; - } - } + ShowSubInventory(selectedSlot, deltaTime, cam, hideSubInventories, false); } } } @@ -513,66 +507,49 @@ namespace Barotrauma { if (subInventorySlot.Inventory == null) continue; subInventorySlot.Inventory.HideTimer -= deltaTime; - if (subInventorySlot.Inventory.HideTimer <= 0.0f) + if (subInventorySlot.Inventory.HideTimer < 0.25f) { highlightedSubInventorySlots.Remove(subInventorySlot); } } - - for (int i = 0; i < capacity; i++) + + if (character.SelectedCharacter == null) // Permanently open subinventories only available when the default UI layout is in use -> not when grabbing characters { - if (Items[i] != null && Items[i].AllowedSlots.Any(a => a != InvSlotType.Any)) + for (int i = 0; i < capacity; i++) { - slots[i].EquipButtonState = slots[i].EquipButtonRect.Contains(PlayerInput.MousePosition) ? - GUIComponent.ComponentState.Hover : GUIComponent.ComponentState.None; - if (PlayerInput.LeftButtonHeld() && PlayerInput.RightButtonHeld()) + var item = Items[i]; + if (item != null) { - slots[i].EquipButtonState = GUIComponent.ComponentState.None; - } - - if (slots[i].EquipButtonState != GUIComponent.ComponentState.Hover) - { - slots[i].QuickUseTimer = Math.Max(0.0f, slots[i].QuickUseTimer - deltaTime * 5.0f); - continue; - } - - var quickUseAction = GetQuickUseAction(Items[i], allowEquip: true, allowInventorySwap: false, allowApplyTreatment: false); - slots[i].QuickUseButtonToolTip = quickUseAction == QuickUseAction.None ? - "" : TextManager.Get("QuickUseAction." + quickUseAction.ToString()); - - //equipped item that can't be put in the inventory, use delayed dropping - if (quickUseAction == QuickUseAction.Drop) - { - slots[i].QuickUseButtonToolTip = - TextManager.Get("QuickUseAction.HoldToUnequip", returnNull: true) ?? - (GameMain.Config.Language == "English" ? "Hold to unequip" : TextManager.Get("QuickUseAction.Unequip")); - - if (PlayerInput.LeftButtonHeld()) + if (HideSlot(i)) continue; + if (character.HasEquippedItem(item)) // Keep a subinventory display open permanently when the container is equipped { - slots[i].QuickUseTimer = Math.Max(0.1f, slots[i].QuickUseTimer + deltaTime); - if (slots[i].QuickUseTimer >= 1.0f) + var itemContainer = item.GetComponent(); + if (itemContainer != null && itemContainer.KeepOpenWhenEquipped && !highlightedSubInventorySlots.Any(s => s.Inventory == itemContainer.Inventory)) { - Items[i].Drop(Character.Controlled); - GUI.PlayUISound(GUISoundType.DropItem); + ShowSubInventory(new SlotReference(this, slots[i], i, false, itemContainer.Inventory), deltaTime, cam, hideSubInventories, true); } } - else - { - slots[i].QuickUseTimer = Math.Max(0.0f, slots[i].QuickUseTimer - deltaTime * 5.0f); - } } - else - { - if (PlayerInput.LeftButtonDown()) slots[i].EquipButtonState = GUIComponent.ComponentState.Pressed; - if (PlayerInput.LeftButtonClicked()) - { - QuickUseItem(Items[i], allowEquip: true, allowInventorySwap: false, allowApplyTreatment: false); - } - } - } } - + + if (doubleClickedItem != null) + { + QuickUseItem(doubleClickedItem, true, true, true); + } + + for (int i = 0; i < capacity; i++) + { + var item = Items[i]; + if (item != null) + { + var slot = slots[i]; + if (item.AllowedSlots.Any(a => a != InvSlotType.Any)) + { + HandleButtonEquipStates(item, slot, deltaTime); + } + } + } //cancel dragging if too far away from the container of the dragged item if (draggingItem != null) @@ -603,6 +580,89 @@ namespace Barotrauma doubleClickedItem = null; } + + private void HandleButtonEquipStates(Item item, InventorySlot slot, float deltaTime) + { + slot.EquipButtonState = slot.EquipButtonRect.Contains(PlayerInput.MousePosition) ? + GUIComponent.ComponentState.Hover : GUIComponent.ComponentState.None; + if (PlayerInput.LeftButtonHeld() && PlayerInput.RightButtonHeld()) + { + slot.EquipButtonState = GUIComponent.ComponentState.None; + } + + if (slot.EquipButtonState != GUIComponent.ComponentState.Hover) + { + slot.QuickUseTimer = Math.Max(0.0f, slot.QuickUseTimer - deltaTime * 5.0f); + return; + } + + var quickUseAction = GetQuickUseAction(item, allowEquip: true, allowInventorySwap: false, allowApplyTreatment: false); + slot.QuickUseButtonToolTip = quickUseAction == QuickUseAction.None ? + "" : TextManager.Get("QuickUseAction." + quickUseAction.ToString()); + + //equipped item that can't be put in the inventory, use delayed dropping + if (quickUseAction == QuickUseAction.Drop) + { + slot.QuickUseButtonToolTip = + TextManager.Get("QuickUseAction.HoldToUnequip", returnNull: true) ?? + (GameMain.Config.Language == "English" ? "Hold to unequip" : TextManager.Get("QuickUseAction.Unequip")); + + if (PlayerInput.LeftButtonHeld()) + { + slot.QuickUseTimer = Math.Max(0.1f, slot.QuickUseTimer + deltaTime); + if (slot.QuickUseTimer >= 1.0f) + { + item.Drop(Character.Controlled); + GUI.PlayUISound(GUISoundType.DropItem); + } + } + else + { + slot.QuickUseTimer = Math.Max(0.0f, slot.QuickUseTimer - deltaTime * 5.0f); + } + } + else + { + if (PlayerInput.LeftButtonDown()) slot.EquipButtonState = GUIComponent.ComponentState.Pressed; + if (PlayerInput.LeftButtonClicked()) + { + QuickUseItem(item, allowEquip: true, allowInventorySwap: false, allowApplyTreatment: false); + } + } + } + + private void ShowSubInventory(SlotReference slotRef, float deltaTime, Camera cam, List hideSubInventories, bool isEquippedSubInventory) + { + Rectangle hoverArea = GetSubInventoryHoverArea(slotRef); + if (isEquippedSubInventory) + { + foreach (SlotReference highlightedSubInventorySlot in highlightedSubInventorySlots) + { + if (highlightedSubInventorySlot == slotRef) continue; + if (hoverArea.Intersects(GetSubInventoryHoverArea(highlightedSubInventorySlot))) + { + return; // If an equipped one intersects with a currently active hover one, do not open + } + } + } + + slotRef.Inventory.OpenState = isEquippedSubInventory ? 1f : 0f; // Reset animation when initially equipped + + highlightedSubInventorySlots.Add(slotRef); + slotRef.Inventory.HideTimer = 1f; + UpdateSubInventory(deltaTime, slotRef.SlotIndex, cam); + + //hide previously opened subinventories if this one overlaps with them + foreach (SlotReference highlightedSubInventorySlot in highlightedSubInventorySlots) + { + if (highlightedSubInventorySlot == slotRef) continue; + if (hoverArea.Intersects(GetSubInventoryHoverArea(highlightedSubInventorySlot))) + { + hideSubInventories.Add(highlightedSubInventorySlot); + highlightedSubInventorySlot.Inventory.HideTimer = 0.0f; + } + } + } private void AssignQuickUseNumKeys() { @@ -633,14 +693,34 @@ namespace Barotrauma if (item.ParentInventory != this) { //in another inventory -> attempt to place in the character's inventory - if (item.ParentInventory.Locked) + if (item.ParentInventory.Locked || item.ParentInventory == null) { return QuickUseAction.None; } else if (allowInventorySwap) { - return item.ParentInventory is CharacterInventory ? - QuickUseAction.TakeFromCharacter : QuickUseAction.TakeFromContainer; + if (item.Container == null || character.Inventory.FindIndex(item.Container) == -1) // Not a subinventory in the character's inventory + { + return item.ParentInventory is CharacterInventory ? + QuickUseAction.TakeFromCharacter : QuickUseAction.TakeFromContainer; + } + else + { + var selectedContainer = character.SelectedConstruction?.GetComponent(); + if (selectedContainer != null && + selectedContainer.Inventory != null && + !selectedContainer.Inventory.Locked && + allowInventorySwap) + { + // Move the item from the subinventory to the selected container + return QuickUseAction.PutToContainer; + } + else + { + // Take from the subinventory and place it in the character's main inventory if no target container is selected + return QuickUseAction.TakeFromContainer; + } + } } } else @@ -756,7 +836,23 @@ namespace Barotrauma } break; case QuickUseAction.TakeFromContainer: - success = TryPutItemWithAutoEquipCheck(item, Character.Controlled, item.AllowedSlots, true); + // Check open subinventories and put the item in it if equipped + ItemInventory activeSubInventory = null; + for (int i = 0; i < capacity; i++) + { + activeSubInventory = GetActiveEquippedSubInventory(i); + if (activeSubInventory != null) + { + success = activeSubInventory.TryPutItem(item, Character.Controlled, item.AllowedSlots, true); + break; + } + } + + // No subinventory found or placing unsuccessful -> attempt to put in the character's inventory + if (!success) + { + success = TryPutItemWithAutoEquipCheck(item, Character.Controlled, item.AllowedSlots, true); + } break; } diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Door.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Door.cs index e624d8ab5..8dd924ac8 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Door.cs @@ -134,7 +134,7 @@ namespace Barotrauma.Items.Components if (item.Submarine != null) pos += item.Submarine.DrawPosition; pos.Y = -pos.Y; - if (brokenSprite == null || item.Health > 0.0f) + if (brokenSprite == null || !IsBroken) { spriteBatch.Draw(doorSprite.Texture, pos, new Rectangle((int) (doorSprite.SourceRect.X + doorSprite.size.X * openState), @@ -160,7 +160,7 @@ namespace Barotrauma.Items.Components if (item.Submarine != null) pos += item.Submarine.DrawPosition; pos.Y = -pos.Y; - if (brokenSprite == null || item.Health > 0.0f) + if (brokenSprite == null || !IsBroken) { spriteBatch.Draw(doorSprite.Texture, pos, new Rectangle(doorSprite.SourceRect.X, @@ -217,7 +217,7 @@ namespace Barotrauma.Items.Components } - public override void ClientRead(ServerNetObject type, Lidgren.Network.NetBuffer msg, float sendingTime) + public override void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { base.ClientRead(type, msg, sendingTime); diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/ItemComponent.cs index 3ff365527..d1ef6d350 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/ItemComponent.cs @@ -1,6 +1,5 @@ using Barotrauma.Networking; using Barotrauma.Sounds; -using Lidgren.Network; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; @@ -188,7 +187,9 @@ namespace Barotrauma.Items.Components if (loopingSound != null) { - if (Vector3.DistanceSquared(GameMain.SoundManager.ListenerPosition, new Vector3(position.X, position.Y, 0.0f)) > loopingSound.Range * loopingSound.Range) + float targetGain = 0.0f; + if (Vector3.DistanceSquared(GameMain.SoundManager.ListenerPosition, new Vector3(position.X, position.Y, 0.0f)) > loopingSound.Range * loopingSound.Range || + (targetGain = GetSoundVolume(loopingSound)) <= 0.0001f) { if (loopingSoundChannel != null) { @@ -220,7 +221,6 @@ namespace Barotrauma.Items.Components lastMuffleCheckTime = (float)Timing.TotalTime; } loopingSoundChannel.Muffled = shouldMuffleLooping; - float targetGain = GetSoundVolume(loopingSound); float gainDiff = targetGain - loopingSoundChannel.Gain; loopingSoundChannel.Gain += Math.Abs(gainDiff) < 0.1f ? gainDiff : Math.Sign(gainDiff) * 0.1f; loopingSoundChannel.Position = new Vector3(position.X, position.Y, 0.0f); @@ -258,7 +258,6 @@ namespace Barotrauma.Items.Components itemSound = matchingSounds[index]; PlaySound(matchingSounds[index], position, user); } - } @@ -278,6 +277,8 @@ namespace Barotrauma.Items.Components } if (loopingSoundChannel == null || !loopingSoundChannel.IsPlaying) { + float volume = GetSoundVolume(itemSound); + if (volume <= 0.0001f) { return; } loopingSoundChannel = loopingSound.RoundSound.Sound.Play( new Vector3(position.X, position.Y, 0.0f), 0.01f, @@ -291,7 +292,7 @@ namespace Barotrauma.Items.Components else { float volume = GetSoundVolume(itemSound); - if (volume <= 0.0f) { return; } + if (volume <= 0.0001f) { return; } SoundPlayer.PlaySound(itemSound.RoundSound.Sound, position, volume, itemSound.Range, item.CurrentHull); } } @@ -465,14 +466,14 @@ namespace Barotrauma.Items.Components } //Starts a coroutine that will read the correct state of the component from the NetBuffer when correctionTimer reaches zero. - protected void StartDelayedCorrection(ServerNetObject type, NetBuffer buffer, float sendingTime, bool waitForMidRoundSync = false) + protected void StartDelayedCorrection(ServerNetObject type, IReadMessage buffer, float sendingTime, bool waitForMidRoundSync = false) { if (delayedCorrectionCoroutine != null) CoroutineManager.StopCoroutines(delayedCorrectionCoroutine); delayedCorrectionCoroutine = CoroutineManager.StartCoroutine(DoDelayedCorrection(type, buffer, sendingTime, waitForMidRoundSync)); } - private IEnumerable DoDelayedCorrection(ServerNetObject type, NetBuffer buffer, float sendingTime, bool waitForMidRoundSync) + private IEnumerable DoDelayedCorrection(ServerNetObject type, IReadMessage buffer, float sendingTime, bool waitForMidRoundSync) { while (GameMain.Client != null && (correctionTimer > 0.0f || (waitForMidRoundSync && GameMain.Client.MidRoundSyncing))) diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/ItemContainer.cs index da3ce0b1d..aaa67e887 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/ItemContainer.cs @@ -73,6 +73,11 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, false)] + public bool KeepOpenWhenEquipped { get; set; } + [Serialize(false, false)] + public bool MovableFrame { get; set; } + public Vector2 DrawSize { //use the extents of the item as the draw size diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/LevelResource.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/LevelResource.cs index b7951ce7d..ec7d506e7 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/LevelResource.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/LevelResource.cs @@ -1,11 +1,10 @@ using Barotrauma.Networking; -using Lidgren.Network; namespace Barotrauma.Items.Components { partial class LevelResource : ItemComponent, IServerSerializable { - public void ClientRead(ServerNetObject type, NetBuffer msg, float sendingTime) + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { deattachTimer = msg.ReadSingle(); if (deattachTimer >= DeattachDuration) diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/LightComponent.cs index 7f676175e..8c25475ee 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/LightComponent.cs @@ -1,6 +1,5 @@ using Barotrauma.Lights; using Barotrauma.Networking; -using Lidgren.Network; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; @@ -36,7 +35,7 @@ namespace Barotrauma.Items.Components } } - public void ClientRead(ServerNetObject type, NetBuffer msg, float sendingTime) + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { IsOn = msg.ReadBoolean(); } diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Controller.cs index ce430e5ee..61b1a9619 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Controller.cs @@ -38,6 +38,8 @@ namespace Barotrauma.Items.Components private void ToggleCrewArea(bool value, bool storeOriginalState) { var crewManager = GameMain.GameSession.CrewManager; + if (crewManager == null) { return; } + if (storeOriginalState) { crewAreaOriginalState = crewManager.ToggleCrewAreaOpen; @@ -48,6 +50,8 @@ namespace Barotrauma.Items.Components private void ToggleChatBox(bool value, bool storeOriginalState) { var crewManager = GameMain.GameSession.CrewManager; + if (crewManager == null) { return; } + if (crewManager.IsSinglePlayer) { if (crewManager.ChatBox != null) diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Deconstructor.cs index dc704f775..fb1c37246 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Deconstructor.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using Lidgren.Network; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System.Linq; @@ -94,12 +93,12 @@ namespace Barotrauma.Items.Components return true; } - public void ClientWrite(NetBuffer msg, object[] extraData = null) + public void ClientWrite(IWriteMessage msg, object[] extraData = null) { msg.Write(pendingState); } - public void ClientRead(ServerNetObject type, NetBuffer msg, float sendingTime) + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { SetActive(msg.ReadBoolean()); progressTimer = msg.ReadSingle(); diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Engine.cs index 6c7404f03..07974570d 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Engine.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using Lidgren.Network; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; @@ -134,13 +133,13 @@ namespace Barotrauma.Items.Components } } - public void ClientWrite(NetBuffer msg, object[] extraData = null) + public void ClientWrite(IWriteMessage msg, object[] extraData = null) { //targetForce can only be adjusted at 10% intervals -> no need for more accuracy than this - msg.WriteRangedInteger(-10, 10, (int)(targetForce / 10.0f)); + msg.WriteRangedIntegerDeprecated(-10, 10, (int)(targetForce / 10.0f)); } - public void ClientRead(ServerNetObject type, NetBuffer msg, float sendingTime) + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { if (correctionTimer > 0.0f) { diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Fabricator.cs index 984885460..ef5f05314 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Fabricator.cs @@ -1,6 +1,5 @@ using Barotrauma.Extensions; using Barotrauma.Networking; -using Lidgren.Network; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; @@ -460,13 +459,13 @@ namespace Barotrauma.Items.Components } } - public void ClientWrite(NetBuffer msg, object[] extraData = null) + public void ClientWrite(IWriteMessage msg, object[] extraData = null) { int itemIndex = pendingFabricatedItem == null ? -1 : fabricationRecipes.IndexOf(pendingFabricatedItem); - msg.WriteRangedInteger(-1, fabricationRecipes.Count - 1, itemIndex); + msg.WriteRangedIntegerDeprecated(-1, fabricationRecipes.Count - 1, itemIndex); } - public void ClientRead(ServerNetObject type, NetBuffer msg, float sendingTime) + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { int itemIndex = msg.ReadRangedInteger(-1, fabricationRecipes.Count - 1); UInt16 userID = msg.ReadUInt16(); @@ -487,4 +486,4 @@ namespace Barotrauma.Items.Components } } } -} \ No newline at end of file +} diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Pump.cs index d588efa0d..9ab572571 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Pump.cs @@ -17,6 +17,7 @@ namespace Barotrauma.Items.Components private GUIScrollBar isActiveSlider; private GUIScrollBar pumpSpeedSlider; private GUITickBox powerIndicator; + private GUITickBox autoControlIndicator; private List> pumpOutEmitters = new List>(); private List> pumpInEmitters = new List>(); @@ -71,12 +72,20 @@ namespace Barotrauma.Items.Components return true; }; - var rightArea = new GUILayoutGroup(new RectTransform(new Vector2(0.75f, 1.0f), paddedFrame.RectTransform, Anchor.CenterRight)) { RelativeSpacing = 0.1f }; + var rightArea = new GUILayoutGroup(new RectTransform(new Vector2(0.75f, 0.95f), paddedFrame.RectTransform, Anchor.CenterRight)) + { + RelativeSpacing = 0.1f, + Stretch = true + }; powerIndicator = new GUITickBox(new RectTransform(new Point((int)(30 * GUI.Scale)), rightArea.RectTransform), TextManager.Get("PumpPowered"), style: "IndicatorLightGreen") { CanBeFocused = false }; + autoControlIndicator = new GUITickBox(new RectTransform(new Point((int)(30 * GUI.Scale)), rightArea.RectTransform), TextManager.Get("PumpAutoControl", fallBackTag: "ReactorAutoControl"), style: "IndicatorLightGreen") + { + CanBeFocused = false + }; var pumpSpeedText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), rightArea.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.0f) }, "", textAlignment: Alignment.BottomLeft, wrap: true); @@ -86,12 +95,12 @@ namespace Barotrauma.Items.Components var sliderArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), rightArea.RectTransform, Anchor.CenterLeft), isHorizontal: true) { Stretch = true, - RelativeSpacing = 0.05f + RelativeSpacing = 0.01f }; - var outLabel = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1.0f), sliderArea.RectTransform), - TextManager.Get("PumpOut"), textAlignment: Alignment.Center, wrap: true, font: GUI.SmallFont); - pumpSpeedSlider = new GUIScrollBar(new RectTransform(new Vector2(0.8f, 1.0f), sliderArea.RectTransform), barSize: 0.25f, style: "GUISlider") + var outLabel = new GUITextBlock(new RectTransform(new Vector2(0.25f, 1.0f), sliderArea.RectTransform), + TextManager.Get("PumpOut"), textAlignment: Alignment.Center, wrap: false, font: GUI.SmallFont); + pumpSpeedSlider = new GUIScrollBar(new RectTransform(new Vector2(0.5f, 1.0f), sliderArea.RectTransform), barSize: 0.25f, style: "GUISlider") { Step = 0.05f, OnMoved = (GUIScrollBar scrollBar, float barScroll) => @@ -109,9 +118,11 @@ namespace Barotrauma.Items.Components return true; } }; - var inLabel = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1.0f), sliderArea.RectTransform), - TextManager.Get("PumpIn"), textAlignment: Alignment.Center, wrap: true, font: GUI.SmallFont); + var inLabel = new GUITextBlock(new RectTransform(new Vector2(0.25f, 1.0f), sliderArea.RectTransform), + TextManager.Get("PumpIn"), textAlignment: Alignment.Center, wrap: false, font: GUI.SmallFont); + rightArea.Recalculate(); + sliderArea.Recalculate(); GUITextBlock.AutoScaleAndNormalize(outLabel, inLabel); } @@ -120,7 +131,6 @@ namespace Barotrauma.Items.Components if (pumpSpeedSlider != null) { pumpSpeedSlider.BarScroll = (flowPercentage + 100.0f) / 200.0f; - } } @@ -151,6 +161,8 @@ namespace Barotrauma.Items.Components public override void UpdateHUD(Character character, float deltaTime, Camera cam) { powerIndicator.Selected = hasPower && IsActive; + autoControlIndicator.Selected = controlLockTimer > 0.0f && IsActive; + pumpSpeedSlider.Enabled = controlLockTimer <= 0.0f && IsActive; if (!PlayerInput.LeftButtonHeld()) { @@ -164,14 +176,14 @@ namespace Barotrauma.Items.Components } } - public void ClientWrite(Lidgren.Network.NetBuffer msg, object[] extraData = null) + public void ClientWrite(IWriteMessage msg, object[] extraData = null) { //flowpercentage can only be adjusted at 10% intervals -> no need for more accuracy than this - msg.WriteRangedInteger(-10, 10, (int)(flowPercentage / 10.0f)); + msg.WriteRangedIntegerDeprecated(-10, 10, (int)(flowPercentage / 10.0f)); msg.Write(IsActive); } - public void ClientRead(ServerNetObject type, Lidgren.Network.NetBuffer msg, float sendingTime) + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { if (correctionTimer > 0.0f) { diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Reactor.cs index dcaff5bb2..d45da755e 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Reactor.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using Lidgren.Network; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; @@ -612,7 +611,7 @@ namespace Barotrauma.Items.Components tempRangeIndicator.Remove(); } - public void ClientWrite(NetBuffer msg, object[] extraData = null) + public void ClientWrite(IWriteMessage msg, object[] extraData = null) { msg.Write(autoTemp); msg.Write(shutDown); @@ -622,7 +621,7 @@ namespace Barotrauma.Items.Components correctionTimer = CorrectionDelay; } - public void ClientRead(ServerNetObject type, NetBuffer msg, float sendingTime) + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { if (correctionTimer > 0.0f) { diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Sonar.cs index 105536133..295bb17cd 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Sonar.cs @@ -332,15 +332,18 @@ namespace Barotrauma.Items.Components float dockingDist = Vector2.Distance(steering.ActiveDockingSource.Item.WorldPosition, steering.DockingTarget.Item.WorldPosition); if (prevDockingDist > steering.DockingAssistThreshold && dockingDist <= steering.DockingAssistThreshold) { - zoom = Math.Max(zoom, MathHelper.Lerp(MinZoom, MaxZoom, 0.25f)); + zoomSlider.BarScroll = 0.25f; + zoom = Math.Max(zoom, MathHelper.Lerp(MinZoom, MaxZoom, zoomSlider.BarScroll)); } else if (prevDockingDist > steering.DockingAssistThreshold * 0.75f && dockingDist <= steering.DockingAssistThreshold * 0.75f) { - zoom = Math.Max(zoom, MathHelper.Lerp(MinZoom, MaxZoom, 0.5f)); + zoomSlider.BarScroll = 0.5f; + zoom = Math.Max(zoom, MathHelper.Lerp(MinZoom, MaxZoom, zoomSlider.BarScroll)); } else if (prevDockingDist > steering.DockingAssistThreshold * 0.5f && dockingDist <= steering.DockingAssistThreshold * 0.5f) { - zoom = Math.Max(zoom, MathHelper.Lerp(MinZoom, MaxZoom, 0.25f)); + zoomSlider.BarScroll = 0.25f; + zoom = Math.Max(zoom, MathHelper.Lerp(MinZoom, MaxZoom, zoomSlider.BarScroll)); } prevDockingDist = Math.Min(dockingDist, prevDockingDist); } @@ -1181,7 +1184,7 @@ namespace Barotrauma.Items.Components 2, GUI.SmallFont); } - public void ClientWrite(Lidgren.Network.NetBuffer msg, object[] extraData = null) + public void ClientWrite(IWriteMessage msg, object[] extraData = null) { msg.Write(currentMode == Mode.Active); if (currentMode == Mode.Active) @@ -1195,9 +1198,9 @@ namespace Barotrauma.Items.Components } } - public void ClientRead(ServerNetObject type, Lidgren.Network.NetBuffer msg, float sendingTime) + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { - long msgStartPos = msg.Position; + int msgStartPos = msg.BitPosition; bool isActive = msg.ReadBoolean(); float zoomT = 1.0f; @@ -1215,8 +1218,8 @@ namespace Barotrauma.Items.Components if (correctionTimer > 0.0f) { - int msgLength = (int)(msg.Position - msgStartPos); - msg.Position = msgStartPos; + int msgLength = (int)(msg.BitPosition - msgStartPos); + msg.BitPosition = msgStartPos; StartDelayedCorrection(type, msg.ExtractBits(msgLength), sendingTime); return; } diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Steering.cs index caec0c63e..b569893e4 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Steering.cs @@ -301,10 +301,9 @@ namespace Barotrauma.Items.Components dockingContainer = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.25f), GuiFrame.RectTransform, Anchor.BottomLeft) { MinSize = new Point(150, 0) }, style: null); var paddedDockingContainer = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), dockingContainer.RectTransform, Anchor.Center), style: null); - - //TODO: add new texts for these ("Dock" & "Undock") - dockText = TextManager.Get("captain.dock"); - undockText = TextManager.Get("captain.undock"); + + dockText = TextManager.Get("label.navterminaldock", fallBackTag: "captain.dock"); + undockText = TextManager.Get("label.navterminalundock", fallBackTag: "captain.undock"); dockingButton = new GUIButton(new RectTransform(new Vector2(0.5f, 0.5f), paddedDockingContainer.RectTransform, Anchor.Center), dockText, style: "GUIButtonLarge") { OnClicked = (btn, userdata) => @@ -786,7 +785,7 @@ namespace Barotrauma.Items.Components steeringIndicator?.Remove(); } - public void ClientWrite(Lidgren.Network.NetBuffer msg, object[] extraData = null) + public void ClientWrite(IWriteMessage msg, object[] extraData = null) { msg.Write(autoPilot); msg.Write(dockingNetworkMessagePending); @@ -813,9 +812,9 @@ namespace Barotrauma.Items.Components } } - public void ClientRead(ServerNetObject type, Lidgren.Network.NetBuffer msg, float sendingTime) + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { - long msgStartPos = msg.Position; + int msgStartPos = msg.BitPosition; bool autoPilot = msg.ReadBoolean(); Vector2 newSteeringInput = steeringInput; @@ -831,8 +830,8 @@ namespace Barotrauma.Items.Components if (maintainPos) { newPosToMaintain = new Vector2( - msg.ReadFloat(), - msg.ReadFloat()); + msg.ReadSingle(), + msg.ReadSingle()); } else { @@ -841,15 +840,15 @@ namespace Barotrauma.Items.Components } else { - newSteeringInput = new Vector2(msg.ReadFloat(), msg.ReadFloat()); - newTargetVelocity = new Vector2(msg.ReadFloat(), msg.ReadFloat()); - newSteeringAdjustSpeed = msg.ReadFloat(); + newSteeringInput = new Vector2(msg.ReadSingle(), msg.ReadSingle()); + newTargetVelocity = new Vector2(msg.ReadSingle(), msg.ReadSingle()); + newSteeringAdjustSpeed = msg.ReadSingle(); } if (correctionTimer > 0.0f) { - int msgLength = (int)(msg.Position - msgStartPos); - msg.Position = msgStartPos; + int msgLength = (int)(msg.BitPosition - msgStartPos); + msg.BitPosition = msgStartPos; StartDelayedCorrection(type, msg.ExtractBits(msgLength), sendingTime); return; } diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Power/PowerContainer.cs index 0f7703b20..9da57eddc 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Power/PowerContainer.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using Lidgren.Network; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; @@ -128,12 +127,12 @@ namespace Barotrauma.Items.Components } - public void ClientWrite(NetBuffer msg, object[] extraData) + public void ClientWrite(IWriteMessage msg, object[] extraData) { - msg.WriteRangedInteger(0, 10, (int)(rechargeSpeed / MaxRechargeSpeed * 10)); + msg.WriteRangedIntegerDeprecated(0, 10, (int)(rechargeSpeed / MaxRechargeSpeed * 10)); } - public void ClientRead(ServerNetObject type, NetBuffer msg, float sendingTime) + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { if (correctionTimer > 0.0f) { diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/RepairTool.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/RepairTool.cs index 8c65373b2..4f609260e 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/RepairTool.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/RepairTool.cs @@ -117,17 +117,20 @@ namespace Barotrauma.Items.Components float progressBarState = targetItem.ConditionPercentage / 100.0f; if (!MathUtils.NearlyEqual(progressBarState, prevProgressBarState)) { - Vector2 progressBarPos = targetItem.DrawPosition; - var progressBar = user.UpdateHUDProgressBar( - targetItem, - progressBarPos, - progressBarState, - Color.Red, Color.Green); - if (progressBar != null) { progressBar.Size = new Vector2(60.0f, 20.0f); } + var door = targetItem.GetComponent(); + if (door == null || door.Stuck <= 0) + { + Vector2 progressBarPos = targetItem.DrawPosition; + var progressBar = user.UpdateHUDProgressBar( + targetItem, + progressBarPos, + progressBarState, + Color.Red, Color.Green); + if (progressBar != null) { progressBar.Size = new Vector2(60.0f, 20.0f); } + } + prevProgressBarState = progressBarState; } - prevProgressBarState = progressBarState; - Vector2 particlePos = ConvertUnits.ToDisplayUnits(pickedPosition); if (targetItem.Submarine != null) particlePos += targetItem.Submarine.DrawPosition; foreach (var emitter in ParticleEmitterHitItem) diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Repairable.cs index c37d81438..ff2511560 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Repairable.cs @@ -1,6 +1,5 @@ using Barotrauma.Networking; using Barotrauma.Particles; -using Lidgren.Network; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System.Collections.Generic; @@ -15,6 +14,11 @@ namespace Barotrauma.Items.Components get { return repairButton; } } private GUIButton repairButton; + public GUIButton SabotageButton + { + get { return sabotageButton; } + } + private GUIButton sabotageButton; private GUIProgressBar progressBar; private List particleEmitters = new List(); @@ -22,6 +26,9 @@ namespace Barotrauma.Items.Components private List particleEmitterConditionRanges = new List(); private string repairButtonText, repairingText; + private string sabotageButtonText, sabotagingText; + + private FixActions requestStartFixAction; [Serialize("", false)] public string Description @@ -39,7 +46,7 @@ namespace Barotrauma.Items.Components public override bool ShouldDrawHUD(Character character) { if (!HasRequiredItems(character, false) || character.SelectedConstruction != item) return false; - return (item.Condition < ShowRepairUIThreshold || (currentFixer == character && !item.IsFullCondition)); + return item.ConditionPercentage < ShowRepairUIThreshold || character.IsTraitor && item.ConditionPercentage > MinSabotageCondition || (CurrentFixer == character && (!item.IsFullCondition || (character.IsTraitor && item.ConditionPercentage > MinSabotageCondition))); } partial void InitProjSpecific(XElement element) @@ -61,24 +68,34 @@ namespace Barotrauma.Items.Components for (int i = 0; i < requiredSkills.Count; i++) { var skillText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), paddedFrame.RectTransform), - " - " + TextManager.AddPunctuation(':', TextManager.Get("SkillName." + requiredSkills[i].Identifier), ((int)requiredSkills[i].Level).ToString()), + " - " + TextManager.AddPunctuation(':', TextManager.Get("SkillName." + requiredSkills[i].Identifier), ((int) requiredSkills[i].Level).ToString()), font: GUI.SmallFont) { UserData = requiredSkills[i] }; } - progressBar = new GUIProgressBar(new RectTransform(new Vector2(1.0f, 0.15f), paddedFrame.RectTransform), + progressBar = new GUIProgressBar(new RectTransform(new Vector2(1.0f, 0.15f), paddedFrame.RectTransform), color: Color.Green, barSize: 0.0f); repairButtonText = TextManager.Get("RepairButton"); repairingText = TextManager.Get("Repairing"); - repairButton = new GUIButton(new RectTransform(new Vector2(0.8f, 0.15f), paddedFrame.RectTransform, Anchor.TopCenter), - repairButtonText) + repairButton = new GUIButton(new RectTransform(new Vector2(0.8f, 0.15f), paddedFrame.RectTransform, Anchor.TopCenter), repairButtonText) { OnClicked = (btn, obj) => { - currentFixer = Character.Controlled; + requestStartFixAction = FixActions.Repair; + item.CreateClientEvent(this); + return true; + } + }; + sabotageButtonText = TextManager.Get("SabotageButton"); + sabotagingText = TextManager.Get("Sabotaging"); + sabotageButton = new GUIButton(new RectTransform(new Vector2(0.8f, 0.15f), paddedFrame.RectTransform, Anchor.BottomCenter), sabotageButtonText) + { + OnClicked = (btn, obj) => + { + requestStartFixAction = FixActions.Sabotage; item.CreateClientEvent(this); return true; } @@ -101,6 +118,21 @@ namespace Barotrauma.Items.Components partial void UpdateProjSpecific(float deltaTime) { + if (!GameMain.IsMultiplayer) + { + switch (requestStartFixAction) + { + case FixActions.Repair: + case FixActions.Sabotage: + StartRepairing(Character.Controlled, requestStartFixAction); + requestStartFixAction = FixActions.None; + break; + default: + requestStartFixAction = FixActions.None; + break; + } + } + for (int i = 0; i < particleEmitters.Count; i++) { if (item.ConditionPercentage >= particleEmitterConditionRanges[i].X && item.ConditionPercentage <= particleEmitterConditionRanges[i].Y) @@ -117,11 +149,17 @@ namespace Barotrauma.Items.Components progressBar.BarSize = item.Condition / item.MaxCondition; progressBar.Color = ToolBox.GradientLerp(progressBar.BarSize, Color.Red, Color.Orange, Color.Green); - repairButton.Enabled = currentFixer == null; - repairButton.Text = currentFixer == null ? + repairButton.Enabled = (currentFixerAction == FixActions.None || (CurrentFixer == character && currentFixerAction != FixActions.Repair)) && item.ConditionPercentage <= ShowRepairUIThreshold; + repairButton.Text = (currentFixerAction == FixActions.None || CurrentFixer != character || currentFixerAction != FixActions.Repair) ? repairButtonText : repairingText + new string('.', ((int)(Timing.TotalTime * 2.0f) % 3) + 1); + sabotageButton.Visible = character.IsTraitor; + sabotageButton.Enabled = (currentFixerAction == FixActions.None || (CurrentFixer == character && currentFixerAction != FixActions.Sabotage)) && character.IsTraitor && item.ConditionPercentage > MinSabotageCondition; + sabotageButton.Text = (currentFixerAction == FixActions.None || CurrentFixer != character || currentFixerAction != FixActions.Sabotage || !character.IsTraitor) ? + sabotageButtonText : + sabotagingText + new string('.', ((int)(Timing.TotalTime * 2.0f) % 3) + 1); + System.Diagnostics.Debug.Assert(GuiFrame.GetChild(0) is GUILayoutGroup, "Repair UI hierarchy has changed, could not find skill texts"); foreach (GUIComponent c in GuiFrame.GetChild(0).Children) { @@ -139,14 +177,18 @@ namespace Barotrauma.Items.Components } } - public void ClientRead(ServerNetObject type, NetBuffer msg, float sendingTime) + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { deteriorationTimer = msg.ReadSingle(); + deteriorateAlwaysResetTimer = msg.ReadSingle(); + DeteriorateAlways = msg.ReadBoolean(); + CurrentFixer = msg.ReadBoolean() ? Character.Controlled : null; + currentFixerAction = (FixActions)msg.ReadRangedInteger(0, 2); } - public void ClientWrite(NetBuffer msg, object[] extraData = null) + public void ClientWrite(IWriteMessage msg, object[] extraData = null) { - //no need to write anything, just letting the server know we started repairing + msg.WriteRangedInteger((int)requestStartFixAction, 0, 2); } public void Draw(SpriteBatch spriteBatch, bool editing) diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Signal/ConnectionPanel.cs index cefe12ae5..9425e5a48 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Signal/ConnectionPanel.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using Lidgren.Network; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; @@ -30,7 +29,7 @@ namespace Barotrauma.Items.Components public override bool ShouldDrawHUD(Character character) { - return character == Character.Controlled && character == user; + return character == Character.Controlled && character == user && character.SelectedConstruction == item; } public override void AddToGUIUpdateList() @@ -40,7 +39,7 @@ namespace Barotrauma.Items.Components public override void UpdateHUD(Character character, float deltaTime, Camera cam) { - if (character != Character.Controlled || character != user) return; + if (character != Character.Controlled || character != user || character.SelectedConstruction != item) { return; } if (HighlightedWire != null) { @@ -64,13 +63,13 @@ namespace Barotrauma.Items.Components } - public void ClientRead(ServerNetObject type, NetBuffer msg, float sendingTime) + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { if (GameMain.Client.MidRoundSyncing) { //delay reading the state until midround syncing is done //because some of the wires connected to the panel may not exist yet - long msgStartPos = msg.Position; + long msgStartPos = msg.BitPosition; foreach (Connection connection in Connections) { for (int i = 0; i < Connection.MaxLinked; i++) @@ -83,8 +82,8 @@ namespace Barotrauma.Items.Components { msg.ReadUInt16(); } - int msgLength = (int)(msg.Position - msgStartPos); - msg.Position = msgStartPos; + int msgLength = (int)(msg.BitPosition - msgStartPos); + msg.BitPosition = (int)msgStartPos; StartDelayedCorrection(type, msg.ExtractBits(msgLength), sendingTime, waitForMidRoundSync: true); } else @@ -93,7 +92,7 @@ namespace Barotrauma.Items.Components } } - private void ApplyRemoteState(NetBuffer msg) + private void ApplyRemoteState(IReadMessage msg) { List prevWires = Connections.SelectMany(c => c.Wires.Where(w => w != null)).ToList(); List newWires = new List(); diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Signal/CustomInterface.cs index 004286d00..daef76609 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Signal/CustomInterface.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -132,7 +131,7 @@ namespace Barotrauma.Items.Components } } - public void ClientWrite(NetBuffer msg, object[] extraData = null) + public void ClientWrite(IWriteMessage msg, object[] extraData = null) { //extradata contains an array of buttons clicked by the player (or nothing if the player didn't click anything) for (int i = 0; i < customInterfaceElementList.Count; i++) @@ -148,7 +147,7 @@ namespace Barotrauma.Items.Components } } - public void ClientRead(ServerNetObject type, NetBuffer msg, float sendingTime) + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { for (int i = 0; i < customInterfaceElementList.Count; i++) { diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Turret.cs index 8795b1dad..1e5c2c62b 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Turret.cs @@ -1,7 +1,6 @@ using Barotrauma.Networking; using Barotrauma.Particles; using Barotrauma.Sounds; -using Lidgren.Network; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; @@ -140,6 +139,11 @@ namespace Barotrauma.Items.Components }; } + public override void Move(Vector2 amount) + { + widgets.Clear(); + } + partial void LaunchProjSpecific() { recoilTimer = Math.Max(Reload, 0.1f); @@ -496,7 +500,7 @@ namespace Barotrauma.Items.Components crosshairPointerSprite?.Draw(spriteBatch, crosshairPointerPos, 0, zoom); } - public void ClientRead(ServerNetObject type, NetBuffer msg, float sendingTime) + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { UInt16 projectileID = msg.ReadUInt16(); //projectile removed, do nothing diff --git a/Barotrauma/BarotraumaClient/Source/Items/Inventory.cs b/Barotrauma/BarotraumaClient/Source/Items/Inventory.cs index 2ebb44dc4..12070f929 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Inventory.cs @@ -1,7 +1,6 @@ using Barotrauma.Extensions; using Barotrauma.Items.Components; using Barotrauma.Networking; -using Lidgren.Network; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; @@ -20,22 +19,22 @@ namespace Barotrauma public bool Disabled; public GUIComponent.ComponentState State; - + public Vector2 DrawOffset; - + public Color Color; public Color HighlightColor; public float HighlightScaleUpAmount; private CoroutineHandle highlightCoroutine; public float HighlightTimer; - + public Sprite SlotSprite; public Keys QuickUseKey; public int SubInventoryDir = -1; - + public bool IsHighlighted { get @@ -130,13 +129,15 @@ namespace Barotrauma protected float prevUIScale = UIScale; protected float prevHUDScale = GUI.Scale; + protected Point prevScreenResolution; protected static Sprite slotSpriteSmall, slotSpriteHorizontal, slotSpriteVertical, slotSpriteRound; public static Sprite EquipIndicator, EquipIndicatorHighlight; public static Sprite DropIndicator, DropIndicatorHighlight; + public static Inventory DraggingInventory; public Rectangle BackgroundFrame { get; protected set; } - + private ushort[] receivedItemIDs; private CoroutineHandle syncItemsCoroutine; @@ -144,6 +145,13 @@ namespace Barotrauma private bool isSubInventory; + private const float movableFrameRectHeight = 40f; + private Color movableFrameRectColor = new Color(60, 60, 60); + private Rectangle movableFrameRect; + private Point savedPosition, originalPos; + private bool canMove = false; + private bool positionUpdateQueued = false; + public class SlotReference { public readonly Inventory ParentInventory; @@ -198,12 +206,12 @@ namespace Barotrauma /// If set, the inventory is automatically positioned inside the rect /// public RectTransform RectTransform; - + public static SlotReference SelectedSlot { get { return selectedSlot; } } - + public virtual void CreateSlots() { slots = new InventorySlot[capacity]; @@ -249,7 +257,7 @@ namespace Barotrauma slotRect.Y = (int)(topLeft.Y + (rectSize.Y + spacing.Y) * ((int)Math.Floor((double)i / slotsPerRow))); slots[i] = new InventorySlot(slotRect); slots[i].InteractRect = new Rectangle( - (int)(slots[i].Rect.X - spacing.X / 2 - 1), (int)(slots[i].Rect.Y - spacing.Y / 2 - 1), + (int)(slots[i].Rect.X - spacing.X / 2 - 1), (int)(slots[i].Rect.Y - spacing.Y / 2 - 1), (int)(slots[i].Rect.Width + spacing.X + 2), (int)(slots[i].Rect.Height + spacing.Y + 2)); if (slots[i].Rect.Width > slots[i].Rect.Height) @@ -273,6 +281,22 @@ namespace Barotrauma { } + public bool Movable() + { + return movableFrameRect.Size != Point.Zero; + } + + public bool IsInventoryHoverAvailable(Character owner, ItemContainer container) + { + if (container == null && this is ItemInventory) + { + container = (this as ItemInventory).Container; + } + + if (container == null) return false; + return owner.SelectedCharacter != null || !container.KeepOpenWhenEquipped || (!(owner is Character)) || !owner.HasEquippedItem(container.Item); + } + protected virtual bool HideSlot(int i) { return slots[i].Disabled || (hideEmptySlot[i] && Items[i] == null); @@ -280,7 +304,7 @@ namespace Barotrauma public virtual void Update(float deltaTime, Camera cam, bool subInventory = false) { - if (slots == null || isSubInventory != subInventory || + if (slots == null || isSubInventory != subInventory || (RectTransform != null && RectTransform.Rect != prevRect)) { CreateSlots(); @@ -341,12 +365,12 @@ namespace Barotrauma } } - + slot.State = GUIComponent.ComponentState.None; - - if (mouseOn && (draggingItem != null || selectedSlot == null || selectedSlot.Slot == slot)) - // && - //(highlightedSubInventories.Count == 0 || highlightedSubInventories.Contains(this) || highlightedSubInventorySlot?.Slot == slot || highlightedSubInventory.Owner == item)) + + if (mouseOn && (draggingItem != null || selectedSlot == null || selectedSlot.Slot == slot) && DraggingInventory == null) + // && + //(highlightedSubInventories.Count == 0 || highlightedSubInventories.Contains(this) || highlightedSubInventorySlot?.Slot == slot || highlightedSubInventory.Owner == item)) { slot.State = GUIComponent.ComponentState.Hover; @@ -369,7 +393,7 @@ namespace Barotrauma { doubleClickedItem = item; } - } + } } } @@ -384,7 +408,12 @@ namespace Barotrauma return container.Inventory; } - float openState; + protected virtual ItemInventory GetActiveEquippedSubInventory(int slotIndex) + { + return null; + } + + public float OpenState; public void UpdateSubInventory(float deltaTime, int slotIndex, Camera cam) { @@ -394,16 +423,45 @@ namespace Barotrauma var container = item.GetComponent(); if (container == null || !container.DrawInventory) return; - var subInventory = container.Inventory; + var subInventory = container.Inventory; if (subInventory.slots == null) subInventory.CreateSlots(); + canMove = container.MovableFrame && !subInventory.IsInventoryHoverAvailable(Owner as Character, container) && subInventory.originalPos != Point.Zero; + + if (canMove) + { + if (subInventory.movableFrameRect.Contains(PlayerInput.MousePosition) && PlayerInput.RightButtonClicked()) + { + container.Inventory.savedPosition = container.Inventory.originalPos; + } + if (subInventory.movableFrameRect.Contains(PlayerInput.MousePosition) || (DraggingInventory != null && DraggingInventory == subInventory)) + { + if (DraggingInventory == null) + { + if (PlayerInput.LeftButtonDown()) + { + DraggingInventory = subInventory; + } + } + else if (PlayerInput.LeftButtonReleased()) + { + DraggingInventory = null; + subInventory.savedPosition = PlayerInput.MousePosition.ToPoint(); + } + else + { + subInventory.savedPosition = PlayerInput.MousePosition.ToPoint(); + } + } + } + int itemCapacity = subInventory.Items.Length; var slot = slots[slotIndex]; int dir = slot.SubInventoryDir; if (itemCapacity == 1 && false) { Point slotSize = (slotSpriteRound.size * UIScale).ToPoint(); - subInventory.slots[0].Rect = + subInventory.slots[0].Rect = new Rectangle(slot.Rect.Center.X - slotSize.X / 2, dir > 0 ? slot.Rect.Bottom + 5 : slot.EquipButtonRect.Bottom + 5, slotSize.X, slotSize.Y); subInventory.slots[0].InteractRect = subInventory.slots[0].Rect; @@ -425,30 +483,43 @@ namespace Barotrauma int width = (int)(subRect.Width * columns + spacing.X * (columns - 1)); int startX = slot.Rect.Center.X - (int)(width / 2.0f); - - //prevent the inventory from extending outside the left side of the screen - startX = Math.Max(startX, 10); - //same for the right side of the screen - startX -= Math.Max((startX + width) - GameMain.GraphicsWidth, 0); - - subRect.X = startX; int startY = dir < 0 ? slot.EquipButtonRect.Y - subRect.Height - (int)(35 * UIScale) : slot.EquipButtonRect.Bottom + (int)(10 * UIScale); - subRect.Y = startY; + + if (canMove) + { + startX += subInventory.savedPosition.X - subInventory.originalPos.X; + startY += subInventory.savedPosition.Y - subInventory.originalPos.Y; + } float totalHeight = itemCapacity / columns * (subRect.Height + spacing.Y); - subInventory.openState = subInventory.HideTimer >= 0.5f ? - Math.Min(subInventory.openState + deltaTime * 5.0f, 1.0f) : - Math.Max(subInventory.openState - deltaTime * 3.0f, 0.0f); + int padding = (int)(20 * UIScale); + + //prevent the inventory from extending outside the left side of the screen + startX = Math.Max(startX, padding); + //same for the right side of the screen + startX -= Math.Max(startX + width - GameMain.GraphicsWidth + padding, 0); + + //prevent the inventory from extending outside the top of the screen + startY = Math.Max(startY, (int)totalHeight - padding / 2); + //same for the bottom side of the screen + startY -= Math.Max(startY - GameMain.GraphicsHeight + padding * 2 + (canMove ? (int)(movableFrameRectHeight * UIScale) : 0), 0); + + subRect.X = startX; + subRect.Y = startY; + + subInventory.OpenState = subInventory.HideTimer >= 0.5f ? + Math.Min(subInventory.OpenState + deltaTime * 5.0f, 1.0f) : + Math.Max(subInventory.OpenState - deltaTime * 3.0f, 0.0f); for (int i = 0; i < itemCapacity; i++) - { + { subInventory.slots[i].Rect = subRect; subInventory.slots[i].Rect.Location += new Point(0, (int)totalHeight * -dir); - subInventory.slots[i].DrawOffset = Vector2.SmoothStep( new Vector2(0, -50 * dir), new Vector2(0, totalHeight * dir), subInventory.openState); - + subInventory.slots[i].DrawOffset = Vector2.SmoothStep(new Vector2(0, -50 * dir), new Vector2(0, totalHeight * dir), subInventory.OpenState); + subInventory.slots[i].InteractRect = new Rectangle( (int)(subInventory.slots[i].Rect.X - spacing.X / 2 - 1), (int)(subInventory.slots[i].Rect.Y - spacing.Y / 2 - 1), (int)(subInventory.slots[i].Rect.Width + spacing.X + 2), (int)(subInventory.slots[i].Rect.Height + spacing.Y + 2)); @@ -463,14 +534,19 @@ namespace Barotrauma { subRect.X = (int)(subInventory.slots[i].Rect.Right + spacing.X); } - } + } + + if (canMove) + { + subInventory.movableFrameRect.X = subRect.X - (int)spacing.X; + subInventory.movableFrameRect.Y = subRect.Y + (int)(spacing.Y / 2f); + } slots[slotIndex].State = GUIComponent.ComponentState.Hover; } - subInventory.isSubInventory = true; + subInventory.isSubInventory = true; subInventory.Update(deltaTime, cam, true); } - public virtual void Draw(SpriteBatch spriteBatch, bool subInventory = false) { if (slots == null || isSubInventory != subInventory) return; @@ -497,7 +573,7 @@ namespace Barotrauma { if (Character.Controlled == null) return false; - if (draggingItem != null) return true; + if (draggingItem != null || DraggingInventory != null) return true; if (Character.Controlled.Inventory != null) { @@ -571,25 +647,32 @@ namespace Barotrauma if (slotIndex < 0 || slotIndex >= Items.Length) return; #endif - Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle; - spriteBatch.End(); - spriteBatch.Begin(SpriteSortMode.Deferred, rasterizerState: GameMain.ScissorTestEnable); - if (slots[slotIndex].SubInventoryDir > 0) + if (!canMove) { - spriteBatch.GraphicsDevice.ScissorRectangle = new Rectangle( - new Point(0, slots[slotIndex].Rect.Bottom), - new Point(GameMain.GraphicsWidth, (int)Math.Max(GameMain.GraphicsHeight - slots[slotIndex].Rect.Bottom, 0))); + Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle; + spriteBatch.End(); + spriteBatch.Begin(SpriteSortMode.Deferred, rasterizerState: GameMain.ScissorTestEnable); + if (slots[slotIndex].SubInventoryDir > 0) + { + spriteBatch.GraphicsDevice.ScissorRectangle = new Rectangle( + new Point(0, slots[slotIndex].Rect.Bottom), + new Point(GameMain.GraphicsWidth, (int)Math.Max(GameMain.GraphicsHeight - slots[slotIndex].Rect.Bottom, 0))); + } + else + { + spriteBatch.GraphicsDevice.ScissorRectangle = new Rectangle( + new Point(0, 0), + new Point(GameMain.GraphicsWidth, slots[slotIndex].Rect.Y)); + } + container.Inventory.Draw(spriteBatch, true); + spriteBatch.End(); + spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect; + spriteBatch.Begin(SpriteSortMode.Deferred); } else { - spriteBatch.GraphicsDevice.ScissorRectangle = new Rectangle( - new Point(0, 0), - new Point(GameMain.GraphicsWidth, slots[slotIndex].Rect.Y)); + container.Inventory.Draw(spriteBatch, true); } - container.Inventory.Draw(spriteBatch, true); - spriteBatch.End(); - spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect; - spriteBatch.Begin(SpriteSortMode.Deferred); container.InventoryBottomSprite?.Draw(spriteBatch, new Vector2(slots[slotIndex].Rect.Center.X, slots[slotIndex].Rect.Y) + slots[slotIndex].DrawOffset, @@ -597,10 +680,33 @@ namespace Barotrauma container.InventoryTopSprite?.Draw(spriteBatch, new Vector2( - slots[slotIndex].Rect.Center.X, + slots[slotIndex].Rect.Center.X, container.Inventory.slots[container.Inventory.slots.Length - 1].Rect.Y) + container.Inventory.slots[container.Inventory.slots.Length - 1].DrawOffset, 0.0f, UIScale); + if (container.MovableFrame && !IsInventoryHoverAvailable(Owner as Character, container)) + { + if (positionUpdateQueued) // Wait a frame before updating the positioning of the container after a resolution change to have everything working + { + container.Inventory.originalPos = container.Inventory.savedPosition = container.Inventory.movableFrameRect.Center; + positionUpdateQueued = false; + } + + if (container.Inventory.movableFrameRect.Size == Point.Zero || GUI.HasSizeChanged(prevScreenResolution, prevUIScale, prevHUDScale)) + { + // Reset position + container.Inventory.savedPosition = container.Inventory.originalPos; + + prevScreenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); + prevUIScale = UIScale; + prevHUDScale = GUI.Scale; + int height = (int)(movableFrameRectHeight * UIScale); + container.Inventory.movableFrameRect = new Rectangle(container.Inventory.BackgroundFrame.X, container.Inventory.BackgroundFrame.Y - height, container.Inventory.BackgroundFrame.Width, height); + positionUpdateQueued = true; + } + + GUI.DrawRectangle(spriteBatch, container.Inventory.movableFrameRect, movableFrameRectColor, true); + } } public static void UpdateDragging() @@ -609,13 +715,13 @@ namespace Barotrauma { Character.Controlled.ClearInputs(); - if (CharacterHealth.OpenHealthWindow != null && + if (CharacterHealth.OpenHealthWindow != null && CharacterHealth.OpenHealthWindow.OnItemDropped(draggingItem, false)) { draggingItem = null; return; } - + if (selectedSlot == null) { if (DraggingItemToWorld && @@ -672,12 +778,23 @@ namespace Barotrauma selectedSlot = null; } } - + protected static Rectangle GetSubInventoryHoverArea(SlotReference subSlot) { - Rectangle hoverArea = subSlot.Slot.Rect; - hoverArea.Location += subSlot.Slot.DrawOffset.ToPoint(); - hoverArea = Rectangle.Union(hoverArea, subSlot.Slot.EquipButtonRect); + Rectangle hoverArea; + if (!subSlot.Inventory.Movable()) + { + hoverArea = subSlot.Slot.Rect; + hoverArea.Location += subSlot.Slot.DrawOffset.ToPoint(); + hoverArea = Rectangle.Union(hoverArea, subSlot.Slot.EquipButtonRect); + } + else + { + hoverArea = subSlot.Inventory.BackgroundFrame; + hoverArea.Location += subSlot.Slot.DrawOffset.ToPoint(); + hoverArea = Rectangle.Union(hoverArea, subSlot.Inventory.movableFrameRect); + } + if (subSlot.Inventory?.slots != null) { foreach (InventorySlot slot in subSlot.Inventory.slots) @@ -697,12 +814,17 @@ namespace Barotrauma hoverArea.Height -= over; } } - hoverArea.Inflate(10, 10); + + float inflateAmount = 10 * UIScale; + + hoverArea.Inflate(inflateAmount, inflateAmount); return hoverArea; } public static void DrawFront(SpriteBatch spriteBatch) { + if (GUI.PauseMenuOpen || GUI.SettingsMenuOpen) return; + foreach (var slot in highlightedSubInventorySlots) { int slotIndex = Array.IndexOf(slot.ParentInventory.slots, slot.Slot); @@ -710,7 +832,7 @@ namespace Barotrauma { slot.ParentInventory.DrawSubInventory(spriteBatch, slotIndex); } - } + } if (draggingItem != null) { @@ -727,7 +849,7 @@ namespace Barotrauma if ((GUI.MouseOn == null || mouseOnHealthInterface) && selectedSlot == null) { var shadowSprite = GUI.Style.GetComponentStyle("OuterGlow").Sprites[GUIComponent.ComponentState.None][0]; - string toolTip = mouseOnHealthInterface ? TextManager.Get("QuickUseAction.UseTreatment") : + string toolTip = mouseOnHealthInterface ? TextManager.Get("QuickUseAction.UseTreatment") : Character.Controlled.FocusedItem != null ? TextManager.GetWithVariable("PutItemIn", "[itemname]", Character.Controlled.FocusedItem.Name, true) : TextManager.Get("DropItem"); @@ -807,7 +929,7 @@ namespace Barotrauma { Rectangle rect = slot.Rect; rect.Location += slot.DrawOffset.ToPoint(); - + if (slot.HighlightColor.A > 0) { float inflateAmount = (slot.HighlightColor.A / 255.0f) * slot.HighlightScaleUpAmount * 0.5f; @@ -860,7 +982,7 @@ namespace Barotrauma Rectangle containedIndicatorArea = new Rectangle(rect.X, dir < 0 ? rect.Bottom + HUDLayoutSettings.Padding / 2 : rect.Y - HUDLayoutSettings.Padding / 2 - ContainedIndicatorHeight, rect.Width, ContainedIndicatorHeight); containedIndicatorArea.Inflate(-4, 0); - + if (itemContainer.ContainedStateIndicator?.Texture == null) { containedIndicatorArea.Inflate(0, -2); @@ -882,7 +1004,7 @@ namespace Barotrauma } indicatorSprite.Draw(spriteBatch, containedIndicatorArea.Center.ToVector2(), - (inventory != null && inventory.Locked) ? Color.DarkGray * 0.5f : Color.DarkGray * 0.9f, + (inventory != null && inventory.Locked) ? Color.DarkGray * 0.5f : Color.DarkGray * 0.9f, origin: indicatorSprite.size / 2, rotate: 0.0f, scale: indicatorScale); @@ -944,19 +1066,19 @@ namespace Barotrauma sprite.Draw(spriteBatch, itemPos, spriteColor, rotation, scale); } - if (inventory != null && + if (inventory != null && !inventory.Locked && - Character.Controlled?.Inventory == inventory && + Character.Controlled?.Inventory == inventory && slot.QuickUseKey != Keys.None) { - GUI.DrawString(spriteBatch, rect.Location.ToVector2(), - slot.QuickUseKey.ToString().Substring(1, 1), - item == null || !drawItem ? Color.Gray : Color.White, + GUI.DrawString(spriteBatch, rect.Location.ToVector2(), + slot.QuickUseKey.ToString().Substring(1, 1), + item == null || !drawItem ? Color.Gray : Color.White, Color.Black * 0.8f); } } - public void ClientRead(ServerNetObject type, NetBuffer msg, float sendingTime) + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { receivedItemIDs = new ushort[capacity]; @@ -1036,7 +1158,7 @@ namespace Barotrauma receivedItemIDs = null; } - public void ClientWrite(NetBuffer msg, object[] extraData = null) + public void ClientWrite(IWriteMessage msg, object[] extraData = null) { SharedWrite(msg, extraData); syncItemsDelay = 1.0f; diff --git a/Barotrauma/BarotraumaClient/Source/Items/Item.cs b/Barotrauma/BarotraumaClient/Source/Items/Item.cs index 9e7d9461f..ac592b1a5 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Item.cs @@ -1,7 +1,6 @@ using Barotrauma.Items.Components; using Barotrauma.Networking; using FarseerPhysics; -using Lidgren.Network; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; @@ -432,23 +431,6 @@ namespace Barotrauma if (!animate) { continue; } spriteState.OffsetState += deltaTime; spriteState.RotationState += deltaTime; - - bool ConditionalMatches(PropertyConditional conditional) - { - if (string.IsNullOrEmpty(conditional.TargetItemComponentName)) - { - if (!conditional.Matches(this)) { return false; } - } - else - { - foreach (ItemComponent component in components) - { - if (component.Name != conditional.TargetItemComponentName) { continue; } - if (!conditional.Matches(component)) { return false; } - } - } - return true; - } } } } @@ -656,10 +638,11 @@ namespace Barotrauma List disallowedAreas = new List(); if (GameMain.GameSession?.CrewManager != null && Screen.Selected == GameMain.GameScreen) { + int disallowedPadding = (int)(50 * GUI.Scale); disallowedAreas.Add(GameMain.GameSession.CrewManager.GetCharacterListArea()); disallowedAreas.Add(new Rectangle( - HUDLayoutSettings.ChatBoxArea.X - 50, HUDLayoutSettings.ChatBoxArea.Y, - HUDLayoutSettings.ChatBoxArea.Width + 50, HUDLayoutSettings.ChatBoxArea.Height)); + HUDLayoutSettings.ChatBoxArea.X - disallowedPadding, HUDLayoutSettings.ChatBoxArea.Y, + HUDLayoutSettings.ChatBoxArea.Width + disallowedPadding, HUDLayoutSettings.ChatBoxArea.Height)); } GUI.PreventElementOverlap(elementsToMove, disallowedAreas, @@ -798,7 +781,7 @@ namespace Barotrauma { if (ic is Repairable repairable) { - if (Condition < repairable.ShowRepairUIThreshold) + if (ConditionPercentage < repairable.ShowRepairUIThreshold) { color = Color.Cyan; } @@ -858,7 +841,7 @@ namespace Barotrauma } } - public void ClientRead(ServerNetObject type, NetBuffer msg, float sendingTime) + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { if (type == ServerNetObject.ENTITY_POSITION) { @@ -868,7 +851,7 @@ namespace Barotrauma NetEntityEvent.Type eventType = (NetEntityEvent.Type)msg.ReadRangedInteger(0, Enum.GetValues(typeof(NetEntityEvent.Type)).Length - 1); - + switch (eventType) { case NetEntityEvent.Type.ComponentState: @@ -939,7 +922,7 @@ namespace Barotrauma } } - public void ClientWrite(NetBuffer msg, object[] extraData = null) + public void ClientWrite(IWriteMessage msg, object[] extraData = null) { if (extraData == null || extraData.Length == 0 || !(extraData[0] is NetEntityEvent.Type)) { @@ -947,17 +930,17 @@ namespace Barotrauma } NetEntityEvent.Type eventType = (NetEntityEvent.Type)extraData[0]; - msg.WriteRangedInteger(0, Enum.GetValues(typeof(NetEntityEvent.Type)).Length - 1, (int)eventType); + msg.WriteRangedIntegerDeprecated(0, Enum.GetValues(typeof(NetEntityEvent.Type)).Length - 1, (int)eventType); switch (eventType) { case NetEntityEvent.Type.ComponentState: int componentIndex = (int)extraData[1]; - msg.WriteRangedInteger(0, components.Count - 1, componentIndex); + msg.WriteRangedIntegerDeprecated(0, components.Count - 1, componentIndex); (components[componentIndex] as IClientSerializable).ClientWrite(msg, extraData); break; case NetEntityEvent.Type.InventoryState: int containerIndex = (int)extraData[1]; - msg.WriteRangedInteger(0, components.Count - 1, containerIndex); + msg.WriteRangedIntegerDeprecated(0, components.Count - 1, containerIndex); (components[containerIndex] as ItemContainer).Inventory.ClientWrite(msg, extraData); break; case NetEntityEvent.Type.Treatment: @@ -1010,7 +993,7 @@ namespace Barotrauma rect.Y = (int)(displayPos.Y + rect.Height / 2.0f); } - public void ClientReadPosition(ServerNetObject type, NetBuffer msg, float sendingTime) + public void ClientReadPosition(ServerNetObject type, IReadMessage msg, float sendingTime) { if (body == null) { @@ -1078,7 +1061,7 @@ namespace Barotrauma GameMain.Client.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.ComponentState, index }); } - public static Item ReadSpawnData(NetBuffer msg, bool spawn = true) + public static Item ReadSpawnData(IReadMessage msg, bool spawn = true) { string itemName = msg.ReadString(); string itemIdentifier = msg.ReadString(); @@ -1140,23 +1123,32 @@ namespace Barotrauma } Inventory inventory = null; - - var inventoryOwner = FindEntityByID(inventoryId); - if (inventoryOwner != null) + if (inventoryId > 0) { - if (inventoryOwner is Character) + var inventoryOwner = FindEntityByID(inventoryId); + if (inventoryOwner is Character character) { - inventory = (inventoryOwner as Character).Inventory; + inventory = character.Inventory; } - else if (inventoryOwner is Item) + else if (inventoryOwner is Item parentItem) { - if ((inventoryOwner as Item).components[itemContainerIndex] is ItemContainer container) + if (itemContainerIndex < 0 || itemContainerIndex >= parentItem.components.Count) + { + string errorMsg = "Failed to spawn item \"" + (itemIdentifier ?? "null") + + "\" in the inventory of \"" + parentItem.prefab.Identifier + "\" (component index out of range). Index: " + itemContainerIndex + ", components: " + parentItem.components.Count + "."; + GameAnalyticsManager.AddErrorEventOnce("Item.ReadSpawnData:ContainerIndexOutOfRange" + (itemName ?? "null") + (itemIdentifier ?? "null"), + GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + errorMsg); + DebugConsole.ThrowError(errorMsg); + } + else if (parentItem.components[itemContainerIndex] is ItemContainer container) { inventory = container.Inventory; } - } + } } + var item = new Item(itemPrefab, pos, sub) { ID = itemId @@ -1197,4 +1189,4 @@ namespace Barotrauma } } } -} \ No newline at end of file +} diff --git a/Barotrauma/BarotraumaClient/Source/Map/Hull.cs b/Barotrauma/BarotraumaClient/Source/Map/Hull.cs index 252662461..c3c89572e 100644 --- a/Barotrauma/BarotraumaClient/Source/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/Source/Map/Hull.cs @@ -1,7 +1,6 @@ using Barotrauma.Networking; using Barotrauma.Particles; using Barotrauma.Sounds; -using Lidgren.Network; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; @@ -528,14 +527,14 @@ namespace Barotrauma } } - public void ClientWrite(NetBuffer msg, object[] extraData = null) + public void ClientWrite(IWriteMessage msg, object[] extraData = null) { msg.WriteRangedSingle(MathHelper.Clamp(waterVolume / Volume, 0.0f, 1.5f), 0.0f, 1.5f, 8); msg.Write(FireSources.Count > 0); if (FireSources.Count > 0) { - msg.WriteRangedInteger(0, 16, Math.Min(FireSources.Count, 16)); + msg.WriteRangedIntegerDeprecated(0, 16, Math.Min(FireSources.Count, 16)); for (int i = 0; i < Math.Min(FireSources.Count, 16); i++) { var fireSource = FireSources[i]; @@ -550,7 +549,7 @@ namespace Barotrauma } } - public void ClientRead(ServerNetObject type, NetBuffer message, float sendingTime) + public void ClientRead(ServerNetObject type, IReadMessage message, float sendingTime) { remoteWaterVolume = message.ReadRangedSingle(0.0f, 1.5f, 8) * Volume; remoteOxygenPercentage = message.ReadRangedSingle(0.0f, 100.0f, 8); diff --git a/Barotrauma/BarotraumaClient/Source/Map/Levels/Level.cs b/Barotrauma/BarotraumaClient/Source/Map/Levels/Level.cs index aea1e2ea7..2137c9261 100644 --- a/Barotrauma/BarotraumaClient/Source/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaClient/Source/Map/Levels/Level.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using Lidgren.Network; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using FarseerPhysics.Dynamics; @@ -95,7 +94,7 @@ namespace Barotrauma renderer.DrawBackground(spriteBatch, cam, levelObjectManager, backgroundCreatureManager); } - public void ClientRead(ServerNetObject type, NetBuffer msg, float sendingTime) + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { foreach (LevelWall levelWall in extraWalls) { diff --git a/Barotrauma/BarotraumaClient/Source/Map/Levels/LevelObjects/LevelObject.cs b/Barotrauma/BarotraumaClient/Source/Map/Levels/LevelObjects/LevelObject.cs index 7637d0ecb..8a2b66f02 100644 --- a/Barotrauma/BarotraumaClient/Source/Map/Levels/LevelObjects/LevelObject.cs +++ b/Barotrauma/BarotraumaClient/Source/Map/Levels/LevelObjects/LevelObject.cs @@ -1,12 +1,12 @@ using Barotrauma.Lights; using Barotrauma.Particles; using Barotrauma.Sounds; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Xml.Linq; using Barotrauma.SpriteDeformations; -using Lidgren.Network; using System.Linq; using FarseerPhysics.Dynamics; @@ -267,7 +267,7 @@ namespace Barotrauma } } - public void ClientRead(NetBuffer msg) + public void ClientRead(IReadMessage msg) { for (int i = 0; i < Triggers.Count; i++) { diff --git a/Barotrauma/BarotraumaClient/Source/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaClient/Source/Map/Levels/LevelObjects/LevelObjectManager.cs index 4bf6eee6a..bf2231d4f 100644 --- a/Barotrauma/BarotraumaClient/Source/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaClient/Source/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using Lidgren.Network; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; @@ -165,7 +164,7 @@ namespace Barotrauma } } - public void ClientRead(ServerNetObject type, NetBuffer msg, float sendingTime) + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { int objIndex = msg.ReadRangedInteger(0, objects.Count); objects[objIndex].ClientRead(msg); diff --git a/Barotrauma/BarotraumaClient/Source/Map/Levels/LevelObjects/LevelTrigger.cs b/Barotrauma/BarotraumaClient/Source/Map/Levels/LevelObjects/LevelTrigger.cs index 600d542e8..dba8bcfcb 100644 --- a/Barotrauma/BarotraumaClient/Source/Map/Levels/LevelObjects/LevelTrigger.cs +++ b/Barotrauma/BarotraumaClient/Source/Map/Levels/LevelObjects/LevelTrigger.cs @@ -1,10 +1,10 @@ -using Lidgren.Network; +using Barotrauma.Networking; namespace Barotrauma { partial class LevelTrigger { - public void ClientRead(NetBuffer msg) + public void ClientRead(IReadMessage msg) { if (ForceFluctuationStrength > 0.0f) { diff --git a/Barotrauma/BarotraumaClient/Source/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/Source/Map/MapEntity.cs index 2d5e84ddb..cb136c4b0 100644 --- a/Barotrauma/BarotraumaClient/Source/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/Source/Map/MapEntity.cs @@ -382,7 +382,7 @@ namespace Barotrauma } else { - selectedList = newSelection; + selectedList = new List(newSelection); //selectedList.Clear(); //newSelection.ForEach(e => AddSelection(e)); foreach (var entity in newSelection) diff --git a/Barotrauma/BarotraumaClient/Source/Map/Structure.cs b/Barotrauma/BarotraumaClient/Source/Map/Structure.cs index a8bca105a..6f18d50c0 100644 --- a/Barotrauma/BarotraumaClient/Source/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/Source/Map/Structure.cs @@ -4,7 +4,6 @@ using Barotrauma.Networking; using FarseerPhysics; using FarseerPhysics.Dynamics; using FarseerPhysics.Dynamics.Contacts; -using Lidgren.Network; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; @@ -346,12 +345,23 @@ namespace Barotrauma } } - public void ClientRead(ServerNetObject type, NetBuffer msg, float sendingTime) + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { - for (int i = 0; i < Sections.Length; i++) + byte sectionCount = msg.ReadByte(); + if (sectionCount != Sections.Length) + { + string errorMsg = $"Error while reading a network event for the structure \"{Name}\". Section count does not match (server: {sectionCount} client: {Sections.Length})"; + DebugConsole.NewMessage(errorMsg, Color.Red); + GameAnalyticsManager.AddErrorEventOnce("Structure.ClientRead:SectionCountMismatch", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + } + + for (int i = 0; i < sectionCount; i++) { float damage = msg.ReadRangedSingle(0.0f, 1.0f, 8) * Health; - SetDamage(i, damage); + if (i < Sections.Length) + { + SetDamage(i, damage); + } } } } diff --git a/Barotrauma/BarotraumaClient/Source/Map/Submarine.cs b/Barotrauma/BarotraumaClient/Source/Map/Submarine.cs index 3063a925b..f5f226950 100644 --- a/Barotrauma/BarotraumaClient/Source/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/Source/Map/Submarine.cs @@ -2,7 +2,6 @@ using Barotrauma.RuinGeneration; using Barotrauma.Sounds; using FarseerPhysics; -using Lidgren.Network; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; @@ -514,7 +513,7 @@ namespace Barotrauma } } - public void ClientRead(ServerNetObject type, NetBuffer msg, float sendingTime) + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { var posInfo = PhysicsBody.ClientRead(type, msg, sendingTime, parentDebugName: Name); msg.ReadPadBits(); diff --git a/Barotrauma/BarotraumaClient/Source/Networking/BanList.cs b/Barotrauma/BarotraumaClient/Source/Networking/BanList.cs index 962e8f436..179732bbd 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/BanList.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/BanList.cs @@ -1,5 +1,4 @@ using Microsoft.Xna.Framework; -using Lidgren.Network; using System; using System.Collections.Generic; @@ -17,7 +16,7 @@ namespace Barotrauma.Networking } } - partial class BanList + public partial class BanList { private GUIComponent banFrame; @@ -129,7 +128,7 @@ namespace Barotrauma.Networking return true; } - public void ClientAdminRead(NetBuffer incMsg) + public void ClientAdminRead(IReadMessage incMsg) { bool hasPermission = incMsg.ReadBoolean(); if (!hasPermission) @@ -142,8 +141,8 @@ namespace Barotrauma.Networking incMsg.ReadPadBits(); bannedPlayers.Clear(); - Int32 bannedPlayerCount = incMsg.ReadVariableInt32(); - for (int i = 0; i < bannedPlayerCount; i++) + UInt32 bannedPlayerCount = incMsg.ReadVariableUInt32(); + for (int i = 0; i < (int)bannedPlayerCount; i++) { string name = incMsg.ReadString(); UInt16 uniqueIdentifier = incMsg.ReadUInt16(); @@ -172,7 +171,7 @@ namespace Barotrauma.Networking } } - public void ClientAdminWrite(NetBuffer outMsg) + public void ClientAdminWrite(IWriteMessage outMsg) { outMsg.Write((UInt16)localRemovedBans.Count); foreach (UInt16 uniqueId in localRemovedBans) diff --git a/Barotrauma/BarotraumaClient/Source/Networking/ChatMessage.cs b/Barotrauma/BarotraumaClient/Source/Networking/ChatMessage.cs index 32c8d5a98..b441ffc38 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/ChatMessage.cs @@ -1,12 +1,11 @@ -using Lidgren.Network; -using System; +using System; using System.Linq; namespace Barotrauma.Networking { partial class ChatMessage { - public virtual void ClientWrite(NetOutgoingMessage msg) + public virtual void ClientWrite(IWriteMessage msg) { msg.Write((byte)ClientNetObject.CHAT_MESSAGE); msg.Write(NetStateID); @@ -14,7 +13,7 @@ namespace Barotrauma.Networking msg.Write(Text); } - public static void ClientRead(NetIncomingMessage msg) + public static void ClientRead(IReadMessage msg) { UInt16 ID = msg.ReadUInt16(); ChatMessageType type = (ChatMessageType)msg.ReadByte(); @@ -37,7 +36,11 @@ namespace Barotrauma.Networking } } - if (type == ChatMessageType.Order) + if (type == ChatMessageType.ServerMessageBox) + { + txt = TextManager.GetServerMessage(txt); + } + else if (type == ChatMessageType.Order) { int orderIndex = msg.ReadByte(); UInt16 targetCharacterID = msg.ReadUInt16(); @@ -63,17 +66,20 @@ namespace Barotrauma.Networking } txt = order.GetChatMessage(targetCharacter?.Name, senderCharacter?.CurrentHull?.DisplayName, givingOrderToSelf: targetCharacter == senderCharacter, orderOption: orderOption); - if (order.TargetAllCharacters) + if (GameMain.Client.GameStarted) { - GameMain.GameSession?.CrewManager?.AddOrder( - new Order(order.Prefab, targetEntity, (targetEntity as Item)?.Components.FirstOrDefault(ic => ic.GetType() == order.ItemComponentType), orderGiver: senderCharacter), - order.Prefab.FadeOutTime); - } - else if (targetCharacter != null) - { - targetCharacter.SetOrder( - new Order(order.Prefab, targetEntity, (targetEntity as Item)?.Components.FirstOrDefault(ic => ic.GetType() == order.ItemComponentType), orderGiver: senderCharacter), - orderOption, senderCharacter); + if (order.TargetAllCharacters) + { + GameMain.GameSession?.CrewManager?.AddOrder( + new Order(order.Prefab, targetEntity, (targetEntity as Item)?.Components.FirstOrDefault(ic => ic.GetType() == order.ItemComponentType), orderGiver: senderCharacter), + order.Prefab.FadeOutTime); + } + else if (targetCharacter != null) + { + targetCharacter.SetOrder( + new Order(order.Prefab, targetEntity, (targetEntity as Item)?.Components.FirstOrDefault(ic => ic.GetType() == order.ItemComponentType), orderGiver: senderCharacter), + orderOption, senderCharacter); + } } if (NetIdUtils.IdMoreRecent(ID, LastID)) @@ -90,7 +96,12 @@ namespace Barotrauma.Networking switch (type) { case ChatMessageType.MessageBox: - new GUIMessageBox("", txt); + case ChatMessageType.ServerMessageBox: + //only show the message box if the text differs from the text in the currently visible box + if ((GUIMessageBox.VisibleBox as GUIMessageBox)?.Text?.Text != txt) + { + new GUIMessageBox("", txt); + } break; case ChatMessageType.Console: DebugConsole.NewMessage(txt, MessageColor[(int)ChatMessageType.Console]); diff --git a/Barotrauma/BarotraumaClient/Source/Networking/Client.cs b/Barotrauma/BarotraumaClient/Source/Networking/Client.cs index 80c53f407..4290225b6 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/Client.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/Client.cs @@ -9,6 +9,7 @@ namespace Barotrauma.Networking struct TempClient { public string Name; + public UInt64 SteamID; public byte ID; public UInt16 CharacterID; public bool Muted; @@ -76,7 +77,7 @@ namespace Barotrauma.Networking VoipQueue = null; VoipSound = null; if (ID == GameMain.Client.ID) return; VoipQueue = new VoipQueue(ID, false, true); - GameMain.Client.VoipClient.RegisterQueue(VoipQueue); + GameMain.Client?.VoipClient?.RegisterQueue(VoipQueue); VoipSound = null; } diff --git a/Barotrauma/BarotraumaClient/Source/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaClient/Source/Networking/EntitySpawner.cs index ff13d9e9c..d414da6a2 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/EntitySpawner.cs @@ -4,7 +4,7 @@ namespace Barotrauma { partial class EntitySpawner : Entity, IServerSerializable { - public void ClientRead(ServerNetObject type, Lidgren.Network.NetBuffer message, float sendingTime) + public void ClientRead(ServerNetObject type, IReadMessage message, float sendingTime) { bool remove = message.ReadBoolean(); diff --git a/Barotrauma/BarotraumaClient/Source/Networking/FileTransfer/FileReceiver.cs b/Barotrauma/BarotraumaClient/Source/Networking/FileTransfer/FileReceiver.cs index d1f3919ab..5126e3de6 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/FileTransfer/FileReceiver.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/FileTransfer/FileReceiver.cs @@ -1,5 +1,4 @@ -using Lidgren.Network; -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.IO; @@ -25,13 +24,13 @@ namespace Barotrauma.Networking private set; } - public ulong FileSize + public int FileSize { get; set; } - public ulong Received + public int Received { get; private set; @@ -72,15 +71,15 @@ namespace Barotrauma.Networking private set; } - public NetConnection Connection + public NetworkConnection Connection { get; private set; } - public int SequenceChannel; + public int ID; - public FileTransferIn(NetConnection connection, string filePath, FileTransferType fileType) + public FileTransferIn(NetworkConnection connection, string filePath, FileTransferType fileType) { FilePath = filePath; FileName = Path.GetFileName(FilePath); @@ -105,17 +104,16 @@ namespace Barotrauma.Networking TimeStarted = Environment.TickCount; } - public void ReadBytes(NetIncomingMessage inc) + public void ReadBytes(IReadMessage inc, int bytesToRead) { - int bytesToRead = inc.LengthBytes - inc.PositionInBytes; - if (Received + (ulong)(bytesToRead) > FileSize) + if (Received + bytesToRead > FileSize) { //strip out excess bytes - bytesToRead -= (int)((Received + (ulong)bytesToRead) - FileSize); + bytesToRead -= Received + bytesToRead - FileSize; } byte[] all = inc.ReadBytes(bytesToRead); - Received += (ulong)all.Length; + Received += all.Length; WriteStream.Write(all, 0, all.Length); int passed = Environment.TickCount - TimeStarted; @@ -162,6 +160,7 @@ namespace Barotrauma.Networking public TransferInDelegate OnTransferFailed; private List activeTransfers; + private List> finishedTransfers; private Dictionary downloadFolders = new Dictionary() { @@ -177,9 +176,10 @@ namespace Barotrauma.Networking public FileReceiver() { activeTransfers = new List(); + finishedTransfers = new List>(); } - public void ReadMessage(NetIncomingMessage inc) + public void ReadMessage(IReadMessage inc) { System.Diagnostics.Debug.Assert(!activeTransfers.Any(t => t.Status == FileTransferStatus.Error || @@ -187,26 +187,38 @@ namespace Barotrauma.Networking 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 transferId = inc.ReadByte(); + var existingTransfer = activeTransfers.Find(t => t.ID == transferId); + finishedTransfers.RemoveAll(t => t.First == transferId); byte fileType = inc.ReadByte(); - ushort chunkLen = inc.ReadUInt16(); - ulong fileSize = inc.ReadUInt64(); + //ushort chunkLen = inc.ReadUInt16(); + int fileSize = inc.ReadInt32(); string fileName = inc.ReadString(); + if (existingTransfer != null) + { + if (fileType != (byte)existingTransfer.FileType || + fileSize != existingTransfer.FileSize || + fileName != existingTransfer.FileName) + { + GameMain.Client.CancelFileTransfer(transferId); + DebugConsole.ThrowError("File transfer error: file transfer initiated with an ID that's already in use"); + } + else //resend acknowledgement packet + { + GameMain.Client.UpdateFileTransfer(transferId, 0); + } + return; + } + if (!ValidateInitialData(fileType, fileName, fileSize, out string errorMsg)) { - GameMain.Client.CancelFileTransfer(inc.SequenceChannel); + GameMain.Client.CancelFileTransfer(transferId); DebugConsole.ThrowError("File transfer failed (" + errorMsg + ")"); return; } @@ -216,7 +228,7 @@ namespace Barotrauma.Networking DebugConsole.Log("Received file transfer initiation message: "); DebugConsole.Log(" File: " + fileName); DebugConsole.Log(" Size: " + fileSize); - DebugConsole.Log(" Sequence channel: " + inc.SequenceChannel); + DebugConsole.Log(" ID: " + transferId); } string downloadFolder = downloadFolders[(FileTransferType)fileType]; @@ -233,9 +245,9 @@ namespace Barotrauma.Networking } } - FileTransferIn newTransfer = new FileTransferIn(inc.SenderConnection, Path.Combine(downloadFolder, fileName), (FileTransferType)fileType) + FileTransferIn newTransfer = new FileTransferIn(inc.Sender, Path.Combine(downloadFolder, fileName), (FileTransferType)fileType) { - SequenceChannel = inc.SequenceChannel, + ID = transferId, Status = FileTransferStatus.Receiving, FileSize = fileSize }; @@ -257,7 +269,7 @@ namespace Barotrauma.Networking else { DebugConsole.NewMessage("Failed to initiate a file transfer {" + e.Message + "}", Color.Red); - GameMain.Client.CancelFileTransfer(inc.SequenceChannel); + GameMain.Client.CancelFileTransfer(transferId); newTransfer.Status = FileTransferStatus.Error; OnTransferFailed(newTransfer); return; @@ -265,10 +277,13 @@ namespace Barotrauma.Networking } } activeTransfers.Add(newTransfer); + + GameMain.Client.UpdateFileTransfer(transferId, 0); //send acknowledgement packet } break; case (byte)FileTransferMessageType.TransferOnSameMachine: { + byte transferId = inc.ReadByte(); byte fileType = inc.ReadByte(); string filePath = inc.ReadString(); @@ -276,19 +291,19 @@ namespace Barotrauma.Networking { DebugConsole.Log("Received file transfer message on the same machine: "); DebugConsole.Log(" File: " + filePath); - DebugConsole.Log(" Sequence channel: " + inc.SequenceChannel); + DebugConsole.Log(" ID: " + transferId); } if (!File.Exists(filePath)) { DebugConsole.ThrowError("File transfer on the same machine failed, file \"" + filePath + "\" not found."); - GameMain.Client.CancelFileTransfer(inc.SequenceChannel); + GameMain.Client.CancelFileTransfer(transferId); return; } - FileTransferIn directTransfer = new FileTransferIn(inc.SenderConnection, filePath, (FileTransferType)fileType) + FileTransferIn directTransfer = new FileTransferIn(inc.Sender, filePath, (FileTransferType)fileType) { - SequenceChannel = inc.SequenceChannel, + ID = transferId, Status = FileTransferStatus.Finished, FileSize = 0 }; @@ -296,79 +311,104 @@ namespace Barotrauma.Networking } 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; - } + byte transferId = inc.ReadByte(); - //allow one extra byte at the end for the 0 that identifies non-compressed messages - if (activeTransfer.Received + (ulong)(inc.LengthBytes - inc.PositionInBytes) > activeTransfer.FileSize + 1) - { - GameMain.Client.CancelFileTransfer(inc.SequenceChannel); - DebugConsole.ThrowError("File transfer error: Received more data than expected (total received: " + activeTransfer.Received + - ", msg received: " + (inc.LengthBytes - inc.PositionInBytes) + - ", msg length: " + inc.LengthBytes + - ", msg read: " + inc.PositionInBytes + - ", filesize: " + activeTransfer.FileSize); - 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(); - - if (ValidateReceivedData(activeTransfer, out string errorMessage)) + var activeTransfer = activeTransfers.Find(t => t.Connection == inc.Sender && t.ID == transferId); + if (activeTransfer == null) { - StopTransfer(activeTransfer); - OnFinished(activeTransfer); + //it's possible for the server to send some extra data + //before it acknowledges that the download is finished, + //so let's suppress the error message in that case + finishedTransfers.RemoveAll(t => t.Second + 5.0 < Timing.TotalTime); + if (!finishedTransfers.Any(t => t.First == transferId)) + { + GameMain.Client.CancelFileTransfer(transferId); + DebugConsole.ThrowError("File transfer error: received data without a transfer initiation message"); + } + return; } - else - { - new GUIMessageBox("File transfer aborted", errorMessage); + int offset = inc.ReadInt32(); + if (offset != activeTransfer.Received) + { + if (offset < activeTransfer.Received) + { + GameMain.Client.UpdateFileTransfer(activeTransfer.ID, activeTransfer.Received); + } + return; + } + + int bytesToRead = inc.ReadUInt16(); + + if (activeTransfer.Received + bytesToRead > activeTransfer.FileSize) + { + GameMain.Client.CancelFileTransfer(transferId); + DebugConsole.ThrowError("File transfer error: Received more data than expected (total received: " + activeTransfer.Received + + ", msg received: " + (inc.LengthBytes - inc.BytePosition) + + ", msg length: " + inc.LengthBytes + + ", msg read: " + inc.BytePosition + + ", filesize: " + activeTransfer.FileSize); + activeTransfer.Status = FileTransferStatus.Error; + StopTransfer(activeTransfer); + return; + } + + try + { + activeTransfer.ReadBytes(inc, bytesToRead); + } + catch (Exception e) + { + GameMain.Client.CancelFileTransfer(transferId); + DebugConsole.ThrowError("File transfer error: " + e.Message); activeTransfer.Status = FileTransferStatus.Error; StopTransfer(activeTransfer, true); + return; + } + + if (activeTransfer.Status == FileTransferStatus.Finished) + { + GameMain.Client.UpdateFileTransfer(activeTransfer.ID, activeTransfer.Received, true); + activeTransfer.Dispose(); + + if (ValidateReceivedData(activeTransfer, out string errorMessage)) + { + finishedTransfers.Add(new Pair(transferId, Timing.TotalTime)); + StopTransfer(activeTransfer); + OnFinished(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); + byte transferId = inc.ReadByte(); + var matchingTransfer = activeTransfers.Find(t => t.Connection == inc.Sender && t.ID == transferId); + if (matchingTransfer != null) + { + new GUIMessageBox("File transfer cancelled", "The server has cancelled the transfer of the file \"" + matchingTransfer.FileName + "\"."); + StopTransfer(matchingTransfer); + } + break; } - break; } } - private bool ValidateInitialData(byte type, string fileName, ulong fileSize, out string errorMessage) + private bool ValidateInitialData(byte type, string fileName, int fileSize, out string errorMessage) { errorMessage = ""; if (fileSize > MaxFileSize) { - errorMessage = "File too large (" + MathUtils.GetBytesReadable((long)fileSize) + ")"; + errorMessage = "File too large (" + MathUtils.GetBytesReadable(fileSize) + ")"; return false; } diff --git a/Barotrauma/BarotraumaClient/Source/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/Source/Networking/GameClient.cs index d5e989882..1de69fbd4 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/GameClient.cs @@ -1,6 +1,5 @@ using Barotrauma.Items.Components; using Barotrauma.Steam; -using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -19,7 +18,8 @@ namespace Barotrauma.Networking get { return true; } } - private NetClient client; + private ClientPeer clientPeer; + public ClientPeer ClientPeer { get { return clientPeer; } } private GUIMessageBox reconnectBox, waitInServerQueueBox; @@ -47,11 +47,10 @@ namespace Barotrauma.Networking private string serverIP, serverName; - private bool needAuth; + private bool allowReconnect; private bool requiresPw; - private int nonce; - private string saltedPw; - private Facepunch.Steamworks.Auth.Ticket steamAuthTicket; + private int pwRetries; + private bool canStart; private UInt16 lastSentChatMsgID = 0; //last message this client has successfully sent private UInt16 lastQueueChatMsgID = 0; //last message added to the queue @@ -63,9 +62,22 @@ namespace Barotrauma.Networking private FileReceiver fileReceiver; +#if DEBUG + public void PrintReceiverTransters() + { + foreach (var transfer in fileReceiver.ActiveTransfers) + { + DebugConsole.NewMessage(transfer.FileName + " " + transfer.Progress.ToString()); + } + } +#endif + //has the client been given a character to control this round public bool HasSpawned; + public bool SpawnAsTraitor; + public string TraitorFirstObjective; + public byte ID { get { return myID; } @@ -95,17 +107,22 @@ namespace Barotrauma.Networking get { return entityEventManager.MidRoundSyncing; } } + private object serverEndpoint; private int ownerKey; + private bool steamP2POwner; public bool IsServerOwner { - get { return ownerKey > 0; } + get { return ownerKey > 0 || steamP2POwner; } } - public GameClient(string newName, string ip, string serverName = null, int ownerKey = 0) + public GameClient(string newName, string ip, UInt64 steamId, string serverName = null, int ownerKey = 0, bool steamP2POwner = false) { //TODO: gui stuff should probably not be here? this.ownerKey = ownerKey; + this.steamP2POwner = steamP2POwner; + + allowReconnect = true; netStats = new NetStats(); @@ -213,7 +230,15 @@ namespace Barotrauma.Networking serverSettings = new ServerSettings(this, "Server", 0, 0, 0, false, false); - ConnectToServer(ip, serverName); + if (steamId == 0) + { + serverEndpoint = ip; + } + else + { + serverEndpoint = steamId; + } + ConnectToServer(serverEndpoint, serverName); //ServerLog = new ServerLog(""); @@ -221,7 +246,7 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen = new NetLobbyScreen(); } - private void ConnectToServer(string hostIP, string hostName) + private void ConnectToServer(object endpoint, string hostName) { chatBox.InputBox.Enabled = false; if (GameMain.NetLobbyScreen?.TextBox != null) @@ -230,63 +255,100 @@ namespace Barotrauma.Networking } serverName = hostName; - - string[] address = hostIP.Split(':'); - if (address.Length == 1) - { - serverIP = hostIP; - Port = NetConfig.DefaultPort; - } - else - { - serverIP = string.Join(":", address.Take(address.Length - 1)); - if (!int.TryParse(address[address.Length - 1], out int port)) - { - DebugConsole.ThrowError("Invalid port: " + address[address.Length - 1] + "!"); - Port = NetConfig.DefaultPort; - } - else - { - Port = port; - } - } - + myCharacter = Character.Controlled; ChatMessage.LastID = 0; - // Create new instance of configs. Parameter is "application Id". It has to be same on client and server. - NetPeerConfiguration = new NetPeerConfiguration("barotrauma"); + clientPeer?.Close(); + clientPeer = null; + object translatedEndpoint = null; + if (endpoint is string hostIP) + { + int port; + string[] address = hostIP.Split(':'); + if (address.Length == 1) + { + serverIP = hostIP; + port = NetConfig.DefaultPort; + } + else + { + serverIP = string.Join(":", address.Take(address.Length - 1)); + if (!int.TryParse(address[address.Length - 1], out port)) + { + DebugConsole.ThrowError("Invalid port: " + address[address.Length - 1] + "!"); + port = NetConfig.DefaultPort; + } + } - NetPeerConfiguration.DisableMessageType(NetIncomingMessageType.DebugMessage | NetIncomingMessageType.WarningMessage | NetIncomingMessageType.Receipt - | NetIncomingMessageType.ErrorMessage | NetIncomingMessageType.Error); + clientPeer = new LidgrenClientPeer(Name); - client = new NetClient(NetPeerConfiguration); - NetPeer = client; - client.Start(); + System.Net.IPEndPoint IPEndPoint = null; + try + { + IPEndPoint = new System.Net.IPEndPoint(Lidgren.Network.NetUtility.Resolve(serverIP), port); + } + catch + { + new GUIMessageBox(TextManager.Get("CouldNotConnectToServer"), + TextManager.GetWithVariables("InvalidIPAddress", new string[2] { "[serverip]", "[port]" }, new string[2] { serverIP, port.ToString() })); + return; + } - System.Net.IPEndPoint IPEndPoint = null; + translatedEndpoint = IPEndPoint; + } + else if (endpoint is UInt64) + { + if (steamP2POwner) + { + clientPeer = new SteamP2POwnerPeer(Name); + } + else + { + clientPeer = new SteamP2PClientPeer(Name); + } + + translatedEndpoint = endpoint; + } + clientPeer.OnDisconnect = OnDisconnect; + clientPeer.OnInitializationComplete = () => + { + if (SteamManager.IsInitialized) + { + SteamManager.Instance.User.ClearRichPresence(); + SteamManager.Instance.User.SetRichPresence("status", "Playing on " + serverName); + SteamManager.Instance.User.SetRichPresence("connect", "-connect \"" + serverName.Replace("\"","\\\"") + "\" " + serverEndpoint); + } + + canStart = true; + connected = true; + + if (Screen.Selected != GameMain.GameScreen) + { + GameMain.NetLobbyScreen.Select(); + } + + chatBox.InputBox.Enabled = true; + if (GameMain.NetLobbyScreen?.TextBox != null) + { + GameMain.NetLobbyScreen.TextBox.Enabled = true; + } + }; + clientPeer.OnRequestPassword = (int salt, int retries) => + { + if (pwRetries != retries) { requiresPw = true; } + pwRetries = retries; + }; + clientPeer.OnMessageReceived = ReadDataMessage; + + // Connect client, to endpoint previously requested from user try { - IPEndPoint = new System.Net.IPEndPoint(NetUtility.Resolve(serverIP), Port); - } - catch - { - new GUIMessageBox(TextManager.Get("CouldNotConnectToServer"), - TextManager.GetWithVariables("InvalidIPAddress", new string[2] { "[serverip]", "[port]" }, new string[2] { serverIP, Port.ToString() })); - return; - } - - NetOutgoingMessage outmsg = client.CreateMessage(); - WriteAuthRequest(outmsg); - - // Connect client, to ip previously requested from user - try - { - client.Connect(IPEndPoint, outmsg); + clientPeer.Start(translatedEndpoint, ownerKey); } catch (Exception e) { - DebugConsole.ThrowError("Couldn't connect to " + hostIP + ". Error message: " + e.Message); + DebugConsole.ThrowError("Couldn't connect to " + endpoint.ToString() + ". Error message: " + e.Message); Disconnect(); chatBox.InputBox.Enabled = true; if (GameMain.NetLobbyScreen?.TextBox != null) @@ -301,178 +363,112 @@ namespace Barotrauma.Networking CoroutineManager.StartCoroutine(WaitForStartingInfo(), "WaitForStartingInfo"); } - + private bool RetryConnection(GUIButton button, object obj) { - if (client != null) client.Shutdown(TextManager.Get("Disconnecting")); - ConnectToServer(serverIP, serverName); + if (clientPeer != null) { clientPeer.Close(); } + clientPeer = null; + ConnectToServer(serverEndpoint, serverName); return true; } - private bool ReturnToServerList(GUIButton button, object obj) + private bool ReturnToPreviousMenu(GUIButton button, object obj) { Disconnect(); Submarine.Unload(); GameMain.Client = null; GameMain.GameSession = null; - GameMain.ServerListScreen.Select(); + if (IsServerOwner) + { + GameMain.MainMenuScreen.Select(); + } + else + { + GameMain.ServerListScreen.Select(); + } + + GUIMessageBox.MessageBoxes.RemoveAll(m => true); return true; } - + private bool connectCancelled; private void CancelConnect() { + if (!(GameMain.ServerChildProcess?.HasExited??true)) + { + GameMain.ServerChildProcess.Kill(); + GameMain.ServerChildProcess = null; + } + connectCancelled = true; - steamAuthTicket?.Cancel(); - steamAuthTicket = null; + clientPeer?.Close(); + clientPeer = null; } // Before main looping starts, we loop here and wait for approval message private IEnumerable WaitForStartingInfo() { requiresPw = false; - needAuth = true; - saltedPw = ""; + pwRetries = -1; connectCancelled = false; // When this is set to true, we are approved and ready to go - bool CanStart = false; + canStart = false; DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, 20); DateTime reqAuthTime = DateTime.Now + new TimeSpan(0, 0, 0, 0, 200); // Loop until we are approved - //TODO: show the name of the server instead of IP when connecting through the server list (more streamer-friendly) string connectingText = TextManager.Get("Connecting"); - while (!CanStart && !connectCancelled) + while (!canStart && !connectCancelled) { - if (reconnectBox == null) + if (reconnectBox == null && waitInServerQueueBox == null) { + string serverDisplayName = serverName; + if (string.IsNullOrEmpty(serverDisplayName)) { serverDisplayName = serverIP; } + if (string.IsNullOrEmpty(serverDisplayName) && clientPeer?.ServerConnection is SteamP2PConnection steamConnection) + { + serverDisplayName = steamConnection.SteamID.ToString(); + if (SteamManager.IsInitialized) + { + string steamUserName = SteamManager.Instance.Friends.GetName(steamConnection.SteamID); + if (!string.IsNullOrEmpty(steamUserName) && steamUserName != "[unknown]") + { + serverDisplayName = steamUserName; + } + } + } + if (string.IsNullOrEmpty(serverDisplayName)) { serverDisplayName = TextManager.Get("Unknown"); } + reconnectBox = new GUIMessageBox( connectingText, - TextManager.GetWithVariable("ConnectingTo", "[serverip]", string.IsNullOrEmpty(serverName) ? serverIP : serverName), + TextManager.GetWithVariable("ConnectingTo", "[serverip]", serverDisplayName), new string[] { TextManager.Get("Cancel") }); reconnectBox.Buttons[0].OnClicked += (btn, userdata) => { CancelConnect(); return true; }; reconnectBox.Buttons[0].OnClicked += reconnectBox.Close; } - reconnectBox.Header.Text = connectingText + new string('.', ((int)Timing.TotalTime % 3 + 1)); - - if (DateTime.Now > reqAuthTime) + if (reconnectBox != null) { - if (needAuth) - { - //request auth again - NetOutgoingMessage reqAuthMsg = client.CreateMessage(); - WriteAuthRequest(reqAuthMsg); - client.SendMessage(reqAuthMsg, NetDeliveryMethod.Unreliable); - } - else - { - //request init again - NetOutgoingMessage outmsg = client.CreateMessage(); - outmsg.Write((byte)ClientPacketHeader.REQUEST_INIT); - if (requiresPw) - { - outmsg.Write(saltedPw); - } - outmsg.Write(GameMain.Version.ToString()); - - var mpContentPackages = GameMain.SelectedPackages.Where(cp => cp.HasMultiplayerIncompatibleContent); - outmsg.Write((UInt16)mpContentPackages.Count()); - foreach (ContentPackage contentPackage in mpContentPackages) - { - outmsg.Write(contentPackage.Name); - outmsg.Write(contentPackage.MD5hash.Hash); - } - outmsg.Write(name); - client.SendMessage(outmsg, NetDeliveryMethod.Unreliable); - - DebugConsole.Log("Sending init request (" + (requiresPw ? "password required" : "no password required") + ")"); - } - reqAuthTime = DateTime.Now + new TimeSpan(0, 0, 1); + reconnectBox.Header.Text = connectingText + new string('.', ((int)Timing.TotalTime % 3 + 1)); } yield return CoroutineStatus.Running; - if (DateTime.Now > timeOut) break; - - NetIncomingMessage inc; - // If new messages arrived - if ((inc = client.ReadMessage()) == null) continue; - - string pwMsg = TextManager.Get("PasswordRequired"); - - try + if (DateTime.Now > timeOut) { - switch (inc.MessageType) - { - case NetIncomingMessageType.Data: - DecompressIncomingMessage(inc); - - ServerPacketHeader header = (ServerPacketHeader)inc.ReadByte(); - switch (header) - { - case ServerPacketHeader.AUTH_RESPONSE: - DebugConsole.Log("Received auth response (needauth: " + needAuth + ")"); - if (needAuth) - { - if (inc.ReadBoolean()) - { - DebugConsole.Log(" password required."); - //requires password - nonce = inc.ReadInt32(); - requiresPw = true; - } - else - { - DebugConsole.Log(" password not required."); - requiresPw = false; - reqAuthTime = DateTime.Now + new TimeSpan(0, 0, 0, 0, 200); - } - needAuth = false; //got auth! - } - break; - case ServerPacketHeader.AUTH_FAILURE: - //failed to authenticate, can still use same nonce - DebugConsole.Log("Received auth failure message"); - pwMsg = inc.ReadString(); - requiresPw = true; - break; - case ServerPacketHeader.UPDATE_LOBBY: - DebugConsole.Log("Recived lobby update"); - //server accepted client - ReadLobbyUpdate(inc); - CanStart = true; - break; - } - break; - case NetIncomingMessageType.StatusChanged: - NetConnectionStatus connectionStatus = (NetConnectionStatus)inc.ReadByte(); - if (connectionStatus == NetConnectionStatus.Disconnected) - { - ReadDisconnectMessage(inc, false); - CancelConnect(); - } - break; - } - } - - catch (Exception e) - { - DebugConsole.ThrowError("Error while connecting to the server", e); + clientPeer?.Close(Lidgren.Network.NetConnection.NoResponseMessage); + reconnectBox?.Close(); reconnectBox = null; break; } - - if (requiresPw && !CanStart && !connectCancelled) + + if (requiresPw && !canStart && !connectCancelled) { - if (reconnectBox != null) - { - reconnectBox.Close(null, null); - reconnectBox = null; - } + reconnectBox?.Close(); reconnectBox = null; + + string pwMsg = TextManager.Get("PasswordRequired"); var msgBox = new GUIMessageBox(pwMsg, "", new string[] { TextManager.Get("OK"), TextManager.Get("Cancel") }, relativeSize: new Vector2(0.25f, 0.2f), minSize: new Point(400, 150)); @@ -486,138 +482,39 @@ namespace Barotrauma.Networking var okButton = msgBox.Buttons[0]; var cancelButton = msgBox.Buttons[1]; + okButton.OnClicked += (GUIButton button, object obj) => + { + clientPeer.SendPassword(passwordBox.Text); + requiresPw = false; + return true; + }; + + cancelButton.OnClicked += (GUIButton button, object obj) => + { + requiresPw = false; + connectCancelled = true; + return true; + }; + while (GUIMessageBox.MessageBoxes.Contains(msgBox)) { - while (client.ReadMessage() != null) + if (!requiresPw) { - switch (inc.MessageType) - { - case NetIncomingMessageType.StatusChanged: - NetConnectionStatus connectionStatus = (NetConnectionStatus)inc.ReadByte(); - if (connectionStatus == NetConnectionStatus.Disconnected) - { - ReadDisconnectMessage(inc, false); - msgBox.Close(null, null); - CancelConnect(); - } - break; - } - } - - if (DateTime.Now > reqAuthTime) - { - //request auth again to prevent timeout - NetOutgoingMessage reqAuthMsg = client.CreateMessage(); - WriteAuthRequest(reqAuthMsg); - client.SendMessage(reqAuthMsg, NetDeliveryMethod.Unreliable); - reqAuthTime = DateTime.Now + new TimeSpan(0, 0, 3); - } - - okButton.Enabled = !string.IsNullOrWhiteSpace(passwordBox.Text); - - if (okButton.Selected) - { - saltedPw = Encoding.UTF8.GetString(NetUtility.ComputeSHAHash(Encoding.UTF8.GetBytes(passwordBox.Text))); - saltedPw = saltedPw + Convert.ToString(nonce); - saltedPw = Encoding.UTF8.GetString(NetUtility.ComputeSHAHash(Encoding.UTF8.GetBytes(saltedPw))); - - timeOut = DateTime.Now + new TimeSpan(0, 0, 20); - reqAuthTime = DateTime.Now + new TimeSpan(0, 0, 1); - - msgBox.Close(null, null); + msgBox.Close(); break; } - else if (cancelButton.Selected) - { - msgBox.Close(null, null); - CancelConnect(); - } - else - { - yield return CoroutineStatus.Running; - } + yield return CoroutineStatus.Running; } } } - if (reconnectBox != null) - { - reconnectBox.Close(null, null); - reconnectBox = null; - } + reconnectBox?.Close(); reconnectBox = null; if (connectCancelled) yield return CoroutineStatus.Success; - - if (client.ConnectionStatus != NetConnectionStatus.Connected) - { - steamAuthTicket?.Cancel(); - steamAuthTicket = null; - var reconnect = new GUIMessageBox(TextManager.Get("ConnectionFailed"), TextManager.Get("CouldNotConnectToServer"), new string[] { TextManager.Get("Retry"), TextManager.Get("Cancel") }); - - DebugConsole.NewMessage("Failed to connect to the server - connection status: " + client.ConnectionStatus.ToString(), Color.Orange); - - reconnect.Buttons[0].OnClicked += RetryConnection; - reconnect.Buttons[0].OnClicked += reconnect.Close; - reconnect.Buttons[1].OnClicked += ReturnToServerList; - reconnect.Buttons[1].OnClicked += reconnect.Close; - } - else - { - if (Screen.Selected != GameMain.GameScreen) - { - GameMain.NetLobbyScreen.Select(); - } - connected = true; - chatBox.InputBox.Enabled = true; - if (GameMain.NetLobbyScreen?.TextBox != null) - { - GameMain.NetLobbyScreen.TextBox.Enabled = true; - } - } - + yield return CoroutineStatus.Success; } - private void WriteAuthRequest(NetOutgoingMessage outmsg) - { - if (SteamManager.IsInitialized && SteamManager.USE_STEAM) - { - outmsg.Write((byte)ClientPacketHeader.REQUEST_STEAMAUTH); - } - else - { - DebugConsole.Log("Sending auth request"); - outmsg.Write((byte)ClientPacketHeader.REQUEST_AUTH); - } - - outmsg.Write(ownerKey); - - if (SteamManager.IsInitialized && SteamManager.USE_STEAM) - { - if (steamAuthTicket == null) - { - steamAuthTicket = SteamManager.GetAuthSessionTicket(); - } - - DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, 3); - while ((steamAuthTicket.Data == null || steamAuthTicket.Data.Length == 0) && - DateTime.Now < timeOut) - { - System.Threading.Thread.Sleep(10); - } - - outmsg.Write(SteamManager.GetSteamID()); - outmsg.Write(steamAuthTicket.Data.Length); - outmsg.Write(steamAuthTicket.Data); - - DebugConsole.Log("Sending Steam auth request"); - DebugConsole.Log(" Steam ID: " + SteamManager.GetSteamID()); - DebugConsole.Log(" Ticket data: " + - ToolBox.LimitString(string.Concat(steamAuthTicket.Data.Select(b => b.ToString("X2"))), 16)); - DebugConsole.Log(" Msg length: " + outmsg.LengthBytes); - } - } - public override void Update(float deltaTime) { #if DEBUG @@ -645,29 +542,22 @@ namespace Barotrauma.Networking } } + /*TODO: reimplement if (ShowNetStats && client?.ServerConnection != null) { netStats.AddValue(NetStats.NetStatType.ReceivedBytes, client.ServerConnection.Statistics.ReceivedBytes); netStats.AddValue(NetStats.NetStatType.SentBytes, client.ServerConnection.Statistics.SentBytes); netStats.AddValue(NetStats.NetStatType.ResentMessages, client.ServerConnection.Statistics.ResentMessages); netStats.Update(deltaTime); - } + }*/ UpdateHUD(deltaTime); base.Update(deltaTime); - - if (!connected) return; - - if (reconnectBox != null) - { - reconnectBox.Close(null, null); - reconnectBox = null; - } - + try { - CheckServerMessages(); + clientPeer?.Update(deltaTime); } catch (Exception e) { @@ -680,6 +570,14 @@ namespace Barotrauma.Networking return; } + if (!connected) return; + + if (reconnectBox != null) + { + reconnectBox.Close(); + reconnectBox = null; + } + if (gameStarted && Screen.Selected == GameMain.GameScreen) { EndVoteTickBox.Visible = serverSettings.Voting.AllowEndVoting && HasSpawned; @@ -703,174 +601,151 @@ namespace Barotrauma.Networking VoipClient?.SendToServer(); } + if (IsServerOwner && connected && !connectCancelled) + { + if (GameMain.ServerChildProcess?.HasExited??true) + { + Disconnect(); + var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), TextManager.Get("ServerProcessClosed")); + msgBox.Buttons[0].OnClicked += ReturnToPreviousMenu; + } + } + // Update current time updateTimer = DateTime.Now + updateInterval; } private CoroutineHandle startGameCoroutine; - private void DecompressIncomingMessage(NetIncomingMessage inc) + private void ReadDataMessage(IReadMessage inc) { - byte[] data = inc.Data; - if (data[data.Length - 1] == 1) + ServerPacketHeader header = (ServerPacketHeader)inc.ReadByte(); + switch (header) { - using (MemoryStream stream = new MemoryStream()) - { - stream.Write(data, 0, inc.LengthBytes-1); - stream.Position = 0; - using (MemoryStream decompressed = new MemoryStream()) + case ServerPacketHeader.UPDATE_LOBBY: + ReadLobbyUpdate(inc); + break; + case ServerPacketHeader.UPDATE_INGAME: + ReadIngameUpdate(inc); + break; + case ServerPacketHeader.VOICE: + VoipClient.Read(inc); + break; + case ServerPacketHeader.QUERY_STARTGAME: + string subName = inc.ReadString(); + string subHash = inc.ReadString(); + + bool usingShuttle = inc.ReadBoolean(); + string shuttleName = inc.ReadString(); + string shuttleHash = inc.ReadString(); + + IWriteMessage readyToStartMsg = new WriteOnlyMessage(); + readyToStartMsg.Write((byte)ClientPacketHeader.RESPONSE_STARTGAME); + + GameMain.NetLobbyScreen.UsingShuttle = usingShuttle; + bool readyToStart = + GameMain.NetLobbyScreen.TrySelectSub(subName, subHash, GameMain.NetLobbyScreen.SubList) && + GameMain.NetLobbyScreen.TrySelectSub(shuttleName, shuttleHash, GameMain.NetLobbyScreen.ShuttleList.ListBox); + readyToStartMsg.Write(readyToStart); + + WriteCharacterInfo(readyToStartMsg); + + clientPeer.Send(readyToStartMsg, DeliveryMethod.Reliable); + + if (readyToStart && !CoroutineManager.IsCoroutineRunning("WaitForStartRound")) { - using (DeflateStream deflate = new DeflateStream(stream, CompressionMode.Decompress, false)) - { - deflate.CopyTo(decompressed); - } - byte[] newData = decompressed.ToArray(); - - inc.Data = newData; - inc.LengthBytes = newData.Length; - inc.Position = 0; + CoroutineManager.StartCoroutine(GameMain.NetLobbyScreen.WaitForStartRound(startButton: null, allowCancel: false), "WaitForStartRound"); } - - } + break; + case ServerPacketHeader.STARTGAME: + startGameCoroutine = GameMain.Instance.ShowLoading(StartGame(inc), false); + break; + case ServerPacketHeader.ENDGAME: + string endMessage = inc.ReadString(); + bool missionSuccessful = inc.ReadBoolean(); + Character.TeamType winningTeam = (Character.TeamType)inc.ReadByte(); + if (missionSuccessful && GameMain.GameSession?.Mission != null) + { + GameMain.GameSession.WinningTeam = winningTeam; + GameMain.GameSession.Mission.Completed = true; + } + CoroutineManager.StartCoroutine(EndGame(endMessage), "EndGame"); + break; + case ServerPacketHeader.CAMPAIGN_SETUP_INFO: + UInt16 saveCount = inc.ReadUInt16(); + List saveFiles = new List(); + for (int i = 0; i < saveCount; i++) + { + saveFiles.Add(inc.ReadString()); + } + GameMain.NetLobbyScreen.CampaignSetupUI = MultiPlayerCampaign.StartCampaignSetup(serverSubmarines, saveFiles); + break; + case ServerPacketHeader.PERMISSIONS: + ReadPermissions(inc); + break; + case ServerPacketHeader.ACHIEVEMENT: + ReadAchievement(inc); + break; + case ServerPacketHeader.CHEATS_ENABLED: + bool cheatsEnabled = inc.ReadBoolean(); + inc.ReadPadBits(); + if (cheatsEnabled == DebugConsole.CheatsEnabled) + { + return; + } + else + { + DebugConsole.CheatsEnabled = cheatsEnabled; + SteamAchievementManager.CheatsEnabled = cheatsEnabled; + if (cheatsEnabled) + { + new GUIMessageBox(TextManager.Get("CheatsEnabledTitle"), TextManager.Get("CheatsEnabledDescription")); + } + } + break; + case ServerPacketHeader.FILE_TRANSFER: + fileReceiver.ReadMessage(inc); + break; + case ServerPacketHeader.TRAITOR_MESSAGE: + ReadTraitorMessage(inc); + break; } } - - /// - /// Check for new incoming messages from server - /// - private void CheckServerMessages() + + private void OnDisconnect(string disconnectMsg) { - // Create new incoming message holder - NetIncomingMessage inc; + HandleDisconnectMessage(disconnectMsg); + } - if (startGameCoroutine != null && CoroutineManager.IsCoroutineRunning(startGameCoroutine)) return; - - while ((inc = client.ReadMessage()) != null) + private void HandleDisconnectMessage(string disconnectMsg) + { + if (SteamManager.IsInitialized) { - switch (inc.MessageType) - { - case NetIncomingMessageType.Data: - DecompressIncomingMessage(inc); - - ServerPacketHeader header = (ServerPacketHeader)inc.ReadByte(); - switch (header) - { - case ServerPacketHeader.UPDATE_LOBBY: - ReadLobbyUpdate(inc); - break; - case ServerPacketHeader.UPDATE_INGAME: - ReadIngameUpdate(inc); - break; - case ServerPacketHeader.VOICE: - VoipClient.Read(inc); - break; - case ServerPacketHeader.QUERY_STARTGAME: - string subName = inc.ReadString(); - string subHash = inc.ReadString(); - - bool usingShuttle = inc.ReadBoolean(); - string shuttleName = inc.ReadString(); - string shuttleHash = inc.ReadString(); - - NetOutgoingMessage readyToStartMsg = client.CreateMessage(); - readyToStartMsg.Write((byte)ClientPacketHeader.RESPONSE_STARTGAME); - - GameMain.NetLobbyScreen.UsingShuttle = usingShuttle; - bool readyToStart = - GameMain.NetLobbyScreen.TrySelectSub(subName, subHash, GameMain.NetLobbyScreen.SubList) && - GameMain.NetLobbyScreen.TrySelectSub(shuttleName, shuttleHash, GameMain.NetLobbyScreen.ShuttleList.ListBox); - readyToStartMsg.Write(readyToStart); - - WriteCharacterInfo(readyToStartMsg); - - client.SendMessage(readyToStartMsg, NetDeliveryMethod.ReliableUnordered); - - if (readyToStart && !CoroutineManager.IsCoroutineRunning("WaitForStartRound")) - { - CoroutineManager.StartCoroutine(GameMain.NetLobbyScreen.WaitForStartRound(startButton: null, allowCancel: false), "WaitForStartRound"); - } - break; - case ServerPacketHeader.STARTGAME: - startGameCoroutine = GameMain.Instance.ShowLoading(StartGame(inc), false); - break; - case ServerPacketHeader.ENDGAME: - string endMessage = inc.ReadString(); - bool missionSuccessful = inc.ReadBoolean(); - Character.TeamType winningTeam = (Character.TeamType)inc.ReadByte(); - if (missionSuccessful && GameMain.GameSession?.Mission != null) - { - GameMain.GameSession.WinningTeam = winningTeam; - GameMain.GameSession.Mission.Completed = true; - } - CoroutineManager.StartCoroutine(EndGame(endMessage), "EndGame"); - break; - case ServerPacketHeader.CAMPAIGN_SETUP_INFO: - UInt16 saveCount = inc.ReadUInt16(); - List saveFiles = new List(); - for (int i = 0; i < saveCount; i++) - { - saveFiles.Add(inc.ReadString()); - } - - GameMain.NetLobbyScreen.CampaignSetupUI = MultiPlayerCampaign.StartCampaignSetup(serverSubmarines, saveFiles); - break; - case ServerPacketHeader.PERMISSIONS: - ReadPermissions(inc); - break; - case ServerPacketHeader.ACHIEVEMENT: - ReadAchievement(inc); - break; - case ServerPacketHeader.CHEATS_ENABLED: - bool cheatsEnabled = inc.ReadBoolean(); - inc.ReadPadBits(); - if (cheatsEnabled == DebugConsole.CheatsEnabled) - { - continue; - } - else - { - DebugConsole.CheatsEnabled = cheatsEnabled; - SteamAchievementManager.CheatsEnabled = cheatsEnabled; - if (cheatsEnabled) - { - new GUIMessageBox(TextManager.Get("CheatsEnabledTitle"), TextManager.Get("CheatsEnabledDescription")); - } - } - break; - case ServerPacketHeader.FILE_TRANSFER: - fileReceiver.ReadMessage(inc); - break; - } - break; - case NetIncomingMessageType.StatusChanged: - NetConnectionStatus connectionStatus = (NetConnectionStatus)inc.ReadByte(); - DebugConsole.NewMessage("Connection status changed: " + connectionStatus.ToString(), Color.Orange); - - if (connectionStatus == NetConnectionStatus.Disconnected) - { - ReadDisconnectMessage(inc, true); - } - break; - } + SteamManager.Instance.User.ClearRichPresence(); } - } - private void ReadDisconnectMessage(NetIncomingMessage inc, bool allowReconnect) - { - steamAuthTicket?.Cancel(); - steamAuthTicket = null; + disconnectMsg = disconnectMsg ?? ""; - string disconnectMsg = inc.ReadString(); string[] splitMsg = disconnectMsg.Split('/'); DisconnectReason disconnectReason = DisconnectReason.Unknown; if (splitMsg.Length > 0) Enum.TryParse(splitMsg[0], out disconnectReason); + if (disconnectMsg == Lidgren.Network.NetConnection.NoResponseMessage) + { + allowReconnect = false; + } + DebugConsole.NewMessage("Received a disconnect message (" + disconnectMsg + ")"); if (disconnectReason == DisconnectReason.ServerFull) { - //already waiting for a slot to free up, do nothing - if (CoroutineManager.IsCoroutineRunning("WaitInServerQueue")) return; + CoroutineManager.StopCoroutines("WaitForStartingInfo"); + //already waiting for a slot to free up, stop waiting for starting info and + //let WaitInServerQueue reattempt connecting later + if (CoroutineManager.IsCoroutineRunning("WaitInServerQueue")) + { + return; + } reconnectBox?.Close(); reconnectBox = null; @@ -896,24 +771,26 @@ namespace Barotrauma.Networking waitInServerQueueBox = null; CoroutineManager.StopCoroutines("WaitInServerQueue"); } - + if (allowReconnect && disconnectReason == DisconnectReason.Unknown) { DebugConsole.NewMessage("Attempting to reconnect..."); string msg = TextManager.GetServerMessage(disconnectMsg); - msg = string.IsNullOrWhiteSpace(msg) ? - TextManager.Get("ConnectionLostReconnecting") : + msg = string.IsNullOrWhiteSpace(msg) ? + TextManager.Get("ConnectionLostReconnecting") : msg + '\n' + TextManager.Get("ConnectionLostReconnecting"); reconnectBox = new GUIMessageBox( TextManager.Get("ConnectionLost"), msg, new string[0]); connected = false; - ConnectToServer(serverIP, serverName); + ConnectToServer(serverEndpoint, serverName); } else { + connectCancelled = true; + string msg = ""; if (disconnectReason == DisconnectReason.Unknown) { @@ -924,23 +801,30 @@ namespace Barotrauma.Networking { DebugConsole.NewMessage("Do not attempt to reconnect (DisconnectReason doesn't allow reconnection)."); msg = TextManager.Get("DisconnectReason." + disconnectReason.ToString()); - + for (int i = 1; i < splitMsg.Length; i++) { msg += TextManager.GetServerMessage(splitMsg[i]); } + + if (disconnectReason == DisconnectReason.ServerCrashed && IsServerOwner) + { + msg = TextManager.Get("ServerProcessCrashed"); + } } - - if (msg == NetConnection.NoResponseMessage) + + reconnectBox?.Close(); + + if (msg == Lidgren.Network.NetConnection.NoResponseMessage) { //display a generic "could not connect" popup if the message is Lidgren's "failed to establish connection" var msgBox = new GUIMessageBox(TextManager.Get("ConnectionFailed"), TextManager.Get(allowReconnect ? "ConnectionLost" : "CouldNotConnectToServer")); - msgBox.Buttons[0].OnClicked += ReturnToServerList; + msgBox.Buttons[0].OnClicked += ReturnToPreviousMenu; } else { var msgBox = new GUIMessageBox(TextManager.Get(allowReconnect ? "ConnectionLost" : "CouldNotConnectToServer"), msg); - msgBox.Buttons[0].OnClicked += ReturnToServerList; + msgBox.Buttons[0].OnClicked += ReturnToPreviousMenu; } } } @@ -962,8 +846,8 @@ namespace Barotrauma.Networking { if (!CoroutineManager.IsCoroutineRunning("WaitForStartingInfo")) { - ConnectToServer(serverIP, serverName); - yield return new WaitForSeconds(2.0f); + ConnectToServer(serverEndpoint, serverName); + yield return new WaitForSeconds(5.0f); } yield return new WaitForSeconds(0.5f); } @@ -975,13 +859,47 @@ namespace Barotrauma.Networking } - private void ReadAchievement(NetIncomingMessage inc) + private void ReadAchievement(IReadMessage inc) { string achievementIdentifier = inc.ReadString(); SteamAchievementManager.UnlockAchievement(achievementIdentifier); } - private void ReadPermissions(NetIncomingMessage inc) + private void ReadTraitorMessage(IReadMessage inc) + { + TraitorMessageType messageType = (TraitorMessageType)inc.ReadByte(); + string message = inc.ReadString(); + message = TextManager.GetServerMessage(message); + + switch(messageType) { + case TraitorMessageType.Objective: + var isTraitor = !string.IsNullOrEmpty(message); + if (Character != null) + { + Character.IsTraitor = isTraitor; + Character.TraitorCurrentObjective = message; + } + else + { + SpawnAsTraitor = isTraitor; + TraitorFirstObjective = message; + } + break; + case TraitorMessageType.Console: + GameMain.Client.AddChatMessage(ChatMessage.Create("", message, ChatMessageType.Console, null)); + DebugConsole.NewMessage(message); + break; + case TraitorMessageType.ServerMessageBox: + new GUIMessageBox("", message); + break; + case TraitorMessageType.Server: + default: + GameMain.Client.AddChatMessage(message, ChatMessageType.Server); + break; + } + } + + private void ReadPermissions(IReadMessage inc) { List permittedConsoleCommands = new List(); byte clientID = inc.ReadByte(); @@ -1012,7 +930,7 @@ namespace Barotrauma.Networking permissions = newPermissions; this.permittedConsoleCommands = new List(permittedConsoleCommands); //don't show the "permissions changed" popup if the client owns the server - if (ownerKey == 0) + if (!IsServerOwner) { GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData as string == "permissions"); @@ -1056,7 +974,7 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.UpdatePermissions(); } - private IEnumerable StartGame(NetIncomingMessage inc) + private IEnumerable StartGame(IReadMessage inc) { if (Character != null) Character.Remove(); HasSpawned = false; @@ -1081,7 +999,7 @@ namespace Barotrauma.Networking int seed = inc.ReadInt32(); string levelSeed = inc.ReadString(); int levelEqualityCheckVal = inc.ReadInt32(); - float levelDifficulty = inc.ReadFloat(); + float levelDifficulty = inc.ReadSingle(); byte losMode = inc.ReadByte(); @@ -1102,8 +1020,6 @@ namespace Barotrauma.Networking bool disguisesAllowed = inc.ReadBoolean(); bool rewiringAllowed = inc.ReadBoolean(); - bool isTraitor = inc.ReadBoolean(); - string traitorTargetName = isTraitor ? inc.ReadString() : null; bool allowRagdollButton = inc.ReadBoolean(); @@ -1238,10 +1154,10 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Success; } - private void ReadInitialUpdate(NetIncomingMessage inc) + private void ReadInitialUpdate(IReadMessage inc) { myID = inc.ReadByte(); - VoipClient = new VoipClient(this, client); + VoipClient = new VoipClient(this, clientPeer); UInt16 subListCount = inc.ReadUInt16(); serverSubmarines.Clear(); @@ -1276,7 +1192,7 @@ namespace Barotrauma.Networking } } - private void ReadClientList(NetIncomingMessage inc) + private void ReadClientList(IReadMessage inc) { UInt16 listId = inc.ReadUInt16(); List tempClients = new List(); @@ -1284,6 +1200,7 @@ namespace Barotrauma.Networking for (int i = 0; i < clientCount; i++) { byte id = inc.ReadByte(); + UInt64 steamId = inc.ReadUInt64(); string name = inc.ReadString(); UInt16 characterID = inc.ReadUInt16(); bool muted = inc.ReadBoolean(); @@ -1293,6 +1210,7 @@ namespace Barotrauma.Networking tempClients.Add(new TempClient { ID = id, + SteamID = steamId, Name = name, CharacterID = characterID, Muted = muted, @@ -1312,6 +1230,7 @@ namespace Barotrauma.Networking { existingClient = new Client(tc.Name, tc.ID) { + SteamID = tc.SteamID, Muted = tc.Muted, AllowKicking = tc.AllowKicking }; @@ -1350,11 +1269,16 @@ namespace Barotrauma.Networking ConnectedClients.RemoveAt(i); } } - if (updateClientListId) LastClientListUpdateID = listId; + if (updateClientListId) { LastClientListUpdateID = listId; } + + if (clientPeer is SteamP2POwnerPeer) + { + Steam.SteamManager.UpdateLobby(serverSettings); + } } } - private void ReadLobbyUpdate(NetIncomingMessage inc) + private void ReadLobbyUpdate(IReadMessage inc) { ServerNetObject objHeader; while ((objHeader = (ServerNetObject)inc.ReadByte()) != ServerNetObject.END_OF_MESSAGE) @@ -1402,21 +1326,22 @@ namespace Barotrauma.Networking int modeIndex = inc.ReadByte(); string levelSeed = inc.ReadString(); - float levelDifficulty = inc.ReadFloat(); + float levelDifficulty = inc.ReadSingle(); byte botCount = inc.ReadByte(); BotSpawnMode botSpawnMode = inc.ReadBoolean() ? BotSpawnMode.Fill : BotSpawnMode.Normal; bool autoRestartEnabled = inc.ReadBoolean(); - float autoRestartTimer = autoRestartEnabled ? inc.ReadFloat() : 0.0f; + float autoRestartTimer = autoRestartEnabled ? inc.ReadSingle() : 0.0f; //ignore the message if we already a more up-to-date one if (NetIdUtils.IdMoreRecent(updateID, GameMain.NetLobbyScreen.LastUpdateID)) { - NetBuffer settingsBuf = new NetBuffer(); - settingsBuf.Write(settingsData, 0, settingsLen); settingsBuf.Position = 0; + ReadWriteMessage settingsBuf = new ReadWriteMessage(); + settingsBuf.Write(settingsData, 0, settingsLen); settingsBuf.BitPosition = 0; serverSettings.ClientRead(settingsBuf); + GameMain.NetLobbyScreen.LastUpdateID = updateID; serverSettings.ServerLog.ServerName = serverSettings.ServerName; @@ -1444,6 +1369,11 @@ namespace Barotrauma.Networking serverSettings.Voting.AllowSubVoting = allowSubVoting; serverSettings.Voting.AllowModeVoting = allowModeVoting; + if (clientPeer is SteamP2POwnerPeer) + { + Steam.SteamManager.UpdateLobby(serverSettings); + } + GUI.KeyboardDispatcher.Subscriber = prevDispatcher; } } @@ -1474,11 +1404,11 @@ namespace Barotrauma.Networking } } - private void ReadIngameUpdate(NetIncomingMessage inc) + private void ReadIngameUpdate(IReadMessage inc) { List entities = new List(); - float sendingTime = inc.ReadFloat() - inc.SenderConnection.RemoteTimeOffset; + float sendingTime = inc.ReadSingle() - 0.0f;//TODO: reimplement inc.SenderConnection.RemoteTimeOffset; ServerNetObject? prevObjHeader = null; long prevBitPos = 0; @@ -1501,7 +1431,7 @@ namespace Barotrauma.Networking UInt16 id = inc.ReadUInt16(); byte msgLength = inc.ReadByte(); - long msgEndPos = inc.Position + msgLength * 8; + int msgEndPos = inc.BitPosition + msgLength * 8; var entity = Entity.FindEntityByID(id) as IServerSerializable; if (entity != null) @@ -1511,7 +1441,7 @@ namespace Barotrauma.Networking //force to the correct position in case the entity doesn't exist //or the message wasn't read correctly for whatever reason - inc.Position = msgEndPos; + inc.BitPosition = msgEndPos; inc.ReadPadBits(); break; case ServerNetObject.CLIENT_LIST: @@ -1566,19 +1496,19 @@ namespace Barotrauma.Networking FileStream fl = File.Open("crashreport_object.bin", FileMode.Create); BinaryWriter sw = new BinaryWriter(fl); - sw.Write(inc.Data, (int)(prevBytePos - prevByteLength), (int)(prevByteLength)); + sw.Write(inc.Buffer, (int)(prevBytePos - prevByteLength), (int)(prevByteLength)); sw.Close(); fl.Close(); throw new Exception("Error while reading update from server: please send us \"crashreport_object.bin\"!"); } - prevBitLength = inc.Position - prevBitPos; - prevByteLength = inc.PositionInBytes - prevByteLength; + prevBitLength = inc.BitPosition - prevBitPos; + prevByteLength = inc.BytePosition - prevByteLength; prevObjHeader = objHeader; - prevBitPos = inc.Position; - prevBytePos = inc.PositionInBytes; + prevBitPos = inc.BitPosition; + prevBytePos = inc.BytePosition; if (eventReadFailed) { @@ -1589,7 +1519,7 @@ namespace Barotrauma.Networking private void SendLobbyUpdate() { - NetOutgoingMessage outmsg = client.CreateMessage(); + IWriteMessage outmsg = new WriteOnlyMessage(); outmsg.Write((byte)ClientPacketHeader.UPDATE_LOBBY); outmsg.Write((byte)ClientNetObject.SYNC_IDS); @@ -1614,7 +1544,7 @@ namespace Barotrauma.Networking chatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, lastSentChatMsgID)); for (int i = 0; i < chatMsgQueue.Count && i < ChatMessage.MaxMessagesPerPacket; i++) { - if (outmsg.LengthBytes + chatMsgQueue[i].EstimateLengthBytesClient() > client.Configuration.MaximumTransmissionUnit - 5) + if (outmsg.LengthBytes + chatMsgQueue[i].EstimateLengthBytesClient() > MsgConstants.MTU - 5) { //no more room in this packet break; @@ -1623,17 +1553,17 @@ namespace Barotrauma.Networking } outmsg.Write((byte)ClientNetObject.END_OF_MESSAGE); - if (outmsg.LengthBytes > client.Configuration.MaximumTransmissionUnit) + if (outmsg.LengthBytes > MsgConstants.MTU) { - DebugConsole.ThrowError("Maximum packet size exceeded (" + outmsg.LengthBytes + " > " + client.Configuration.MaximumTransmissionUnit); + DebugConsole.ThrowError("Maximum packet size exceeded (" + outmsg.LengthBytes + " > " + MsgConstants.MTU); } - client.SendMessage(outmsg, NetDeliveryMethod.Unreliable); + clientPeer.Send(outmsg, DeliveryMethod.Unreliable); } private void SendIngameUpdate() { - NetOutgoingMessage outmsg = client.CreateMessage(); + IWriteMessage outmsg = new WriteOnlyMessage(); outmsg.Write((byte)ClientPacketHeader.UPDATE_INGAME); outmsg.Write((byte)ClientNetObject.SYNC_IDS); @@ -1645,12 +1575,12 @@ namespace Barotrauma.Networking Character.Controlled?.ClientWrite(outmsg); GameMain.GameScreen.Cam?.ClientWrite(outmsg); - entityEventManager.Write(outmsg, client.ServerConnection); + entityEventManager.Write(outmsg, clientPeer?.ServerConnection); chatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, lastSentChatMsgID)); for (int i = 0; i < chatMsgQueue.Count && i < ChatMessage.MaxMessagesPerPacket; i++) { - if (outmsg.LengthBytes + chatMsgQueue[i].EstimateLengthBytesClient() > client.Configuration.MaximumTransmissionUnit - 5) + if (outmsg.LengthBytes + chatMsgQueue[i].EstimateLengthBytesClient() > MsgConstants.MTU - 5) { //not enough room in this packet break; @@ -1660,17 +1590,17 @@ namespace Barotrauma.Networking outmsg.Write((byte)ClientNetObject.END_OF_MESSAGE); - if (outmsg.LengthBytes > client.Configuration.MaximumTransmissionUnit) + if (outmsg.LengthBytes > MsgConstants.MTU) { - DebugConsole.ThrowError("Maximum packet size exceeded (" + outmsg.LengthBytes + " > " + client.Configuration.MaximumTransmissionUnit); + DebugConsole.ThrowError("Maximum packet size exceeded (" + outmsg.LengthBytes + " > " + MsgConstants.MTU); } - client.SendMessage(outmsg, NetDeliveryMethod.Unreliable); + clientPeer.Send(outmsg, DeliveryMethod.Unreliable); } public void SendChatMessage(ChatMessage msg) { - if (client.ServerConnection == null) return; + if (clientPeer.ServerConnection == null) return; lastQueueChatMsgID++; msg.NetStateID = lastQueueChatMsgID; chatMsgQueue.Add(msg); @@ -1678,7 +1608,7 @@ namespace Barotrauma.Networking public void SendChatMessage(string message, ChatMessageType type = ChatMessageType.Default) { - if (client.ServerConnection == null) return; + if (clientPeer.ServerConnection == null) return; ChatMessage chatMessage = ChatMessage.Create( gameStarted && myCharacter != null ? myCharacter.Name : name, @@ -1694,27 +1624,37 @@ namespace Barotrauma.Networking public void RequestFile(FileTransferType fileType, string file, string fileHash) { - NetOutgoingMessage msg = client.CreateMessage(); + IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ClientPacketHeader.FILE_REQUEST); msg.Write((byte)FileTransferMessageType.Initiate); msg.Write((byte)fileType); if (file != null) msg.Write(file); if (fileHash != null) msg.Write(fileHash); - client.SendMessage(msg, NetDeliveryMethod.ReliableUnordered); + clientPeer.Send(msg, DeliveryMethod.Reliable); } public void CancelFileTransfer(FileReceiver.FileTransferIn transfer) { - CancelFileTransfer(transfer.SequenceChannel); + CancelFileTransfer(transfer.ID); } - public void CancelFileTransfer(int sequenceChannel) + public void UpdateFileTransfer(int id, int offset, bool reliable=false) { - NetOutgoingMessage msg = client.CreateMessage(); + IWriteMessage msg = new WriteOnlyMessage(); + msg.Write((byte)ClientPacketHeader.FILE_REQUEST); + msg.Write((byte)FileTransferMessageType.Data); + msg.Write((byte)id); + msg.Write(offset); + clientPeer.Send(msg, reliable ? DeliveryMethod.Reliable : DeliveryMethod.Unreliable); + } + + public void CancelFileTransfer(int id) + { + IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ClientPacketHeader.FILE_REQUEST); msg.Write((byte)FileTransferMessageType.Cancel); - msg.Write((byte)sequenceChannel); - client.SendMessage(msg, NetDeliveryMethod.ReliableUnordered); + msg.Write((byte)id); + clientPeer.Send(msg, DeliveryMethod.Reliable); } private void OnFileReceived(FileReceiver.FileTransferIn transfer) @@ -1841,9 +1781,15 @@ namespace Barotrauma.Networking public override void Disconnect() { - client.Shutdown(""); - steamAuthTicket?.Cancel(); - steamAuthTicket = null; + allowReconnect = false; + + if (clientPeer is SteamP2PClientPeer || clientPeer is SteamP2POwnerPeer) + { + SteamManager.LeaveLobby(); + } + + clientPeer?.Close(); + clientPeer = null; List activeTransfers = new List(FileReceiver.ActiveTransfers); foreach (var fileTransfer in activeTransfers) @@ -1876,7 +1822,7 @@ namespace Barotrauma.Networking GameMain.Client = null; } - public void WriteCharacterInfo(NetOutgoingMessage msg) + public void WriteCharacterInfo(IWriteMessage msg) { msg.Write(characterInfo == null); if (characterInfo == null) return; @@ -1900,13 +1846,13 @@ namespace Barotrauma.Networking public void Vote(VoteType voteType, object data) { - NetOutgoingMessage msg = client.CreateMessage(); + IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ClientPacketHeader.UPDATE_LOBBY); msg.Write((byte)ClientNetObject.VOTE); serverSettings.Voting.ClientWrite(msg, voteType, data); msg.Write((byte)ServerNetObject.END_OF_MESSAGE); - client.SendMessage(msg, NetDeliveryMethod.ReliableUnordered); + clientPeer.Send(msg, DeliveryMethod.Reliable); } public void VoteForKick(Client votedClient) @@ -1927,18 +1873,18 @@ namespace Barotrauma.Networking public override void KickPlayer(string kickedName, string reason) { - NetOutgoingMessage msg = client.CreateMessage(); + IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ClientPacketHeader.SERVER_COMMAND); msg.Write((UInt16)ClientPermissions.Kick); msg.Write(kickedName); msg.Write(reason); - client.SendMessage(msg, NetDeliveryMethod.ReliableUnordered); + clientPeer.Send(msg, DeliveryMethod.Reliable); } public override void BanPlayer(string kickedName, string reason, bool range = false, TimeSpan? duration = null) { - NetOutgoingMessage msg = client.CreateMessage(); + IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ClientPacketHeader.SERVER_COMMAND); msg.Write((UInt16)ClientPermissions.Ban); msg.Write(kickedName); @@ -1946,26 +1892,26 @@ namespace Barotrauma.Networking msg.Write(range); msg.Write(duration.HasValue ? duration.Value.TotalSeconds : 0.0); //0 = permaban - client.SendMessage(msg, NetDeliveryMethod.ReliableUnordered); + clientPeer.Send(msg, DeliveryMethod.Reliable); } public override void UnbanPlayer(string playerName, string playerIP) { - NetOutgoingMessage msg = client.CreateMessage(); + IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ClientPacketHeader.SERVER_COMMAND); msg.Write((UInt16)ClientPermissions.Unban); msg.Write(string.IsNullOrEmpty(playerName) ? "" : playerName); msg.Write(string.IsNullOrEmpty(playerIP) ? "" : playerIP); - client.SendMessage(msg, NetDeliveryMethod.ReliableUnordered); + clientPeer.Send(msg, DeliveryMethod.Reliable); } public void UpdateClientPermissions(Client targetClient) { - NetOutgoingMessage msg = client.CreateMessage(); + IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ClientPacketHeader.SERVER_COMMAND); msg.Write((UInt16)ClientPermissions.ManagePermissions); targetClient.WritePermissions(msg); - client.SendMessage(msg, NetDeliveryMethod.ReliableUnordered); + clientPeer.Send(msg, DeliveryMethod.Reliable); } public void SendCampaignState() @@ -1977,13 +1923,13 @@ namespace Barotrauma.Networking return; } - NetOutgoingMessage msg = client.CreateMessage(); + IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ClientPacketHeader.SERVER_COMMAND); msg.Write((UInt16)ClientPermissions.ManageCampaign); campaign.ClientWrite(msg); msg.Write((byte)ServerNetObject.END_OF_MESSAGE); - client.SendMessage(msg, NetDeliveryMethod.ReliableUnordered); + clientPeer.Send(msg, DeliveryMethod.Reliable); } public void SendConsoleCommand(string command) @@ -1994,7 +1940,7 @@ namespace Barotrauma.Networking return; } - NetOutgoingMessage msg = client.CreateMessage(); + IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ClientPacketHeader.SERVER_COMMAND); msg.Write((UInt16)ClientPermissions.ConsoleCommands); msg.Write(command); @@ -2002,7 +1948,7 @@ namespace Barotrauma.Networking msg.Write(cursorWorldPos.X); msg.Write(cursorWorldPos.Y); - client.SendMessage(msg, NetDeliveryMethod.ReliableUnordered); + clientPeer.Send(msg, DeliveryMethod.Reliable); } /// @@ -2012,12 +1958,12 @@ namespace Barotrauma.Networking { if (!HasPermission(ClientPermissions.ManageRound)) return; - NetOutgoingMessage msg = client.CreateMessage(); + IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ClientPacketHeader.SERVER_COMMAND); msg.Write((UInt16)ClientPermissions.ManageRound); msg.Write(false); //indicates round start - client.SendMessage(msg, NetDeliveryMethod.ReliableUnordered); + clientPeer.Send(msg, DeliveryMethod.Reliable); } /// @@ -2035,14 +1981,14 @@ namespace Barotrauma.Networking return; } - NetOutgoingMessage msg = client.CreateMessage(); + IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ClientPacketHeader.SERVER_COMMAND); msg.Write((UInt16)ClientPermissions.SelectSub); msg.Write(isShuttle); msg.WritePadBits(); msg.Write((UInt16)subIndex); msg.Write((byte)ServerNetObject.END_OF_MESSAGE); - client.SendMessage(msg, NetDeliveryMethod.ReliableUnordered); + clientPeer.Send(msg, DeliveryMethod.Reliable); } /// @@ -2057,20 +2003,20 @@ namespace Barotrauma.Networking return; } - NetOutgoingMessage msg = client.CreateMessage(); + IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ClientPacketHeader.SERVER_COMMAND); msg.Write((UInt16)ClientPermissions.SelectMode); msg.Write((UInt16)modeIndex); msg.Write((byte)ServerNetObject.END_OF_MESSAGE); - client.SendMessage(msg, NetDeliveryMethod.ReliableUnordered); + clientPeer.Send(msg, DeliveryMethod.Reliable); } public void SetupNewCampaign(Submarine sub, string saveName, string mapSeed) { saveName = Path.GetFileNameWithoutExtension(saveName); - NetOutgoingMessage msg = client.CreateMessage(); + IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ClientPacketHeader.CAMPAIGN_SETUP_INFO); msg.Write(true); msg.WritePadBits(); @@ -2079,20 +2025,20 @@ namespace Barotrauma.Networking msg.Write(sub.Name); msg.Write(sub.MD5Hash.Hash); - client.SendMessage(msg, NetDeliveryMethod.ReliableUnordered); + clientPeer.Send(msg, DeliveryMethod.Reliable); GameMain.NetLobbyScreen.CampaignSetupUI = null; } public void SetupLoadCampaign(string saveName) { - NetOutgoingMessage msg = client.CreateMessage(); + IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ClientPacketHeader.CAMPAIGN_SETUP_INFO); msg.Write(false); msg.WritePadBits(); msg.Write(saveName); - client.SendMessage(msg, NetDeliveryMethod.ReliableUnordered); + clientPeer.Send(msg, DeliveryMethod.Reliable); GameMain.NetLobbyScreen.CampaignSetupUI = null; } @@ -2102,19 +2048,19 @@ namespace Barotrauma.Networking /// public void RequestRoundEnd() { - NetOutgoingMessage msg = client.CreateMessage(); + IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ClientPacketHeader.SERVER_COMMAND); msg.Write((UInt16)ClientPermissions.ManageRound); msg.Write(true); //indicates round end - client.SendMessage(msg, NetDeliveryMethod.ReliableUnordered); + clientPeer.Send(msg, DeliveryMethod.Reliable); } public bool SpectateClicked(GUIButton button, object userData) { if (button != null) button.Enabled = false; - NetOutgoingMessage readyToStartMsg = client.CreateMessage(); + IWriteMessage readyToStartMsg = new WriteOnlyMessage(); readyToStartMsg.Write((byte)ClientPacketHeader.RESPONSE_STARTGAME); //assume we have the required sub files to start the round @@ -2123,7 +2069,7 @@ namespace Barotrauma.Networking WriteCharacterInfo(readyToStartMsg); - client.SendMessage(readyToStartMsg, NetDeliveryMethod.ReliableUnordered); + clientPeer.Send(readyToStartMsg, DeliveryMethod.Reliable); return false; } @@ -2388,6 +2334,7 @@ namespace Barotrauma.Networking GUI.DrawRectangle(spriteBatch, new Rectangle(x, y, width, height), Color.Black * 0.7f, true); GUI.Font.DrawString(spriteBatch, "Network statistics:", new Vector2(x + 10, y + 10), Color.White); + /* TODO: reimplement if (client.ServerConnection != null) { GUI.Font.DrawString(spriteBatch, "Ping: " + (int)(client.ServerConnection.AverageRoundtripTime * 1000.0f) + " ms", new Vector2(x + 10, y + 25), Color.White); @@ -2403,7 +2350,7 @@ namespace Barotrauma.Networking else { GUI.Font.DrawString(spriteBatch, "Disconnected", new Vector2(x + 10, y + 25), Color.White); - } + }*/ } public virtual bool SelectCrewCharacter(Character character, GUIComponent characterFrame) @@ -2541,7 +2488,7 @@ namespace Barotrauma.Networking public void ReportError(ClientNetError error, UInt16 expectedID = 0, UInt16 eventID = 0, UInt16 entityID = 0) { - NetOutgoingMessage outMsg = client.CreateMessage(); + IWriteMessage outMsg = new WriteOnlyMessage(); outMsg.Write((byte)ClientPacketHeader.ERROR); outMsg.Write((byte)error); outMsg.Write(Level.Loaded == null ? 0 : Level.Loaded.EqualityCheckVal); @@ -2556,7 +2503,7 @@ namespace Barotrauma.Networking outMsg.Write(entityID); break; } - client.SendMessage(outMsg, NetDeliveryMethod.ReliableUnordered); + clientPeer.Send(outMsg, DeliveryMethod.Reliable); } } } diff --git a/Barotrauma/BarotraumaClient/Source/Networking/KarmaManager.cs b/Barotrauma/BarotraumaClient/Source/Networking/KarmaManager.cs index 8b3bb1dad..227da1038 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/KarmaManager.cs @@ -1,7 +1,7 @@ -using System; +using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; using System.Text; -using Microsoft.Xna.Framework; namespace Barotrauma { @@ -10,6 +10,10 @@ namespace Barotrauma public void CreateSettingsFrame(GUIComponent parent) { CreateLabeledSlider(parent, 0.0f, 40.0f, 1.0f, "KickBanThreshold"); + if (TextManager.ContainsTag("Karma.KicksBeforeBan")) + { + CreateLabeledNumberInput(parent, 0, 10, "KicksBeforeBan"); + } CreateLabeledSlider(parent, 0.0f, 50.0f, 1.0f, "HerpesThreshold"); CreateLabeledSlider(parent, 0.0f, 0.5f, 0.01f, "KarmaDecay"); @@ -37,7 +41,7 @@ namespace Barotrauma CreateLabeledSlider(parent, 0.0f, 1.0f, 0.01f, "DamageFriendlyKarmaDecrease"); CreateLabeledSlider(parent, 0.0f, 100.0f, 1.0f, "ReactorMeltdownKarmaDecrease"); CreateLabeledSlider(parent, 0.0f, 10.0f, 0.05f, "ReactorOverheatKarmaDecrease"); - CreateLabeledSlider(parent, 0.0f, 20.0f, 1f, "AllowedWireDisconnectionsPerMinute"); + CreateLabeledNumberInput(parent, 0, 20, "AllowedWireDisconnectionsPerMinute"); CreateLabeledSlider(parent, 0.0f, 20.0f, 0.5f, "WireDisconnectionKarmaDecrease"); CreateLabeledSlider(parent, 0.0f, 30.0f, 1.0f, "SpamFilterKarmaDecrease"); } @@ -74,5 +78,29 @@ namespace Barotrauma GameMain.NetworkMember.ServerSettings.AssignGUIComponent(propertyName, slider); slider.OnMoved(slider, slider.BarScroll); } + + private void CreateLabeledNumberInput(GUIComponent parent, int min, int max, string propertyName) + { + var container = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), parent.RectTransform), isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.05f, + ToolTip = TextManager.Get("Karma." + propertyName + "ToolTip") + }; + + string labelText = TextManager.Get("Karma." + propertyName); + var label = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.8f), container.RectTransform), + labelText, font: GUI.SmallFont) + { + ToolTip = TextManager.Get("Karma." + propertyName + "ToolTip") + }; + + var numInput = new GUINumberInput(new RectTransform(new Vector2(0.3f, 0.8f), container.RectTransform), GUINumberInput.NumberType.Int) + { + MinValueInt = min, + MaxValueFloat = max + }; + GameMain.NetworkMember.ServerSettings.AssignGUIComponent(propertyName, numInput); + } } } diff --git a/Barotrauma/BarotraumaClient/Source/Networking/NetEntityEvent/ClientEntityEventManager.cs b/Barotrauma/BarotraumaClient/Source/Networking/NetEntityEvent/ClientEntityEventManager.cs index e983956af..466258b7f 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/NetEntityEvent/ClientEntityEventManager.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/NetEntityEvent/ClientEntityEventManager.cs @@ -1,5 +1,4 @@ -using Lidgren.Network; -using System; +using System; using System.Collections.Generic; namespace Barotrauma.Networking @@ -75,7 +74,7 @@ namespace Barotrauma.Networking events.Add(newEvent); } - public void Write(NetOutgoingMessage msg, NetConnection serverConnection) + public void Write(IWriteMessage msg, NetworkConnection serverConnection) { if (events.Count == 0 || serverConnection == null) return; @@ -97,7 +96,7 @@ namespace Barotrauma.Networking //find the first event that hasn't been sent in roundtriptime or at all eventLastSent.TryGetValue(events[i].ID, out float lastSent); - if (lastSent > NetTime.Now - serverConnection.AverageRoundtripTime) + if (lastSent > Lidgren.Network.NetTime.Now - 50) //TODO: reimplement serverConnection.AverageRoundtripTime { continue; } @@ -109,7 +108,7 @@ namespace Barotrauma.Networking foreach (NetEntityEvent entityEvent in eventsToSync) { - eventLastSent[entityEvent.ID] = (float)NetTime.Now; + eventLastSent[entityEvent.ID] = (float)Lidgren.Network.NetTime.Now; } msg.Write((byte)ClientNetObject.ENTITY_STATE); @@ -121,7 +120,7 @@ namespace Barotrauma.Networking /// /// Read the events from the message, ignoring ones we've already received. Returns false if reading the events fails. /// - public bool Read(ServerNetObject type, NetIncomingMessage msg, float sendingTime, List entities) + public bool Read(ServerNetObject type, IReadMessage msg, float sendingTime, List entities) { UInt16 unreceivedEntityEventCount = 0; @@ -176,7 +175,7 @@ namespace Barotrauma.Networking IServerSerializable entity = Entity.FindEntityByID(entityID) as IServerSerializable; entities.Add(entity); - + //skip the event if we've already received it or if the entity isn't found if (thisEventID != (UInt16)(lastReceivedID + 1) || entity == null) { @@ -200,11 +199,11 @@ namespace Barotrauma.Networking return false; } - msg.Position += msgLength * 8; + msg.BitPosition += msgLength * 8; } else { - long msgPosition = msg.Position; + long msgPosition = msg.BitPosition; if (GameSettings.VerboseLogging) { DebugConsole.NewMessage("received msg " + thisEventID + " (" + entity.ToString() + ")", @@ -231,7 +230,7 @@ namespace Barotrauma.Networking } GameAnalyticsManager.AddErrorEventOnce("ClientEntityEventManager.Read:ReadFailed" + entity.ToString(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); - msg.Position = msgPosition + msgLength * 8; + msg.BitPosition = (int)(msgPosition + msgLength * 8); } } msg.ReadPadBits(); @@ -239,7 +238,7 @@ namespace Barotrauma.Networking return true; } - protected override void WriteEvent(NetBuffer buffer, NetEntityEvent entityEvent, Client recipient = null) + protected override void WriteEvent(IWriteMessage buffer, NetEntityEvent entityEvent, Client recipient = null) { var clientEvent = entityEvent as ClientEntityEvent; if (clientEvent == null) return; @@ -248,7 +247,7 @@ namespace Barotrauma.Networking clientEvent.Sent = true; } - protected void ReadEvent(NetIncomingMessage buffer, IServerSerializable entity, float sendingTime) + protected void ReadEvent(IReadMessage buffer, IServerSerializable entity, float sendingTime) { entity.ClientRead(ServerNetObject.ENTITY_EVENT, buffer, sendingTime); } diff --git a/Barotrauma/BarotraumaClient/Source/Networking/NetEntityEvent/NetEntityEvent.cs b/Barotrauma/BarotraumaClient/Source/Networking/NetEntityEvent/NetEntityEvent.cs index 15e2c9cde..bcecc22dd 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/NetEntityEvent/NetEntityEvent.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/NetEntityEvent/NetEntityEvent.cs @@ -1,5 +1,4 @@ -using Lidgren.Network; -using System; +using System; namespace Barotrauma.Networking { @@ -15,7 +14,7 @@ namespace Barotrauma.Networking serializable = entity; } - public void Write(NetBuffer msg) + public void Write(IWriteMessage msg) { msg.Write(CharacterStateID); serializable.ClientWrite(msg, Data); diff --git a/Barotrauma/BarotraumaClient/Source/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaClient/Source/Networking/OrderChatMessage.cs index c4adc8baf..84c5b31bd 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/OrderChatMessage.cs @@ -5,7 +5,7 @@ namespace Barotrauma.Networking { partial class OrderChatMessage : ChatMessage { - public override void ClientWrite(NetOutgoingMessage msg) + public override void ClientWrite(IWriteMessage msg) { msg.Write((byte)ClientNetObject.CHAT_MESSAGE); msg.Write(NetStateID); diff --git a/Barotrauma/BarotraumaClient/Source/Networking/Primitives/Peers/ClientPeer.cs b/Barotrauma/BarotraumaClient/Source/Networking/Primitives/Peers/ClientPeer.cs new file mode 100644 index 000000000..2e18f01bd --- /dev/null +++ b/Barotrauma/BarotraumaClient/Source/Networking/Primitives/Peers/ClientPeer.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Barotrauma.Networking +{ + abstract class ClientPeer + { + public delegate void MessageCallback(IReadMessage message); + public delegate void DisconnectCallback(string msg); + public delegate void PasswordCallback(int salt, int retries); + public delegate void InitializationCompleteCallback(); + + public MessageCallback OnMessageReceived; + public DisconnectCallback OnDisconnect; + public PasswordCallback OnRequestPassword; + public InitializationCompleteCallback OnInitializationComplete; + + public string Name; + + public string Version { get; protected set; } + + public NetworkConnection ServerConnection { get; protected set; } + + public abstract void Start(object endPoint, int ownerKey); + public abstract void Close(string msg = null); + public abstract void Update(float deltaTime); + public abstract void Send(IWriteMessage msg, DeliveryMethod deliveryMethod); + public abstract void SendPassword(string password); + } +} diff --git a/Barotrauma/BarotraumaClient/Source/Networking/Primitives/Peers/LidgrenClientPeer.cs b/Barotrauma/BarotraumaClient/Source/Networking/Primitives/Peers/LidgrenClientPeer.cs new file mode 100644 index 000000000..2bbdb9d7a --- /dev/null +++ b/Barotrauma/BarotraumaClient/Source/Networking/Primitives/Peers/LidgrenClientPeer.cs @@ -0,0 +1,253 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Text; +using Lidgren.Network; +using Facepunch.Steamworks; +using Barotrauma.Steam; +using System.Linq; + +namespace Barotrauma.Networking +{ + class LidgrenClientPeer : ClientPeer + { + private bool isActive; + private NetClient netClient; + private NetPeerConfiguration netPeerConfiguration; + + private ConnectionInitialization initializationStep; + private int ownerKey; + private int passwordSalt; + private Auth.Ticket steamAuthTicket; + List incomingLidgrenMessages; + + public LidgrenClientPeer(string name) + { + ServerConnection = null; + + Name = name; + + netClient = null; + isActive = false; + } + + public override void Start(object endPoint, int ownerKey) + { + if (isActive) { return; } + + this.ownerKey = ownerKey; + + netPeerConfiguration = new NetPeerConfiguration("barotrauma"); + + netPeerConfiguration.DisableMessageType(NetIncomingMessageType.DebugMessage | NetIncomingMessageType.WarningMessage | NetIncomingMessageType.Receipt + | NetIncomingMessageType.ErrorMessage | NetIncomingMessageType.Error); + + netClient = new NetClient(netPeerConfiguration); + + if (SteamManager.IsInitialized) + { + steamAuthTicket = SteamManager.GetAuthSessionTicket(); + //TODO: wait for GetAuthSessionTicketResponse_t + + if (steamAuthTicket == null) + { + throw new Exception("GetAuthSessionTicket returned null"); + } + } + + incomingLidgrenMessages = new List(); + + initializationStep = ConnectionInitialization.SteamTicketAndVersion; + + if (!(endPoint is IPEndPoint ipEndPoint)) + { + throw new InvalidCastException("endPoint is not IPEndPoint"); + } + if (ServerConnection != null) + { + throw new InvalidOperationException("ServerConnection is not null"); + } + + netClient.Start(); + ServerConnection = new LidgrenConnection("Server", netClient.Connect(ipEndPoint), 0); + ServerConnection.Status = NetworkConnectionStatus.Connected; + + isActive = true; + } + + public override void Update(float deltaTime) + { + if (!isActive) { return; } + + netClient.ReadMessages(incomingLidgrenMessages); + + foreach (NetIncomingMessage inc in incomingLidgrenMessages) + { + if (inc.SenderConnection != (ServerConnection as LidgrenConnection).NetConnection) { continue; } + + switch (inc.MessageType) + { + case NetIncomingMessageType.Data: + HandleDataMessage(inc); + break; + case NetIncomingMessageType.StatusChanged: + HandleStatusChanged(inc); + break; + } + } + + incomingLidgrenMessages.Clear(); + } + + private void HandleDataMessage(NetIncomingMessage inc) + { + if (!isActive) { return; } + + byte incByte = inc.ReadByte(); + bool isCompressed = (incByte & (byte)PacketHeader.IsCompressed) != 0; + bool isConnectionInitializationStep = (incByte & (byte)PacketHeader.IsConnectionInitializationStep) != 0; + + //DebugConsole.NewMessage(isCompressed + " " + isConnectionInitializationStep + " " + (int)incByte); + + if (isConnectionInitializationStep && initializationStep != ConnectionInitialization.Success) + { + ReadConnectionInitializationStep(inc); + } + else + { + if (initializationStep != ConnectionInitialization.Success) + { + OnInitializationComplete?.Invoke(); + initializationStep = ConnectionInitialization.Success; + } + UInt16 length = inc.ReadUInt16(); + IReadMessage msg = new ReadOnlyMessage(inc.Data, isCompressed, inc.PositionInBytes, length, ServerConnection); + OnMessageReceived?.Invoke(msg); + } + } + + private void HandleStatusChanged(NetIncomingMessage inc) + { + if (!isActive) { return; } + + NetConnectionStatus status = (NetConnectionStatus)inc.ReadByte(); + switch (status) + { + case NetConnectionStatus.Disconnected: + string disconnectMsg = inc.ReadString(); + Close(disconnectMsg); + break; + } + } + + private void ReadConnectionInitializationStep(NetIncomingMessage inc) + { + if (!isActive) { return; } + + ConnectionInitialization step = (ConnectionInitialization)inc.ReadByte(); + //DebugConsole.NewMessage(step + " " + initializationStep); + switch (step) + { + case ConnectionInitialization.SteamTicketAndVersion: + if (initializationStep != ConnectionInitialization.SteamTicketAndVersion) { return; } + NetOutgoingMessage outMsg = netClient.CreateMessage(); + outMsg.Write((byte)PacketHeader.IsConnectionInitializationStep); + outMsg.Write((byte)ConnectionInitialization.SteamTicketAndVersion); + outMsg.Write(Name); + outMsg.Write(ownerKey); + outMsg.Write(SteamManager.GetSteamID()); + if (steamAuthTicket == null) + { + outMsg.Write((UInt16)0); + } + else + { + outMsg.Write((UInt16)steamAuthTicket.Data.Length); + outMsg.Write(steamAuthTicket.Data, 0, steamAuthTicket.Data.Length); + } + + outMsg.Write(GameMain.Version.ToString()); + + IEnumerable mpContentPackages = GameMain.SelectedPackages.Where(cp => cp.HasMultiplayerIncompatibleContent); + outMsg.WriteVariableInt32(mpContentPackages.Count()); + foreach (ContentPackage contentPackage in mpContentPackages) + { + outMsg.Write(contentPackage.Name); + outMsg.Write(contentPackage.MD5hash.Hash); + } + + netClient.SendMessage(outMsg, NetDeliveryMethod.ReliableUnordered); + break; + case ConnectionInitialization.Password: + if (initializationStep == ConnectionInitialization.SteamTicketAndVersion) { initializationStep = ConnectionInitialization.Password; } + if (initializationStep != ConnectionInitialization.Password) { return; } + bool incomingSalt = inc.ReadBoolean(); inc.ReadPadBits(); + int retries = 0; + if (incomingSalt) + { + passwordSalt = inc.ReadInt32(); + } + else + { + retries = inc.ReadInt32(); + } + OnRequestPassword?.Invoke(passwordSalt, retries); + break; + } + } + + public override void SendPassword(string password) + { + if (!isActive) { return; } + + if (initializationStep != ConnectionInitialization.Password) { return; } + NetOutgoingMessage outMsg = netClient.CreateMessage(); + outMsg.Write((byte)PacketHeader.IsConnectionInitializationStep); + outMsg.Write((byte)ConnectionInitialization.Password); + byte[] saltedPw = ServerSettings.SaltPassword(NetUtility.ComputeSHAHash(Encoding.UTF8.GetBytes(password)), passwordSalt); + outMsg.Write((byte)saltedPw.Length); + outMsg.Write(saltedPw, 0, saltedPw.Length); + netClient.SendMessage(outMsg, NetDeliveryMethod.ReliableUnordered); + } + + public override void Close(string msg = null) + { + if (!isActive) { return; } + + isActive = false; + + netClient.Shutdown(msg ?? TextManager.Get("Disconnecting")); + netClient = null; + steamAuthTicket?.Cancel(); steamAuthTicket = null; + OnDisconnect?.Invoke(msg); + } + + public override void Send(IWriteMessage msg, DeliveryMethod deliveryMethod) + { + if (!isActive) { return; } + + NetDeliveryMethod lidgrenDeliveryMethod = NetDeliveryMethod.Unreliable; + switch (deliveryMethod) + { + case DeliveryMethod.Unreliable: + lidgrenDeliveryMethod = NetDeliveryMethod.Unreliable; + break; + case DeliveryMethod.Reliable: + lidgrenDeliveryMethod = NetDeliveryMethod.ReliableUnordered; + break; + case DeliveryMethod.ReliableOrdered: + lidgrenDeliveryMethod = NetDeliveryMethod.ReliableOrdered; + break; + } + + NetOutgoingMessage lidgrenMsg = netClient.CreateMessage(); + byte[] msgData = new byte[msg.LengthBytes]; + msg.PrepareForSending(ref msgData, out bool isCompressed, out int length); + lidgrenMsg.Write((byte)(isCompressed ? PacketHeader.IsCompressed : PacketHeader.None)); + lidgrenMsg.Write((UInt16)length); + lidgrenMsg.Write(msgData, 0, length); + + netClient.SendMessage(lidgrenMsg, lidgrenDeliveryMethod); + } + } +} diff --git a/Barotrauma/BarotraumaClient/Source/Networking/Primitives/Peers/SteamP2PClientPeer.cs b/Barotrauma/BarotraumaClient/Source/Networking/Primitives/Peers/SteamP2PClientPeer.cs new file mode 100644 index 000000000..223cf50b8 --- /dev/null +++ b/Barotrauma/BarotraumaClient/Source/Networking/Primitives/Peers/SteamP2PClientPeer.cs @@ -0,0 +1,338 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Linq; +using Barotrauma.Steam; +using Facepunch.Steamworks; +using System.Threading; + +namespace Barotrauma.Networking +{ + class SteamP2PClientPeer : ClientPeer + { + private bool isActive; + private UInt64 hostSteamId; + private ConnectionInitialization initializationStep; + private int passwordSalt; + private Auth.Ticket steamAuthTicket; + private double timeout; + private double heartbeatTimer; + + private List incomingInitializationMessages; + private List incomingDataMessages; + + public SteamP2PClientPeer(string name) + { + ServerConnection = null; + + Name = name; + + isActive = false; + } + + public override void Start(object endPoint, int ownerKey) + { + steamAuthTicket = SteamManager.GetAuthSessionTicket(); + //TODO: wait for GetAuthSessionTicketResponse_t + + if (steamAuthTicket == null) + { + throw new Exception("GetAuthSessionTicket returned null"); + } + + if (!(endPoint is UInt64 steamIdEndpoint)) + { + throw new InvalidCastException("endPoint is not UInt64"); + } + + hostSteamId = steamIdEndpoint; + + Steam.SteamManager.Instance.Networking.OnIncomingConnection = OnIncomingConnection; + Steam.SteamManager.Instance.Networking.OnP2PData = OnP2PData; + Steam.SteamManager.Instance.Networking.SetListenChannel(0, true); + + ServerConnection = new SteamP2PConnection("Server", hostSteamId); + + incomingInitializationMessages = new List(); + incomingDataMessages = new List(); + + IWriteMessage outMsg = new WriteOnlyMessage(); + outMsg.Write((byte)DeliveryMethod.Reliable); + outMsg.Write((byte)PacketHeader.IsConnectionInitializationStep); + outMsg.Write((byte)ConnectionInitialization.ConnectionStarted); + + SteamManager.Instance.Networking.SendP2PPacket(hostSteamId, outMsg.Buffer, outMsg.LengthBytes, + Facepunch.Steamworks.Networking.SendType.Reliable); + + initializationStep = ConnectionInitialization.SteamTicketAndVersion; + + timeout = 20.0; + heartbeatTimer = 1.0; + + isActive = true; + } + + private bool OnIncomingConnection(UInt64 steamId) + { + if (!isActive) { return false; } + return steamId == hostSteamId; + } + + private void OnP2PData(ulong steamId, byte[] data, int dataLength, int channel) + { + if (!isActive) { return; } + if (steamId != hostSteamId) { return; } + + timeout = 20.0; + + byte incByte = data[0]; + bool isCompressed = (incByte & (byte)PacketHeader.IsCompressed) != 0; + bool isConnectionInitializationStep = (incByte & (byte)PacketHeader.IsConnectionInitializationStep) != 0; + bool isDisconnectMessage = (incByte & (byte)PacketHeader.IsDisconnectMessage) != 0; + bool isServerMessage = (incByte & (byte)PacketHeader.IsServerMessage) != 0; + bool isHeartbeatMessage = (incByte & (byte)PacketHeader.IsHeartbeatMessage) != 0; + + if (!isServerMessage) { return; } + + if (isConnectionInitializationStep) + { + ulong low = Lidgren.Network.NetBitWriter.ReadUInt32(data, 32, 8); + ulong high = Lidgren.Network.NetBitWriter.ReadUInt32(data, 32, 8+32); + ulong lobbyId = low + (high << 32); + + Steam.SteamManager.JoinLobby(lobbyId, false); + IReadMessage inc = new ReadOnlyMessage(data, false, 1+8, dataLength - 9, ServerConnection); + if (initializationStep != ConnectionInitialization.Success) + { + incomingInitializationMessages.Add(inc); + } + } + else if (isHeartbeatMessage) + { + return; //TODO: implement heartbeats + } + else if (isDisconnectMessage) + { + IReadMessage inc = new ReadOnlyMessage(data, false, 1, dataLength - 1, ServerConnection); + string msg = inc.ReadString(); + Close(msg); + } + else + { + UInt16 length = data[1]; + length |= (UInt16)(((UInt32)data[2]) << 8); + + IReadMessage inc = new ReadOnlyMessage(data, isCompressed, 3, length, ServerConnection); + incomingDataMessages.Add(inc); + } + } + + public override void Update(float deltaTime) + { + if (!isActive) { return; } + + timeout -= deltaTime; + heartbeatTimer -= deltaTime; + + if (heartbeatTimer < 0.0) + { + IWriteMessage outMsg = new WriteOnlyMessage(); + outMsg.Write((byte)DeliveryMethod.Unreliable); + outMsg.Write((byte)PacketHeader.IsHeartbeatMessage); + + SteamManager.Instance.Networking.SendP2PPacket(hostSteamId, outMsg.Buffer, outMsg.LengthBytes, + Facepunch.Steamworks.Networking.SendType.Unreliable); + + heartbeatTimer = 5.0; + } + + if (timeout < 0.0) + { + Close("Timed out"); + return; + } + + if (initializationStep != ConnectionInitialization.Success) + { + if (incomingDataMessages.Count > 0) + { + OnInitializationComplete?.Invoke(); + initializationStep = ConnectionInitialization.Success; + } + else + { + foreach (IReadMessage inc in incomingInitializationMessages) + { + ReadConnectionInitializationStep(inc); + } + } + } + + if (initializationStep == ConnectionInitialization.Success) + { + foreach (IReadMessage inc in incomingDataMessages) + { + OnMessageReceived?.Invoke(inc); + } + } + + incomingInitializationMessages.Clear(); + incomingDataMessages.Clear(); + } + + private void ReadConnectionInitializationStep(IReadMessage inc) + { + if (!isActive) { return; } + + ConnectionInitialization step = (ConnectionInitialization)inc.ReadByte(); + //DebugConsole.NewMessage(step + " " + initializationStep); + switch (step) + { + case ConnectionInitialization.SteamTicketAndVersion: + if (initializationStep != ConnectionInitialization.SteamTicketAndVersion) { return; } + IWriteMessage outMsg = new WriteOnlyMessage(); + outMsg.Write((byte)DeliveryMethod.Reliable); + outMsg.Write((byte)PacketHeader.IsConnectionInitializationStep); + outMsg.Write((byte)ConnectionInitialization.SteamTicketAndVersion); + outMsg.Write(Name); + outMsg.Write(SteamManager.GetSteamID()); + outMsg.Write((UInt16)steamAuthTicket.Data.Length); + outMsg.Write(steamAuthTicket.Data, 0, steamAuthTicket.Data.Length); + + outMsg.Write(GameMain.Version.ToString()); + + IEnumerable mpContentPackages = GameMain.SelectedPackages.Where(cp => cp.HasMultiplayerIncompatibleContent); + outMsg.WriteVariableUInt32((UInt32)mpContentPackages.Count()); + foreach (ContentPackage contentPackage in mpContentPackages) + { + outMsg.Write(contentPackage.Name); + outMsg.Write(contentPackage.MD5hash.Hash); + } + + heartbeatTimer = 5.0; + SteamManager.Instance.Networking.SendP2PPacket(hostSteamId, outMsg.Buffer, outMsg.LengthBytes, + Facepunch.Steamworks.Networking.SendType.Reliable); + break; + case ConnectionInitialization.Password: + if (initializationStep == ConnectionInitialization.SteamTicketAndVersion) { initializationStep = ConnectionInitialization.Password; } + if (initializationStep != ConnectionInitialization.Password) { return; } + bool incomingSalt = inc.ReadBoolean(); inc.ReadPadBits(); + int retries = 0; + if (incomingSalt) + { + passwordSalt = inc.ReadInt32(); + } + else + { + retries = inc.ReadInt32(); + } + OnRequestPassword?.Invoke(passwordSalt, retries); + break; + } + } + + public override void Send(IWriteMessage msg, DeliveryMethod deliveryMethod) + { + if (!isActive) { return; } + + byte[] buf = new byte[msg.LengthBytes + 4]; + buf[0] = (byte)deliveryMethod; + + byte[] bufAux = new byte[msg.LengthBytes]; + bool isCompressed; int length; + msg.PrepareForSending(ref bufAux, out isCompressed, out length); + + buf[1] = (byte)(isCompressed ? PacketHeader.IsCompressed : PacketHeader.None); + + buf[2] = (byte)(length & 0xff); + buf[3] = (byte)((length >> 8) & 0xff); + + Array.Copy(bufAux, 0, buf, 4, length); + + Facepunch.Steamworks.Networking.SendType sendType; + switch (deliveryMethod) + { + case DeliveryMethod.Reliable: + case DeliveryMethod.ReliableOrdered: + //the documentation seems to suggest that the Reliable send type + //enforces packet order (TODO: verify) + sendType = Facepunch.Steamworks.Networking.SendType.Reliable; + break; + default: + sendType = Facepunch.Steamworks.Networking.SendType.Unreliable; + break; + } + + if (length + 8 >= MsgConstants.MTU) + { + DebugConsole.Log("WARNING: message length comes close to exceeding MTU, forcing reliable send (" + length.ToString() + " bytes)"); + sendType = Facepunch.Steamworks.Networking.SendType.Reliable; + } + + heartbeatTimer = 5.0; + bool successSend = SteamManager.Instance.Networking.SendP2PPacket(hostSteamId, buf, length + 4, sendType); + + if (!successSend) + { + if (sendType != Facepunch.Steamworks.Networking.SendType.Reliable) + { + DebugConsole.Log("WARNING: message couldn't be sent unreliably, forcing reliable send (" + length.ToString() + " bytes)"); + sendType = Facepunch.Steamworks.Networking.SendType.Reliable; + successSend = Steam.SteamManager.Instance.Networking.SendP2PPacket(hostSteamId, buf, length + 4, sendType); + } + if (!successSend) + { + DebugConsole.ThrowError("Failed to send message to remote peer! (" + length.ToString() + " bytes)"); + } + } + } + + public override void SendPassword(string password) + { + if (!isActive) { return; } + + if (initializationStep != ConnectionInitialization.Password) { return; } + IWriteMessage outMsg = new WriteOnlyMessage(); + outMsg.Write((byte)DeliveryMethod.Reliable); + outMsg.Write((byte)PacketHeader.IsConnectionInitializationStep); + outMsg.Write((byte)ConnectionInitialization.Password); + byte[] saltedPw = ServerSettings.SaltPassword(Lidgren.Network.NetUtility.ComputeSHAHash(Encoding.UTF8.GetBytes(password)), passwordSalt); + outMsg.Write((byte)saltedPw.Length); + outMsg.Write(saltedPw, 0, saltedPw.Length); + + heartbeatTimer = 5.0; + SteamManager.Instance.Networking.SendP2PPacket(hostSteamId, outMsg.Buffer, outMsg.LengthBytes, + Facepunch.Steamworks.Networking.SendType.Reliable); + } + + public override void Close(string msg = null) + { + if (!isActive) { return; } + + SteamManager.LeaveLobby(); + + isActive = false; + + IWriteMessage outMsg = new WriteOnlyMessage(); + outMsg.Write((byte)DeliveryMethod.Reliable); + outMsg.Write((byte)PacketHeader.IsDisconnectMessage); + outMsg.Write(msg ?? "Disconnected"); + + SteamManager.Instance.Networking.SendP2PPacket(hostSteamId, outMsg.Buffer, outMsg.LengthBytes, + Facepunch.Steamworks.Networking.SendType.Reliable); + + Thread.Sleep(100); + + Steam.SteamManager.Instance.Networking.OnIncomingConnection = null; + Steam.SteamManager.Instance.Networking.OnP2PData = null; + Steam.SteamManager.Instance.Networking.SetListenChannel(0, false); + + Steam.SteamManager.Instance.Networking.CloseSession(hostSteamId); + + steamAuthTicket?.Cancel(); steamAuthTicket = null; + hostSteamId = 0; + + OnDisconnect?.Invoke(msg); + } + } +} diff --git a/Barotrauma/BarotraumaClient/Source/Networking/Primitives/Peers/SteamP2POwnerPeer.cs b/Barotrauma/BarotraumaClient/Source/Networking/Primitives/Peers/SteamP2POwnerPeer.cs new file mode 100644 index 000000000..067920920 --- /dev/null +++ b/Barotrauma/BarotraumaClient/Source/Networking/Primitives/Peers/SteamP2POwnerPeer.cs @@ -0,0 +1,472 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Text; +using Lidgren.Network; +using Facepunch.Steamworks; +using Barotrauma.Steam; +using System.Linq; +using System.Threading; + +namespace Barotrauma.Networking +{ + class SteamP2POwnerPeer : ClientPeer + { + private bool isActive; + private NetClient netClient; + private NetPeerConfiguration netPeerConfiguration; + + private ConnectionInitialization initializationStep; + private UInt64 selfSteamID; + List incomingLidgrenMessages; + + class RemotePeer + { + public UInt64 SteamID; + public double? DisconnectTime; + public bool Authenticating; + public bool Authenticated; + public List> UnauthedMessages; + + public RemotePeer(UInt64 steamId) + { + SteamID = steamId; + DisconnectTime = null; + Authenticating = false; + Authenticated = false; + + UnauthedMessages = new List>(); + } + + } + List remotePeers; + + public SteamP2POwnerPeer(string name) + { + ServerConnection = null; + + Name = name; + + netClient = null; + isActive = false; + + selfSteamID = Steam.SteamManager.GetSteamID(); + } + + public override void Start(object endPoint, int ownerKey) + { + if (isActive) { return; } + + netPeerConfiguration = new NetPeerConfiguration("barotrauma"); + + netPeerConfiguration.DisableMessageType(NetIncomingMessageType.DebugMessage | NetIncomingMessageType.WarningMessage | NetIncomingMessageType.Receipt + | NetIncomingMessageType.ErrorMessage | NetIncomingMessageType.Error); + + netClient = new NetClient(netPeerConfiguration); + + incomingLidgrenMessages = new List(); + + initializationStep = ConnectionInitialization.SteamTicketAndVersion; + + IPEndPoint ipEndPoint = new IPEndPoint(IPAddress.Loopback, Steam.SteamManager.STEAMP2P_OWNER_PORT); + + netClient.Start(); + ServerConnection = new LidgrenConnection("Server", netClient.Connect(ipEndPoint), 0); + ServerConnection.Status = NetworkConnectionStatus.Connected; + + remotePeers = new List(); + + Steam.SteamManager.Instance.Networking.OnIncomingConnection = OnIncomingConnection; + Steam.SteamManager.Instance.Networking.OnP2PData = OnP2PData; + Steam.SteamManager.Instance.Networking.SetListenChannel(0, true); + Steam.SteamManager.Instance.Auth.OnAuthChange = OnAuthChange; + + isActive = true; + } + + private void OnAuthChange(ulong steamID, ulong ownerID, ClientAuthStatus status) + { + RemotePeer remotePeer = remotePeers.Find(p => p.SteamID == steamID); + DebugConsole.NewMessage(steamID + " validation: " + status + ", " + (remotePeer != null)); + + if (remotePeer == null) { return; } + + if (remotePeer.Authenticated) + { + if (status != ClientAuthStatus.OK) + { + DisconnectPeer(remotePeer, DisconnectReason.SteamAuthenticationFailed.ToString() + "/ Steam authentication status changed: " + status.ToString()); + } + return; + } + + if (status == ClientAuthStatus.OK) + { + remotePeer.Authenticated = true; + remotePeer.Authenticating = false; + foreach (var msg in remotePeer.UnauthedMessages) + { + netClient.SendMessage(msg.Second, msg.First); + } + remotePeer.UnauthedMessages.Clear(); + } + else + { + DisconnectPeer(remotePeer, DisconnectReason.SteamAuthenticationFailed.ToString() + "/ Steam authentication failed: " + status.ToString()); + return; + } + } + + private bool OnIncomingConnection(UInt64 steamId) + { + if (!isActive) { return false; } + + if (!remotePeers.Any(p => p.SteamID == steamId)) + { + remotePeers.Add(new RemotePeer(steamId)); + } + + return true; //accept all connections, the server will figure things out later + } + + private void OnP2PData(ulong steamId, byte[] data, int dataLength, int channel) + { + if (!isActive) { return; } + + RemotePeer remotePeer = remotePeers.Find(p => p.SteamID == steamId); + if (remotePeer == null || remotePeer.DisconnectTime != null) + { + return; + } + + NetOutgoingMessage outMsg = netClient.CreateMessage(); + outMsg.Write(steamId); + outMsg.Write(data, 1, dataLength - 1); + + NetDeliveryMethod lidgrenDeliveryMethod = NetDeliveryMethod.Unreliable; + switch ((DeliveryMethod)data[0]) + { + case DeliveryMethod.Unreliable: + lidgrenDeliveryMethod = NetDeliveryMethod.Unreliable; + break; + case DeliveryMethod.Reliable: + lidgrenDeliveryMethod = NetDeliveryMethod.ReliableUnordered; + break; + case DeliveryMethod.ReliableOrdered: + lidgrenDeliveryMethod = NetDeliveryMethod.ReliableOrdered; + break; + } + + byte incByte = data[1]; + bool isCompressed = (incByte & (byte)PacketHeader.IsCompressed) != 0; + bool isConnectionInitializationStep = (incByte & (byte)PacketHeader.IsConnectionInitializationStep) != 0; + bool isDisconnectMessage = (incByte & (byte)PacketHeader.IsDisconnectMessage) != 0; + bool isServerMessage = (incByte & (byte)PacketHeader.IsServerMessage) != 0; + bool isHeartbeatMessage = (incByte & (byte)PacketHeader.IsHeartbeatMessage) != 0; + + if (!remotePeer.Authenticated) + { + if (!remotePeer.Authenticating) + { + if (isConnectionInitializationStep) + { + remotePeer.DisconnectTime = null; + + IReadMessage authMsg = new ReadOnlyMessage(data, isCompressed, 2, dataLength - 2, null); + ConnectionInitialization initializationStep = (ConnectionInitialization)authMsg.ReadByte(); + if (initializationStep == ConnectionInitialization.SteamTicketAndVersion) + { + remotePeer.Authenticating = true; + + authMsg.ReadString(); //skip name + authMsg.ReadUInt64(); //skip steamid + UInt16 ticketLength = authMsg.ReadUInt16(); + byte[] ticket = authMsg.ReadBytes(ticketLength); + + ClientStartAuthSessionResult authSessionStartState = Steam.SteamManager.StartAuthSession(ticket, steamId); + if (authSessionStartState != ClientStartAuthSessionResult.OK) + { + DisconnectPeer(remotePeer, DisconnectReason.SteamAuthenticationFailed.ToString() + "/ Steam auth session failed to start: " + authSessionStartState.ToString()); + return; + } + } + } + } + } + + if (remotePeer.Authenticating) + { + remotePeer.UnauthedMessages.Add(new Pair(lidgrenDeliveryMethod, outMsg)); + } + else + { + netClient.SendMessage(outMsg, lidgrenDeliveryMethod); + } + } + + public override void Update(float deltaTime) + { + if (!isActive) { return; } + + for (int i = remotePeers.Count - 1; i >= 0; i--) + { + if (remotePeers[i].DisconnectTime != null && remotePeers[i].DisconnectTime < Timing.TotalTime) + { + ClosePeerSession(remotePeers[i]); + } + } + + netClient.ReadMessages(incomingLidgrenMessages); + + foreach (NetIncomingMessage inc in incomingLidgrenMessages) + { + if (inc.SenderConnection != (ServerConnection as LidgrenConnection).NetConnection) { continue; } + + switch (inc.MessageType) + { + case NetIncomingMessageType.Data: + HandleDataMessage(inc); + break; + case NetIncomingMessageType.StatusChanged: + HandleStatusChanged(inc); + break; + } + } + + incomingLidgrenMessages.Clear(); + } + + private void HandleDataMessage(NetIncomingMessage inc) + { + if (!isActive) { return; } + + UInt64 recipientSteamId = inc.ReadUInt64(); + + int p2pDataStart = inc.PositionInBytes; + + byte incByte = inc.ReadByte(); + + bool isCompressed = (incByte & (byte)PacketHeader.IsCompressed) != 0; + bool isConnectionInitializationStep = (incByte & (byte)PacketHeader.IsConnectionInitializationStep) != 0; + bool isDisconnectMessage = (incByte & (byte)PacketHeader.IsDisconnectMessage) != 0; + bool isServerMessage = (incByte & (byte)PacketHeader.IsServerMessage) != 0; + bool isHeartbeatMessage = (incByte & (byte)PacketHeader.IsHeartbeatMessage) != 0; + + if (recipientSteamId != selfSteamID) + { + if (!isServerMessage) + { + DebugConsole.ThrowError("Received non-server message meant for remote peer"); + return; + } + + RemotePeer peer = remotePeers.Find(p => p.SteamID == recipientSteamId); + + if (peer == null) { return; } + + if (isDisconnectMessage) + { + DisconnectPeer(peer, inc.ReadString()); + return; + } + + Facepunch.Steamworks.Networking.SendType sendType; + switch (inc.DeliveryMethod) + { + case NetDeliveryMethod.ReliableUnordered: + case NetDeliveryMethod.ReliableSequenced: + case NetDeliveryMethod.ReliableOrdered: + //the documentation seems to suggest that the Reliable send type + //enforces packet order (TODO: verify) + sendType = Facepunch.Steamworks.Networking.SendType.Reliable; + break; + default: + sendType = Facepunch.Steamworks.Networking.SendType.Unreliable; + break; + } + + byte[] p2pData; + + if (isConnectionInitializationStep) + { + p2pData = new byte[inc.LengthBytes - p2pDataStart + 8]; + p2pData[0] = inc.Data[p2pDataStart]; + Lidgren.Network.NetBitWriter.WriteUInt64(Steam.SteamManager.Instance.Lobby.CurrentLobby, 64, p2pData, 8); + Array.Copy(inc.Data, p2pDataStart+1, p2pData, 9, inc.LengthBytes - p2pDataStart - 1); + } + else + { + p2pData = new byte[inc.LengthBytes - p2pDataStart]; + Array.Copy(inc.Data, p2pDataStart, p2pData, 0, p2pData.Length); + } + + if (p2pData.Length + 4 >= MsgConstants.MTU) + { + DebugConsole.Log("WARNING: message length comes close to exceeding MTU, forcing reliable send (" + p2pData.Length.ToString() + " bytes)"); + sendType = Facepunch.Steamworks.Networking.SendType.Reliable; + } + + bool successSend = Steam.SteamManager.Instance.Networking.SendP2PPacket(recipientSteamId, p2pData, p2pData.Length, sendType); + + if (!successSend) + { + if (sendType != Facepunch.Steamworks.Networking.SendType.Reliable) + { + DebugConsole.Log("WARNING: message couldn't be sent unreliably, forcing reliable send (" + p2pData.Length.ToString() + " bytes)"); + sendType = Facepunch.Steamworks.Networking.SendType.Reliable; + successSend = Steam.SteamManager.Instance.Networking.SendP2PPacket(recipientSteamId, p2pData, p2pData.Length, sendType); + } + if (!successSend) + { + DebugConsole.ThrowError("Failed to send message to remote peer! (" + p2pData.Length.ToString() + " bytes)"); + } + } + } + else + { + if (isDisconnectMessage) + { + DebugConsole.ThrowError("Received disconnect message from owned server"); + return; + } + if (!isServerMessage) + { + DebugConsole.ThrowError("Received non-server message from owned server"); + return; + } + if (isHeartbeatMessage) + { + return; //timeout is handled by Lidgren, ignore this message + } + if (isConnectionInitializationStep) + { + NetOutgoingMessage outMsg = netClient.CreateMessage(); + outMsg.Write(selfSteamID); + outMsg.Write((byte)(PacketHeader.IsConnectionInitializationStep)); + outMsg.Write(Name); + netClient.SendMessage(outMsg, NetDeliveryMethod.ReliableUnordered); + + return; + } + else + { + if (initializationStep != ConnectionInitialization.Success) + { + OnInitializationComplete?.Invoke(); + initializationStep = ConnectionInitialization.Success; + } + UInt16 length = inc.ReadUInt16(); + IReadMessage msg = new ReadOnlyMessage(inc.Data, isCompressed, inc.PositionInBytes, length, ServerConnection); + OnMessageReceived?.Invoke(msg); + + return; + } + } + } + + private void DisconnectPeer(RemotePeer peer, string msg) + { + if (!string.IsNullOrWhiteSpace(msg)) + { + if (peer.DisconnectTime == null) + { + peer.DisconnectTime = Timing.TotalTime + 1.0; + } + + IWriteMessage outMsg = new WriteOnlyMessage(); + outMsg.Write((byte)(PacketHeader.IsServerMessage | PacketHeader.IsDisconnectMessage)); + outMsg.Write(msg); + + Steam.SteamManager.Instance.Networking.SendP2PPacket(peer.SteamID, outMsg.Buffer, outMsg.LengthBytes, + Facepunch.Steamworks.Networking.SendType.Reliable); + } + else + { + ClosePeerSession(peer); + } + } + + private void ClosePeerSession(RemotePeer peer) + { + Steam.SteamManager.Instance.Networking.CloseSession(peer.SteamID); + remotePeers.Remove(peer); + } + + private void HandleStatusChanged(NetIncomingMessage inc) + { + if (!isActive) { return; } + + NetConnectionStatus status = (NetConnectionStatus)inc.ReadByte(); + switch (status) + { + case NetConnectionStatus.Disconnected: + string disconnectMsg = inc.ReadString(); + Close(disconnectMsg); + break; + } + } + + public override void SendPassword(string password) + { + return; //owner doesn't send passwords + } + + public override void Close(string msg = null) + { + if (!isActive) { return; } + + isActive = false; + + for (int i=remotePeers.Count-1;i>=0;i--) + { + DisconnectPeer(remotePeers[i], msg ?? DisconnectReason.ServerShutdown.ToString()); + } + + Thread.Sleep(100); + + for (int i = remotePeers.Count - 1; i >= 0; i--) + { + ClosePeerSession(remotePeers[i]); + } + + netClient.Shutdown(msg ?? TextManager.Get("Disconnecting")); + netClient = null; + + OnDisconnect?.Invoke(msg); + + Steam.SteamManager.Instance.Networking.OnIncomingConnection = null; + Steam.SteamManager.Instance.Networking.OnP2PData = null; + Steam.SteamManager.Instance.Networking.SetListenChannel(0, false); + Steam.SteamManager.Instance.Auth.OnAuthChange = null; + } + + public override void Send(IWriteMessage msg, DeliveryMethod deliveryMethod) + { + if (!isActive) { return; } + + NetDeliveryMethod lidgrenDeliveryMethod = NetDeliveryMethod.Unreliable; + switch (deliveryMethod) + { + case DeliveryMethod.Unreliable: + lidgrenDeliveryMethod = NetDeliveryMethod.Unreliable; + break; + case DeliveryMethod.Reliable: + lidgrenDeliveryMethod = NetDeliveryMethod.ReliableUnordered; + break; + case DeliveryMethod.ReliableOrdered: + lidgrenDeliveryMethod = NetDeliveryMethod.ReliableOrdered; + break; + } + + NetOutgoingMessage lidgrenMsg = netClient.CreateMessage(); + byte[] msgData = new byte[msg.LengthBytes]; + msg.PrepareForSending(ref msgData, out bool isCompressed, out int length); + lidgrenMsg.Write(selfSteamID); + lidgrenMsg.Write((byte)(isCompressed ? PacketHeader.IsCompressed : PacketHeader.None)); + lidgrenMsg.Write((UInt16)length); + lidgrenMsg.Write(msgData, 0, length); + + netClient.SendMessage(lidgrenMsg, lidgrenDeliveryMethod); + } + } +} diff --git a/Barotrauma/BarotraumaClient/Source/Networking/RespawnManager.cs b/Barotrauma/BarotraumaClient/Source/Networking/RespawnManager.cs index 06d4fd199..aad9283e8 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/RespawnManager.cs @@ -21,8 +21,7 @@ namespace Barotrauma.Networking GameMain.Client.AddChatMessage("ServerMessage.ShuttleLeaving", ChatMessageType.Server); } } - - public void ClientRead(ServerNetObject type, NetBuffer msg, float sendingTime) + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { var newState = (State)msg.ReadRangedInteger(0, Enum.GetNames(typeof(State)).Length); diff --git a/Barotrauma/BarotraumaClient/Source/Networking/ServerInfo.cs b/Barotrauma/BarotraumaClient/Source/Networking/ServerInfo.cs index fa1b4510d..167bb9e89 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/ServerInfo.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/ServerInfo.cs @@ -1,5 +1,6 @@ using Barotrauma.Steam; using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; using System.Linq; @@ -9,6 +10,9 @@ namespace Barotrauma.Networking { public string IP; public string Port; + + public UInt64 LobbyID; + public string ServerName; public string ServerMessage; public bool GameStarted; diff --git a/Barotrauma/BarotraumaClient/Source/Networking/ServerLog.cs b/Barotrauma/BarotraumaClient/Source/Networking/ServerLog.cs index e636ac341..da33bf8b0 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/ServerLog.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/ServerLog.cs @@ -5,7 +5,7 @@ using System.Linq; namespace Barotrauma.Networking { - partial class ServerLog + public partial class ServerLog { public GUIButton LogFrame; private GUIListBox listBox; diff --git a/Barotrauma/BarotraumaClient/Source/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/Source/Networking/ServerSettings.cs index 2040864b4..b8b849359 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/ServerSettings.cs @@ -1,5 +1,4 @@ -using Lidgren.Network; -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.ComponentModel; @@ -31,6 +30,10 @@ namespace Barotrauma.Networking else if (GUIComponent is GUIScrollBar scrollBar) return scrollBar.BarScrollValue; else if (GUIComponent is GUIRadioButtonGroup radioButtonGroup) return radioButtonGroup.Selected; else if (GUIComponent is GUIDropDown dropdown) return dropdown.SelectedData; + else if (GUIComponent is GUINumberInput numInput) + { + if (numInput.InputType == GUINumberInput.NumberType.Int) { return numInput.IntValue; } else { return numInput.FloatValue; } + } return null; } set @@ -38,9 +41,30 @@ namespace Barotrauma.Networking if (GUIComponent == null) return; else if (GUIComponent is GUITickBox tickBox) tickBox.Selected = (bool)value; else if (GUIComponent is GUITextBox textBox) textBox.Text = (string)value; - else if (GUIComponent is GUIScrollBar scrollBar) scrollBar.BarScrollValue = (float)value; + else if (GUIComponent is GUIScrollBar scrollBar) + { + if (value.GetType() == typeof(int)) + { + scrollBar.BarScrollValue = (int)value; + } + else + { + scrollBar.BarScrollValue = (float)value; + } + } else if (GUIComponent is GUIRadioButtonGroup radioButtonGroup) radioButtonGroup.Selected = (Enum)value; else if (GUIComponent is GUIDropDown dropdown) dropdown.SelectItem(value); + else if (GUIComponent is GUINumberInput numInput) + { + if (numInput.InputType == GUINumberInput.NumberType.Int) + { + numInput.IntValue = (int)value; + } + else + { + numInput.FloatValue = (float)value; + } + } } } @@ -68,7 +92,7 @@ namespace Barotrauma.Networking } } - public void ClientAdminRead(NetBuffer incMsg) + public void ClientAdminRead(IReadMessage incMsg) { int count = incMsg.ReadUInt16(); for (int i = 0; i < count; i++) @@ -91,7 +115,7 @@ namespace Barotrauma.Networking else { UInt32 size = incMsg.ReadVariableUInt32(); - incMsg.Position += 8 * size; + incMsg.BitPosition += (int)(8 * size); } } @@ -100,10 +124,14 @@ namespace Barotrauma.Networking Whitelist.ClientAdminRead(incMsg); } - public void ClientRead(NetBuffer incMsg) + public void ClientRead(IReadMessage incMsg) { ServerName = incMsg.ReadString(); ServerMessageText = incMsg.ReadString(); + MaxPlayers = incMsg.ReadByte(); + HasPassword = incMsg.ReadBoolean(); + isPublic = incMsg.ReadBoolean(); + incMsg.ReadPadBits(); TickRate = incMsg.ReadRangedInteger(1, 60); GameMain.NetworkMember.TickRate = TickRate; @@ -123,7 +151,7 @@ namespace Barotrauma.Networking { if (!GameMain.Client.HasPermission(Networking.ClientPermissions.ManageSettings)) return; - NetOutgoingMessage outMsg = GameMain.NetworkMember.NetPeer.CreateMessage(); + IWriteMessage outMsg = new WriteOnlyMessage(); outMsg.Write((byte)ClientPacketHeader.SERVER_SETTINGS); @@ -191,7 +219,7 @@ namespace Barotrauma.Networking outMsg.Write(GameMain.NetLobbyScreen.SeedBox.Text); } - (GameMain.NetworkMember.NetPeer as NetClient).SendMessage(outMsg, NetDeliveryMethod.ReliableOrdered); + GameMain.Client.ClientPeer.Send(outMsg, DeliveryMethod.Reliable); } //GUI stuff @@ -508,7 +536,7 @@ namespace Barotrauma.Networking var ragdollButtonBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), roundsTab.RectTransform), TextManager.Get("ServerSettingsAllowRagdollButton")); GetPropertyData("AllowRagdollButton").AssignGUIComponent(ragdollButtonBox); - var traitorRatioBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), roundsTab.RectTransform), TextManager.Get("ServerSettingsUseTraitorRatio")); + /*var traitorRatioBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), roundsTab.RectTransform), TextManager.Get("ServerSettingsUseTraitorRatio")); CreateLabeledSlider(roundsTab, "", out slider, out sliderLabel); var traitorRatioSlider = slider; @@ -553,7 +581,7 @@ namespace Barotrauma.Networking GetPropertyData("TraitorRatio").AssignGUIComponent(traitorRatioSlider); traitorRatioSlider.OnMoved(traitorRatioSlider, traitorRatioSlider.BarScroll); - traitorRatioBox.OnSelected(traitorRatioBox); + traitorRatioBox.OnSelected(traitorRatioBox);*/ var buttonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.07f), roundsTab.RectTransform), isHorizontal: true) { @@ -829,4 +857,4 @@ namespace Barotrauma.Networking return false; } } -} \ No newline at end of file +} diff --git a/Barotrauma/BarotraumaClient/Source/Networking/SteamManager.cs b/Barotrauma/BarotraumaClient/Source/Networking/SteamManager.cs index e0443bb38..128dfaaef 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/SteamManager.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/SteamManager.cs @@ -6,70 +6,269 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; namespace Barotrauma.Steam { partial class SteamManager { - private static List initializationErrors = new List(); - public static IEnumerable InitializationErrors - { - get { return initializationErrors; } - } + public Facepunch.Steamworks.Networking Networking => client?.Networking; + public Facepunch.Steamworks.User User => client?.User; + public Facepunch.Steamworks.Friends Friends => client?.Friends; + public Facepunch.Steamworks.Overlay Overlay => client?.Overlay; + public Facepunch.Steamworks.Auth Auth => client?.Auth; + public Facepunch.Steamworks.Lobby Lobby => client?.Lobby; + public Facepunch.Steamworks.LobbyList LobbyList => client?.LobbyList; private SteamManager() { + client = null; + isInitialized = InitializeClient(); + } + + private bool InitializeClient() + { + if (client != null) { return true; } + bool clientInitialized = false; try { client = new Facepunch.Steamworks.Client(AppID); - isInitialized = client.IsSubscribed && client.IsValid; + clientInitialized = client.IsSubscribed && client.IsValid; - if (isInitialized) + if (clientInitialized) { - DebugConsole.Log("Logged in as " + client.Username + " (SteamID " + client.SteamId + ")"); + DebugConsole.NewMessage("Logged in as " + client.Username + " (SteamID " + SteamIDUInt64ToString(client.SteamId) + ")"); } } catch (DllNotFoundException) { - isInitialized = false; + clientInitialized = false; initializationErrors.Add("SteamDllNotFound"); } catch (Exception) { - isInitialized = false; + clientInitialized = false; initializationErrors.Add("SteamClientInitFailed"); } - if (!isInitialized) + if (!clientInitialized) { try { - Facepunch.Steamworks.Client.Instance.Dispose(); } catch (Exception e) { if (GameSettings.VerboseLogging) DebugConsole.ThrowError("Disposing Steam client failed.", e); } + client = null; + } + return clientInitialized; + } + + private enum LobbyState + { + NotConnected, + Creating, + Owner, + Joining, + Joined + } + private static UInt64 lobbyID = 0; + private static LobbyState lobbyState = LobbyState.NotConnected; + private static string lobbyIP = ""; + private static Thread lobbyIPRetrievalThread; + + private static void RetrieveLobbyIP() + { + //TODO: set up our own server for IP retrieval? + + Server tempServer = null; + try + { + var serverInit = new ServerInit("Barotrauma", "Barotrauma IP Retrieval") + { + GamePort = (ushort)27015, + QueryPort = (ushort)27016 + }; + tempServer = new Server(AppID, serverInit, false); + if (!tempServer.IsValid) + { + tempServer.Dispose(); + tempServer = null; + DebugConsole.ThrowError("Failed to retrieve public IP: Initializing Steam server failed."); + return; + } + + tempServer.LogOnAnonymous(); + lobbyIP = ""; + string error = "Timed out."; + for (int i = 0; i < 30*60; i++) + { + if (instance.client.Lobby.CurrentLobby == 0) + { + error = ""; + break; + } + tempServer.Update(); + tempServer.ForceHeartbeat(); + if (tempServer.PublicIp != null) + { + if (instance.client.Lobby.CurrentLobby != 0) + { + lobbyIP = tempServer.PublicIp.ToString(); + DebugConsole.NewMessage("Successfully retrieved public IP: " + lobbyIP, Microsoft.Xna.Framework.Color.Lime); + instance.client.Lobby.CurrentLobbyData.SetData("hostipaddress", lobbyIP); + } + else + { + error = ""; + lobbyIP = ""; + } + break; + } + Thread.Sleep(16); + } + + tempServer.Dispose(); + tempServer = null; + if (string.IsNullOrWhiteSpace(lobbyIP) && !string.IsNullOrWhiteSpace(error)) + { + DebugConsole.ThrowError("Failed to retrieve public IP: "+error); + } + } + catch + { + tempServer?.Dispose(); + tempServer = null; } } - public static ulong GetSteamID() + public static void CreateLobby(ServerSettings serverSettings) { - if (instance == null || !instance.isInitialized) + instance.client.Lobby.OnLobbyJoined = null; + instance.client.Lobby.OnLobbyCreated = (success) => { - return 0; + if (!success) + { + DebugConsole.ThrowError("Failed to create Steam lobby!"); + lobbyState = LobbyState.NotConnected; + return; + } + + DebugConsole.NewMessage("Lobby created!", Microsoft.Xna.Framework.Color.Lime); + + lobbyIPRetrievalThread?.Abort(); + lobbyIPRetrievalThread?.Join(); + lobbyIPRetrievalThread = null; + lobbyIPRetrievalThread = new Thread(new ThreadStart(RetrieveLobbyIP)); + lobbyIPRetrievalThread.IsBackground = true; + lobbyIPRetrievalThread.Start(); + + lobbyState = LobbyState.Owner; + lobbyID = instance.client.Lobby.CurrentLobby; + UpdateLobby(serverSettings); + }; + if (lobbyState != LobbyState.NotConnected) { return; } + lobbyState = LobbyState.Creating; + instance.client.Lobby.Create(serverSettings.isPublic ? Lobby.Type.Public : Lobby.Type.FriendsOnly, serverSettings.MaxPlayers+10); + instance.client.Lobby.Joinable = true; + } + + public static void UpdateLobby(ServerSettings serverSettings) + { + if (GameMain.Client == null) + { + LeaveLobby(); } - return instance.client.SteamId; + + if (lobbyState == LobbyState.NotConnected) + { + CreateLobby(serverSettings); + } + + if (lobbyState != LobbyState.Owner) + { + return; + } + + var contentPackages = GameMain.Config.SelectedContentPackages.Where(cp => cp.HasMultiplayerIncompatibleContent); + + instance.client.Lobby.Name = serverSettings.ServerName; + instance.client.Lobby.Owner = Steam.SteamManager.GetSteamID(); + instance.client.Lobby.MaxMembers = serverSettings.MaxPlayers+10; + instance.client.Lobby.CurrentLobbyData.SetData("playercount", (GameMain.Client?.ConnectedClients?.Count??0).ToString()); + instance.client.Lobby.CurrentLobbyData.SetData("maxplayernum", serverSettings.MaxPlayers.ToString()); + instance.client.Lobby.CurrentLobbyData.SetData("hostipaddress", lobbyIP); + //instance.client.Lobby.CurrentLobbyData.SetData("connectsteamid", Steam.SteamManager.GetSteamID().ToString()); + instance.client.Lobby.CurrentLobbyData.SetData("haspassword", serverSettings.HasPassword.ToString()); + + instance.client.Lobby.CurrentLobbyData.SetData("message", serverSettings.ServerMessageText); + instance.client.Lobby.CurrentLobbyData.SetData("version", GameMain.Version.ToString()); + + instance.client.Lobby.CurrentLobbyData.SetData("contentpackage", string.Join(",", contentPackages.Select(cp => cp.Name))); + instance.client.Lobby.CurrentLobbyData.SetData("contentpackagehash", string.Join(",", contentPackages.Select(cp => cp.MD5hash.Hash))); + instance.client.Lobby.CurrentLobbyData.SetData("contentpackageurl", string.Join(",", contentPackages.Select(cp => cp.SteamWorkshopUrl ?? ""))); + instance.client.Lobby.CurrentLobbyData.SetData("usingwhitelist", (serverSettings.Whitelist != null && serverSettings.Whitelist.Enabled).ToString()); + instance.client.Lobby.CurrentLobbyData.SetData("modeselectionmode", serverSettings.ModeSelectionMode.ToString()); + instance.client.Lobby.CurrentLobbyData.SetData("subselectionmode", serverSettings.SubSelectionMode.ToString()); + instance.client.Lobby.CurrentLobbyData.SetData("voicechatenabled", serverSettings.VoiceChatEnabled.ToString()); + instance.client.Lobby.CurrentLobbyData.SetData("allowspectating", serverSettings.AllowSpectating.ToString()); + instance.client.Lobby.CurrentLobbyData.SetData("allowrespawn", serverSettings.AllowRespawn.ToString()); + instance.client.Lobby.CurrentLobbyData.SetData("traitors", serverSettings.TraitorsEnabled.ToString()); + instance.client.Lobby.CurrentLobbyData.SetData("gamestarted", GameMain.Client.GameStarted.ToString()); + instance.client.Lobby.CurrentLobbyData.SetData("gamemode", serverSettings.GameModeIdentifier); + + DebugConsole.NewMessage("Lobby updated!", Microsoft.Xna.Framework.Color.Lime); } - public static string GetUsername() + public static void LeaveLobby() { - if (instance == null || !instance.isInitialized) + if (lobbyState != LobbyState.NotConnected) { - return ""; + lobbyIPRetrievalThread?.Abort(); + lobbyIPRetrievalThread?.Join(); + lobbyIPRetrievalThread = null; + + instance.client.Lobby.Leave(); + lobbyID = 0; + lobbyIP = ""; + lobbyState = LobbyState.NotConnected; + + instance.client.Lobby.OnLobbyJoined = null; } - return instance.client.Username; + } + public static void JoinLobby(UInt64 id, bool joinServer) + { + if (instance.client.Lobby.CurrentLobby == id) { return; } + if (lobbyID == id) { return; } + instance.client.Lobby.OnLobbyJoined = (success) => + { + try + { + if (!success) + { + DebugConsole.ThrowError("Failed to join Steam lobby: "+id.ToString()); + return; + } + lobbyState = LobbyState.Joined; + lobbyID = instance.client.Lobby.CurrentLobby; + if (joinServer) + { + GameMain.Instance.ConnectLobby = 0; + GameMain.Instance.ConnectName = instance.client.Lobby.Name; + GameMain.Instance.ConnectEndpoint = instance.client.Lobby.Owner.ToString(); + } + } + finally + { + instance.client.Lobby.OnLobbyJoined = null; + } + }; + lobbyState = LobbyState.Joining; + lobbyID = id; + instance.client.Lobby.Join(id); } public static ulong GetWorkshopItemIDFromUrl(string url) @@ -112,13 +311,16 @@ namespace Barotrauma.Steam //the response is queried using the server's query port, not the game port, //so it may be possible to play on the server even if it doesn't respond to server list queries var query = instance.client.ServerList.Internet(filter); - query.OnUpdate += () => { UpdateServerQuery(query, onServerFound, onServerRulesReceived, includeUnresponsive: true); }; + query.OnUpdate = () => { UpdateServerQuery(query, onServerFound, onServerRulesReceived, includeUnresponsive: true); }; query.OnFinished = onFinished; var localQuery = instance.client.ServerList.Local(filter); - localQuery.OnUpdate += () => { UpdateServerQuery(localQuery, onServerFound, onServerRulesReceived, includeUnresponsive: true); }; + localQuery.OnUpdate = () => { UpdateServerQuery(localQuery, onServerFound, onServerRulesReceived, includeUnresponsive: true); }; localQuery.OnFinished = onFinished; + instance.client.LobbyList.OnLobbiesUpdated = () => { UpdateLobbyQuery(onServerFound, onServerRulesReceived, onFinished); }; + instance.client.LobbyList.Refresh(); + return true; } @@ -141,7 +343,7 @@ namespace Barotrauma.Steam //the response is queried using the server's query port, not the game port, //so it may be possible to play on the server even if it doesn't respond to server list queries var query = instance.client.ServerList.Favourites(filter); - query.OnUpdate += () => { UpdateServerQuery(query, onServerFound, onServerRulesReceived, includeUnresponsive: true); }; + query.OnUpdate = () => { UpdateServerQuery(query, onServerFound, onServerRulesReceived, includeUnresponsive: true); }; query.OnFinished = onFinished; return true; @@ -166,12 +368,71 @@ namespace Barotrauma.Steam //the response is queried using the server's query port, not the game port, //so it may be possible to play on the server even if it doesn't respond to server list queries var query = instance.client.ServerList.History(filter); - query.OnUpdate += () => { UpdateServerQuery(query, onServerFound, onServerRulesReceived, includeUnresponsive: true); }; + query.OnUpdate = () => { UpdateServerQuery(query, onServerFound, onServerRulesReceived, includeUnresponsive: true); }; query.OnFinished = onFinished; return true; } + private static void UpdateLobbyQuery(Action onServerFound, Action onServerRulesReceived, Action onFinished) + { + foreach (LobbyList.Lobby lobby in instance.client.LobbyList.Lobbies) + { + if (string.IsNullOrWhiteSpace(lobby.GetData("haspassword"))) { continue; } + bool.TryParse(lobby.GetData("haspassword"), out bool hasPassword); + int.TryParse(lobby.GetData("playercount"), out int currPlayers); + int.TryParse(lobby.GetData("maxplayernum"), out int maxPlayers); + //UInt64.TryParse(lobby.GetData("connectsteamid"), out ulong connectSteamId); + string ip = lobby.GetData("hostipaddress"); + if (string.IsNullOrWhiteSpace(ip)) { ip = ""; } + + var serverInfo = new ServerInfo() + { + ServerName = lobby.Name, + Port = "", + IP = ip, + PlayerCount = currPlayers, + MaxPlayers = maxPlayers, + HasPassword = hasPassword, + RespondedToSteamQuery = true, + LobbyID = lobby.LobbyID + }; + serverInfo.PingChecked = false; + serverInfo.ServerMessage = lobby.GetData("message"); + serverInfo.GameVersion = lobby.GetData("version"); + + serverInfo.ContentPackageNames.AddRange(lobby.GetData("contentpackage").Split(',')); + serverInfo.ContentPackageHashes.AddRange(lobby.GetData("contentpackagehash").Split(',')); + serverInfo.ContentPackageWorkshopUrls.AddRange(lobby.GetData("contentpackageurl").Split(',')); + + serverInfo.UsingWhiteList = lobby.GetData("usingwhitelist") == "True"; + SelectionMode selectionMode; + if (Enum.TryParse(lobby.GetData("modeselectionmode"), out selectionMode)) serverInfo.ModeSelectionMode = selectionMode; + if (Enum.TryParse(lobby.GetData("subselectionmode"), out selectionMode)) serverInfo.SubSelectionMode = selectionMode; + + serverInfo.AllowSpectating = lobby.GetData("allowspectating") == "True"; + serverInfo.AllowRespawn = lobby.GetData("allowrespawn") == "True"; + serverInfo.VoipEnabled = lobby.GetData("voicechatenabled") == "True"; + if (Enum.TryParse(lobby.GetData("traitors"), out YesNoMaybe traitorsEnabled)) serverInfo.TraitorsEnabled = traitorsEnabled; + + serverInfo.GameStarted = lobby.GetData("gamestarted") == "True"; + serverInfo.GameMode = lobby.GetData("gamemode"); + + if (serverInfo.ContentPackageNames.Count != serverInfo.ContentPackageHashes.Count || + serverInfo.ContentPackageHashes.Count != serverInfo.ContentPackageWorkshopUrls.Count) + { + //invalid contentpackage info + serverInfo.ContentPackageNames.Clear(); + serverInfo.ContentPackageHashes.Clear(); + } + + onServerFound(serverInfo); + //onServerRulesReceived(serverInfo); + } + + onFinished(); + } + private static void UpdateServerQuery(ServerList.Request query, Action onServerFound, Action onServerRulesReceived, bool includeUnresponsive) { IEnumerable servers = includeUnresponsive ? @@ -191,6 +452,9 @@ namespace Barotrauma.Steam { DebugConsole.Log(s.Name + " did not respond to server query."); } + + if (s.Description == "Barotrauma IP Retrieval") { continue; } + var serverInfo = new ServerInfo() { ServerName = s.Name, @@ -203,6 +467,7 @@ namespace Barotrauma.Steam }; serverInfo.PingChecked = true; serverInfo.Ping = s.Ping; + serverInfo.LobbyID = 0; if (responded) { s.FetchRules(); @@ -210,7 +475,7 @@ namespace Barotrauma.Steam s.OnReceivedRules += (bool rulesReceived) => { if (!rulesReceived || s.Rules == null) { return; } - + if (s.Rules.ContainsKey("message")) serverInfo.ServerMessage = s.Rules["message"]; if (s.Rules.ContainsKey("version")) serverInfo.GameVersion = s.Rules["version"]; @@ -270,6 +535,7 @@ namespace Barotrauma.Steam return true; } + private static Auth.Ticket currentTicket = null; public static Auth.Ticket GetAuthSessionTicket() { if (instance == null || !instance.isInitialized) @@ -277,17 +543,39 @@ namespace Barotrauma.Steam return null; } - return instance.client.Auth.GetAuthSessionTicket(); + currentTicket?.Cancel(); + currentTicket = instance.client.Auth.GetAuthSessionTicket(); + return currentTicket; + } + + public static ClientStartAuthSessionResult StartAuthSession(byte[] authTicketData, ulong clientSteamID) + { + if (instance == null || !instance.isInitialized || instance.client == null) return ClientStartAuthSessionResult.ServerNotConnectedToSteam; + + DebugConsole.NewMessage("SteamManager authenticating Steam client " + clientSteamID); + ClientStartAuthSessionResult startResult = instance.client.Auth.StartSession(authTicketData, clientSteamID); + if (startResult != ClientStartAuthSessionResult.OK) + { + DebugConsole.NewMessage("Authentication failed: failed to start auth session (" + startResult.ToString() + ")"); + } + + return startResult; + } + + public static void StopAuthSession(ulong clientSteamID) + { + if (instance == null || !instance.isInitialized || instance.client == null) return; + + DebugConsole.NewMessage("SteamManager ending auth session with Steam client " + clientSteamID); + instance.client.Auth.EndSession(clientSteamID); } #endregion #region Workshop - - public const string WorkshopItemStagingFolder = "NewWorkshopItem"; + public const string WorkshopItemPreviewImageFolder = "Workshop"; public const string PreviewImageName = "PreviewImage.png"; - private const string MetadataFileName = "filelist.xml"; public const string DefaultPreviewImagePath = "Content/DefaultWorkshopPreviewImage.png"; private Sprite defaultPreviewImage; @@ -404,54 +692,45 @@ namespace Barotrauma.Steam item.Subscribe(); item.Download(); } - - /// - /// Creates a new folder, copies the specified files there and creates a metadata file with install instructions. - /// - public static void CreateWorkshopItemStaging(List contentFiles, out Workshop.Editor itemEditor, out ContentPackage contentPackage) + + public static void CreateWorkshopItemStaging(ContentPackage contentPackage, out Workshop.Editor itemEditor) { - var stagingFolder = new DirectoryInfo(WorkshopItemStagingFolder); - if (stagingFolder.Exists) + itemEditor = instance.client.Workshop.CreateItem(Workshop.ItemType.Community); + itemEditor.Visibility = Workshop.Editor.VisibilityType.Public; + itemEditor.WorkshopUploadAppId = AppID; + itemEditor.Folder = Path.GetFullPath(Path.GetDirectoryName(contentPackage.Path)); + + string previewImagePath = Path.GetFullPath(Path.Combine(itemEditor.Folder, PreviewImageName)); + if (!Directory.Exists(itemEditor.Folder)) { Directory.CreateDirectory(itemEditor.Folder); } + if (!File.Exists(previewImagePath)) { - SaveUtil.ClearFolder(stagingFolder.FullName); + File.Copy("Content/DefaultWorkshopPreviewImage.png", previewImagePath); } - else - { - stagingFolder.Create(); - } - Directory.CreateDirectory(Path.Combine(WorkshopItemStagingFolder, "Submarines")); - Directory.CreateDirectory(Path.Combine(WorkshopItemStagingFolder, "Mods")); - Directory.CreateDirectory(Path.Combine(WorkshopItemStagingFolder, "Mods", "ModName")); + } + + /// + /// Creates a new empty content package + /// + public static void CreateWorkshopItemStaging(string itemName, out Workshop.Editor itemEditor, out ContentPackage contentPackage) + { + string dirPath = Path.Combine("Mods", ToolBox.RemoveInvalidFileNameChars(itemName)); + Directory.CreateDirectory("Mods"); + Directory.CreateDirectory(dirPath); itemEditor = instance.client.Workshop.CreateItem(Workshop.ItemType.Community); itemEditor.Visibility = Workshop.Editor.VisibilityType.Public; itemEditor.WorkshopUploadAppId = AppID; - itemEditor.Folder = stagingFolder.FullName; + itemEditor.Folder = dirPath; - string previewImagePath = Path.GetFullPath(Path.Combine(itemEditor.Folder, PreviewImageName)); - File.Copy("Content/DefaultWorkshopPreviewImage.png", previewImagePath); - - //copy content files to the staging folder - List copiedFilePaths = new List(); - foreach (ContentFile file in contentFiles) + string previewImagePath = Path.GetFullPath(Path.Combine(dirPath, PreviewImageName)); + if (!File.Exists(previewImagePath)) { - string relativePath = UpdaterUtil.GetRelativePath(Path.GetFullPath(file.Path), Environment.CurrentDirectory); - string destinationPath = Path.Combine(stagingFolder.FullName, relativePath); - //make sure the directory exists - Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)); - File.Copy(file.Path, destinationPath); - copiedFilePaths.Add(destinationPath); + File.Copy("Content/DefaultWorkshopPreviewImage.png", previewImagePath); } - System.Diagnostics.Debug.Assert(copiedFilePaths.Count == contentFiles.Count); - + //create a new content package and include the copied files in it - contentPackage = ContentPackage.CreatePackage("ContentPackage", Path.Combine(itemEditor.Folder, MetadataFileName), false); - for (int i = 0; i < copiedFilePaths.Count; i++) - { - contentPackage.AddFile(copiedFilePaths[i], contentFiles[i].Type); - } - - contentPackage.Save(Path.Combine(stagingFolder.FullName, MetadataFileName)); + contentPackage = ContentPackage.CreatePackage(itemName, Path.Combine(dirPath, MetadataFileName), false); + contentPackage.Save(Path.Combine(dirPath, MetadataFileName)); } /// @@ -467,23 +746,67 @@ namespace Barotrauma.Steam return; } - var stagingFolder = new DirectoryInfo(WorkshopItemStagingFolder); - if (stagingFolder.Exists) - { - SaveUtil.ClearFolder(stagingFolder.FullName); - } - else - { - stagingFolder.Create(); - } - itemEditor = instance.client.Workshop.EditItem(existingItem.Id); itemEditor.Visibility = Workshop.Editor.VisibilityType.Public; itemEditor.Title = existingItem.Title; itemEditor.Tags = existingItem.Tags.ToList(); itemEditor.Description = existingItem.Description; itemEditor.WorkshopUploadAppId = AppID; - itemEditor.Folder = stagingFolder.FullName; + + if (!CheckWorkshopItemEnabled(existingItem, checkContentFiles: false)) + { + if (!EnableWorkShopItem(existingItem, false, out string errorMsg)) + { + DebugConsole.ThrowError(errorMsg); + new GUIMessageBox( + TextManager.Get("Error"), + TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { existingItem.Title, errorMsg })); + itemEditor = null; + contentPackage = null; + return; + } + } + + ContentPackage tempContentPackage = new ContentPackage(Path.Combine(existingItem.Directory.FullName, MetadataFileName)); + string installedContentPackagePath = Path.GetFullPath(GetWorkshopItemContentPackagePath(tempContentPackage)); + contentPackage = ContentPackage.List.Find(cp => Path.GetFullPath(cp.Path) == installedContentPackagePath); + if (tempContentPackage.GameVersion > new Version(0, 9, 1, 0)) + { + itemEditor.Folder = Path.GetDirectoryName(installedContentPackagePath); + } + else //legacy support + { + try + { + tempContentPackage.GameVersion = GameMain.Version; + string newPath = GetWorkshopItemContentPackagePath(tempContentPackage); + string newDir = Path.GetDirectoryName(newPath); + contentPackage.Path = newPath; + itemEditor.Folder = newDir; + if (!Directory.Exists(newDir)) { Directory.CreateDirectory(newDir); } + File.Move(installedContentPackagePath, newPath); + //move all files inside the Mods folder + foreach (ContentFile cf in contentPackage.Files) + { + string relativePath = UpdaterUtil.GetRelativePath(Path.GetFullPath(cf.Path), Path.GetFullPath(newDir)); + if (relativePath.StartsWith("..")) + { + string destinationPath = Path.Combine(newDir, cf.Path); + if (!Directory.Exists(Path.GetDirectoryName(destinationPath))) { Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)); } + File.Move(cf.Path, destinationPath); + cf.Path = destinationPath; + } + } + contentPackage.Save(contentPackage.Path); + } + catch (Exception e) + { + string errorMsg = TextManager.GetWithVariable("WorkshopErrorOnEnable", "[itemname]", TextManager.EnsureUTF8(existingItem.Title)); + new GUIMessageBox(TextManager.Get("Error"), errorMsg); + DebugConsole.ThrowError(errorMsg, e); + return; + } + } string previewImagePath = Path.GetFullPath(Path.Combine(itemEditor.Folder, PreviewImageName)); itemEditor.PreviewImage = previewImagePath; @@ -512,46 +835,6 @@ namespace Barotrauma.Steam GameAnalyticsManager.AddErrorEventOnce("SteamManager.CreateWorkshopItemStaging:WriteAllBytesFailed" + previewImagePath, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg + "\n" + e.Message); } - - ContentPackage tempContentPackage = new ContentPackage(Path.Combine(existingItem.Directory.FullName, MetadataFileName)); - //item already installed, copy it from the game folder - if (existingItem != null && CheckWorkshopItemEnabled(existingItem, checkContentFiles: false)) - { - string installedItemPath = GetWorkshopItemContentPackagePath(tempContentPackage); - if (File.Exists(installedItemPath)) - { - tempContentPackage = new ContentPackage(installedItemPath); - } - } - if (File.Exists(tempContentPackage.Path)) - { - string newContentPackagePath = Path.Combine(WorkshopItemStagingFolder, MetadataFileName); - File.Copy(tempContentPackage.Path, newContentPackagePath, overwrite: true); - contentPackage = new ContentPackage(newContentPackagePath); - foreach (ContentFile contentFile in tempContentPackage.Files) - { - string sourceFile; - if (contentFile.Type == ContentType.Submarine && File.Exists(contentFile.Path)) - { - sourceFile = contentFile.Path; - } - else - { - sourceFile = Path.Combine(existingItem.Directory.FullName, contentFile.Path); - } - if (!File.Exists(sourceFile)) { continue; } - //make sure the destination directory exists - string destinationPath = Path.Combine(WorkshopItemStagingFolder, contentFile.Path); - Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)); - File.Copy(sourceFile, destinationPath, overwrite: true); - contentPackage.AddFile(contentFile.Path, contentFile.Type); - } - } - else - { - contentPackage = ContentPackage.CreatePackage(existingItem.Title, Path.Combine(WorkshopItemStagingFolder, MetadataFileName), false); - contentPackage.Save(contentPackage.Path); - } } public static void StartPublishItem(ContentPackage contentPackage, Workshop.Editor item) @@ -568,12 +851,11 @@ namespace Barotrauma.Steam DebugConsole.ThrowError("Cannot publish workshop item \"" + item.Title + "\" - folder not set."); return; } - - contentPackage.Name = item.Title; + contentPackage.GameVersion = GameMain.Version; - contentPackage.Save(Path.Combine(WorkshopItemStagingFolder, MetadataFileName)); + contentPackage.Save(contentPackage.Path); - if (File.Exists(PreviewImageName)) File.Delete(PreviewImageName); + if (File.Exists(PreviewImageName)) { File.Delete(PreviewImageName); } //move the preview image out of the staging folder, it does not need to be included in the folder sent to Workshop File.Move(Path.GetFullPath(Path.Combine(item.Folder, PreviewImageName)), PreviewImageName); item.PreviewImage = Path.GetFullPath(PreviewImageName); @@ -604,18 +886,7 @@ namespace Barotrauma.Steam { DebugConsole.NewMessage("Publishing workshop item " + item.Title + " failed. " + item.Error, Microsoft.Xna.Framework.Color.Red); } - - SaveUtil.ClearFolder(WorkshopItemStagingFolder); - File.Delete(PreviewImageName); - try - { - Directory.Delete(WorkshopItemStagingFolder); - } - catch (Exception e) - { - DebugConsole.ThrowError("Failed to delete Workshop item staging folder.", e); - } - + yield return CoroutineStatus.Success; } @@ -632,6 +903,14 @@ namespace Barotrauma.Steam } string metaDataFilePath = Path.Combine(item.Directory.FullName, MetadataFileName); + + if (!File.Exists(metaDataFilePath)) + { + errorMsg = TextManager.GetWithVariable("WorkshopErrorInstallRequiredToEnable", "[itemname]", item.Title); + DebugConsole.ThrowError(errorMsg); + return false; + } + ContentPackage contentPackage = new ContentPackage(metaDataFilePath); string newContentPackagePath = GetWorkshopItemContentPackagePath(contentPackage); @@ -644,11 +923,52 @@ namespace Barotrauma.Steam if (contentPackage.CorePackage && !contentPackage.ContainsRequiredCorePackageFiles(out List missingContentTypes)) { - errorMsg = TextManager.GetWithVariables("ContentPackageMissingCoreFiles", new string[2] { "[packagename]", "[missingfiletypes]" }, + errorMsg = TextManager.GetWithVariables("ContentPackageMissingCoreFiles", new string[2] { "[packagename]", "[missingfiletypes]" }, new string[2] { contentPackage.Name, string.Join(", ", missingContentTypes) }, new bool[2] { false, true }); return false; } + if (contentPackage.GameVersion > new Version(0, 9, 1, 0)) + { + SaveUtil.CopyFolder(item.Directory.FullName, Path.GetDirectoryName(GetWorkshopItemContentPackagePath(contentPackage)), copySubDirs: true, overwriteExisting: true); + } + else //legacy support + { + EnableWorkShopItemLegacy(item, contentPackage, newContentPackagePath, metaDataFilePath, allowFileOverwrite, out errorMsg); + } + + var newPackage = new ContentPackage(contentPackage.Path, newContentPackagePath) + { + SteamWorkshopUrl = item.Url, + InstallTime = item.Modified > item.Created ? item.Modified : item.Created + }; + if (!Directory.Exists(Path.GetDirectoryName(newContentPackagePath))) + { + Directory.CreateDirectory(Path.GetDirectoryName(newContentPackagePath)); + } + newPackage.Save(newContentPackagePath); + ContentPackage.List.Add(newPackage); + if (newPackage.CorePackage) + { + //if enabling a core package, disable all other core packages + GameMain.Config.SelectedContentPackages.RemoveWhere(cp => cp.CorePackage); + } + GameMain.Config.SelectedContentPackages.Add(newPackage); + GameMain.Config.SaveNewPlayerConfig(); + + if (newPackage.Files.Any(f => f.Type == ContentType.Submarine)) + { + Submarine.RefreshSavedSubs(); + } + + errorMsg = ""; + return true; + } + + private static bool EnableWorkShopItemLegacy(Workshop.Item item, ContentPackage contentPackage, string newContentPackagePath, string metaDataFilePath, bool allowFileOverwrite, out string errorMsg) + { + errorMsg = ""; + var allPackageFiles = Directory.GetFiles(item.Directory.FullName, "*", SearchOption.AllDirectories); List nonContentFiles = new List(); foreach (string file in allPackageFiles) @@ -748,27 +1068,6 @@ namespace Barotrauma.Steam return false; } - var newPackage = new ContentPackage(contentPackage.Path, newContentPackagePath) - { - SteamWorkshopUrl = item.Url, - InstallTime = item.Modified > item.Created ? item.Modified : item.Created - }; - newPackage.Save(newContentPackagePath); - ContentPackage.List.Add(newPackage); - if (newPackage.CorePackage) - { - //if enabling a core package, disable all other core packages - GameMain.Config.SelectedContentPackages.RemoveWhere(cp => cp.CorePackage); - } - GameMain.Config.SelectedContentPackages.Add(newPackage); - GameMain.Config.SaveNewPlayerConfig(); - - if (newPackage.Files.Any(f => f.Type == ContentType.Submarine)) - { - Submarine.RefreshSavedSubs(); - } - - errorMsg = ""; return true; } @@ -894,7 +1193,14 @@ namespace Barotrauma.Steam public static bool CheckWorkshopItemEnabled(Workshop.Item item, bool checkContentFiles = true) { - if (!item.Installed) return false; + if (!item.Installed) { return false; } + + if (!Directory.Exists(item.Directory.FullName)) + { + DebugConsole.ThrowError("Workshop item \"" + item.Title + "\" has been installed but the install directory cannot be found. Attempting to redownload..."); + item.ForceDownload(); + return false; + } string metaDataPath = Path.Combine(item.Directory.FullName, MetadataFileName); if (!File.Exists(metaDataPath)) @@ -1004,10 +1310,11 @@ namespace Barotrauma.Steam public static string GetWorkshopItemContentPackagePath(ContentPackage contentPackage) { - string fileName = contentPackage.Name + ".xml"; - string invalidChars = new string(Path.GetInvalidFileNameChars()) + new string(Path.GetInvalidPathChars()); - foreach (char c in invalidChars) fileName = fileName.Replace(c.ToString(), ""); - return Path.Combine("Data", "ContentPackages", fileName); + string fileName = contentPackage.Name; + string invalidChars = ToolBox.RemoveInvalidFileNameChars(fileName); + return contentPackage.GameVersion > new Version(0, 9, 1, 0) ? + Path.Combine("Mods", fileName, MetadataFileName) : + Path.Combine("Data", "ContentPackages", fileName + ".xml"); //legacy support } #endregion diff --git a/Barotrauma/BarotraumaClient/Source/Networking/Voip/VoipCapture.cs b/Barotrauma/BarotraumaClient/Source/Networking/Voip/VoipCapture.cs index f0bcb18f2..a2846cd78 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/Voip/VoipCapture.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/Voip/VoipCapture.cs @@ -1,5 +1,4 @@ -using Lidgren.Network; -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using OpenAL; using System; using System.Linq; @@ -97,7 +96,8 @@ namespace Barotrauma.Networking UserData = "capturedevicenotfound" }; } - GameAnalyticsManager.AddErrorEventOnce("Alc.CaptureDeviceOpen(" + deviceName + ") failed", GameAnalyticsSDK.Net.EGAErrorSeverity.Error,"Error code: "+errorCode); + GameAnalyticsManager.AddErrorEventOnce("Alc.CaptureDeviceOpenFailed", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + "Alc.CaptureDeviceOpen(" + deviceName + ") failed. Error code: " + errorCode); GameMain.Config.VoiceSetting = GameSettings.VoiceMode.Disabled; Instance?.Dispose(); Instance = null; @@ -234,7 +234,7 @@ namespace Barotrauma.Networking } } - public override void Write(NetBuffer msg) + public override void Write(IWriteMessage msg) { lock (buffers) { diff --git a/Barotrauma/BarotraumaClient/Source/Networking/Voip/VoipClient.cs b/Barotrauma/BarotraumaClient/Source/Networking/Voip/VoipClient.cs index 1ad61e4cd..74fe4b3fa 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/Voip/VoipClient.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/Voip/VoipClient.cs @@ -1,5 +1,4 @@ using Barotrauma.Sounds; -using Lidgren.Network; using System; using System.Collections.Generic; using System.Linq; @@ -13,13 +12,13 @@ namespace Barotrauma.Networking class VoipClient : IDisposable { private GameClient gameClient; - private NetClient netClient; + private ClientPeer netClient; private DateTime lastSendTime; private List queues; private UInt16 storedBufferID = 0; - public VoipClient(GameClient gClient,NetClient nClient) + public VoipClient(GameClient gClient,ClientPeer nClient) { gameClient = gClient; netClient = nClient; @@ -59,19 +58,19 @@ namespace Barotrauma.Networking if (DateTime.Now >= lastSendTime + VoipConfig.SEND_INTERVAL) { - NetOutgoingMessage msg = netClient.CreateMessage(); + IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ClientPacketHeader.VOICE); msg.Write((byte)VoipCapture.Instance.QueueID); VoipCapture.Instance.Write(msg); - netClient.SendMessage(msg, NetDeliveryMethod.Unreliable); + netClient.Send(msg, DeliveryMethod.Unreliable); lastSendTime = DateTime.Now; } } - public void Read(NetBuffer msg) + public void Read(IReadMessage msg) { byte queueId = msg.ReadByte(); VoipQueue queue = queues.Find(q => q.QueueID == queueId); @@ -95,7 +94,7 @@ namespace Barotrauma.Networking client.VoipSound = new VoipSound(GameMain.SoundManager, client.VoipQueue); } - if (client.Character != null && !client.Character.IsDead && !client.Character.IsDead && client.Character.SpeechImpediment <= 100.0f) + if (client.Character != null && !client.Character.IsDead && client.Character.SpeechImpediment <= 100.0f) { var messageType = ChatMessage.CanUseRadio(client.Character, out WifiComponent radio) ? ChatMessageType.Radio : ChatMessageType.Default; client.Character.ShowSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]); @@ -114,8 +113,27 @@ namespace Barotrauma.Networking client.VoipSound.UseMuffleFilter = SoundPlayer.ShouldMuffleSound(Character.Controlled, client.Character.WorldPosition, ChatMessage.SpeakRange, client.Character.CurrentHull); } } - GameMain.NetLobbyScreen.SetPlayerSpeaking(client); + + GameMain.NetLobbyScreen?.SetPlayerSpeaking(client); GameMain.GameSession?.CrewManager?.SetClientSpeaking(client); + + if (client.VoipSound.CurrentAmplitude > 0.1f) //TODO: might need to tweak + { + if (client.Character != null) + { + Vector3 clientPos = new Vector3(client.Character.WorldPosition.X, client.Character.WorldPosition.Y, 0.0f); + Vector3 listenerPos = GameMain.SoundManager.ListenerPosition; + float attenuationDist = client.VoipSound.Near * 1.125f; + if (Vector3.DistanceSquared(clientPos, listenerPos) < attenuationDist * attenuationDist) + { + GameMain.SoundManager.VoipAttenuatedGain = 0.5f; + } + } + else + { + GameMain.SoundManager.VoipAttenuatedGain = 0.5f; + } + } } } diff --git a/Barotrauma/BarotraumaClient/Source/Networking/Voting.cs b/Barotrauma/BarotraumaClient/Source/Networking/Voting.cs index 4faa534c2..3be8cc760 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/Voting.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/Voting.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using Lidgren.Network; using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Linq; @@ -105,7 +104,7 @@ namespace Barotrauma } } - public void ClientWrite(NetBuffer msg, VoteType voteType, object data) + public void ClientWrite(IWriteMessage msg, VoteType voteType, object data) { msg.Write((byte)voteType); @@ -141,7 +140,7 @@ namespace Barotrauma msg.WritePadBits(); } - public void ClientRead(NetBuffer inc) + public void ClientRead(IReadMessage inc) { AllowSubVoting = inc.ReadBoolean(); if (allowSubVoting) diff --git a/Barotrauma/BarotraumaClient/Source/Networking/WhiteList.cs b/Barotrauma/BarotraumaClient/Source/Networking/WhiteList.cs index 58427e1e1..9e65cdaad 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/WhiteList.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/WhiteList.cs @@ -1,5 +1,4 @@ -using Lidgren.Network; -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; @@ -177,7 +176,7 @@ namespace Barotrauma.Networking return true; } - public void ClientAdminRead(NetBuffer incMsg) + public void ClientAdminRead(IReadMessage incMsg) { bool hasPermission = incMsg.ReadBoolean(); if (!hasPermission) @@ -192,8 +191,8 @@ namespace Barotrauma.Networking incMsg.ReadPadBits(); whitelistedPlayers.Clear(); - Int32 bannedPlayerCount = incMsg.ReadVariableInt32(); - for (int i = 0; i < bannedPlayerCount; i++) + UInt32 bannedPlayerCount = incMsg.ReadVariableUInt32(); + for (int i = 0; i < (int)bannedPlayerCount; i++) { string name = incMsg.ReadString(); UInt16 uniqueIdentifier = incMsg.ReadUInt16(); @@ -216,7 +215,7 @@ namespace Barotrauma.Networking } } - public void ClientAdminWrite(NetBuffer outMsg) + public void ClientAdminWrite(IWriteMessage outMsg) { outMsg.Write(localEnabled); outMsg.WritePadBits(); diff --git a/Barotrauma/BarotraumaClient/Source/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaClient/Source/Physics/PhysicsBody.cs index d79fc2499..30af3aa13 100644 --- a/Barotrauma/BarotraumaClient/Source/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaClient/Source/Physics/PhysicsBody.cs @@ -144,7 +144,7 @@ namespace Barotrauma 1.0f / bodyShapeTextureScale, SpriteEffects.None, 0.0f); } - public PosInfo ClientRead(ServerNetObject type, NetBuffer msg, float sendingTime, string parentDebugName) + public PosInfo ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime, string parentDebugName) { float MaxVel = NetConfig.MaxPhysicsBodyVelocity; float MaxAngularVel = NetConfig.MaxPhysicsBodyAngularVelocity; @@ -156,8 +156,8 @@ namespace Barotrauma float? newAngularVelocity = null; newPosition = new Vector2( - msg.ReadFloat(), - msg.ReadFloat()); + msg.ReadSingle(), + msg.ReadSingle()); awake = msg.ReadBoolean(); bool fixedRotation = msg.ReadBoolean(); diff --git a/Barotrauma/BarotraumaClient/Source/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/Source/Screens/CampaignUI.cs index 7f02549df..532901020 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/CampaignUI.cs @@ -599,7 +599,8 @@ namespace Barotrauma if (!tb.Selected) { return false; } RefreshMissionTab(tb.UserData as Mission); Campaign.Map.OnMissionSelected?.Invoke(connection, mission); - if (GameMain.Client != null && GameMain.Client.HasPermission(Networking.ClientPermissions.ManageCampaign)) + if ((Campaign is MultiPlayerCampaign multiPlayerCampaign) && !multiPlayerCampaign.SuppressStateSending && + GameMain.Client != null && GameMain.Client.HasPermission(Networking.ClientPermissions.ManageCampaign)) { GameMain.Client?.SendCampaignState(); } diff --git a/Barotrauma/BarotraumaClient/Source/Screens/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/Source/Screens/CharacterEditorScreen.cs index 9ae6411bd..35824c844 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/CharacterEditorScreen.cs @@ -97,7 +97,7 @@ namespace Barotrauma SoundPlayer.OverrideMusicType = "none"; SoundPlayer.OverrideMusicDuration = null; - GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", 0.0f); + GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", 0.0f, 0); GUI.ForceMouseOn(null); CalculateSpritesheetPosition(); @@ -195,7 +195,7 @@ namespace Barotrauma base.Deselect(); SoundPlayer.OverrideMusicType = null; - GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", GameMain.Config.SoundVolume); + GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", GameMain.Config.SoundVolume, 0); GUI.ForceMouseOn(null); if (isEndlessRunner) @@ -1467,6 +1467,7 @@ namespace Barotrauma private void ShowWearables() { + if (character.Inventory == null) { return; } foreach (var item in character.Inventory.Items) { if (item == null) { continue; } @@ -1478,7 +1479,7 @@ namespace Barotrauma private void HideWearables() { - character.Inventory.Items.ForEachMod(i => i?.Unequip(character)); + character.Inventory?.Items.ForEachMod(i => i?.Unequip(character)); } #endregion diff --git a/Barotrauma/BarotraumaClient/Source/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/Source/Screens/MainMenuScreen.cs index 62b74fa4b..01651d775 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/MainMenuScreen.cs @@ -23,8 +23,8 @@ namespace Barotrauma private CampaignSetupUI campaignSetupUI; - private GUITextBox serverNameBox, portBox, queryPortBox, passwordBox, maxPlayersBox; - private GUITickBox isPublicBox, useUpnpBox; + private GUITextBox serverNameBox, /*portBox, queryPortBox,*/ passwordBox, maxPlayersBox; + private GUITickBox isPublicBox/*, useUpnpBox*/; private GUIButton joinServerButton, hostServerButton, steamWorkshopButton; @@ -203,10 +203,6 @@ namespace Barotrauma UserData = Tab.SteamWorkshop, OnClicked = SelectTab }; - -/*#if OSX && !DEBUG - steamWorkshopButton.Text += " (Not yet available on MacOS)"; -#endif*/ } new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), customizeList.RectTransform), TextManager.Get("SubEditorButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") @@ -256,7 +252,7 @@ namespace Barotrauma UserData = Tab.Settings, OnClicked = SelectTab }; - //TODO: translate + new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), optionList.RectTransform), TextManager.Get("CreditsButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { ForceUpperCase = true, @@ -323,7 +319,7 @@ namespace Barotrauma StartNewGame = StartGame }; - var hostServerScale = new Vector2(0.7f, 1.0f); + var hostServerScale = new Vector2(0.7f, 0.6f); menuTabs[(int)Tab.HostServer] = new GUIFrame(new RectTransform( Vector2.Multiply(relativeSize, hostServerScale), GUI.Canvas, anchor, pivot, minSize.Multiply(hostServerScale), maxSize.Multiply(hostServerScale)) { RelativeOffset = relativeSpacing }); @@ -703,7 +699,7 @@ namespace Barotrauma return false; } - if (!int.TryParse(portBox.Text, out int port) || port < 0 || port > 65535) + /*if (!int.TryParse(portBox.Text, out int port) || port < 0 || port > 65535) { portBox.Text = NetConfig.DefaultPort.ToString(); portBox.Flash(); @@ -720,7 +716,7 @@ namespace Barotrauma portBox.Flash(); return false; } - } + }*/ GameMain.NetLobbyScreen = new NetLobbyScreen(); try @@ -732,16 +728,22 @@ namespace Barotrauma exeName = "DedicatedServer.exe"; } - int ownerKey = Math.Max(CryptoRandom.Instance.Next(), 1); - string arguments = "-name \"" + name.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"" + - " -port " + port.ToString() + - " -queryport " + queryPort.ToString() + " -public " + isPublicBox.Selected.ToString() + " -password \"" + passwordBox.Text.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"" + - " -upnp " + useUpnpBox.Selected + - " -maxplayers " + maxPlayersBox.Text + - " -ownerkey " + ownerKey.ToString(); + " -maxplayers " + maxPlayersBox.Text; + + int ownerKey = 0; + + if (Steam.SteamManager.GetSteamID()!=0) + { + arguments += " -steamid " + Steam.SteamManager.GetSteamID(); + } + else + { + ownerKey = Math.Max(CryptoRandom.Instance.Next(), 1); + arguments += " -ownerkey " + ownerKey; + } string filename = exeName; #if LINUX || OSX @@ -752,13 +754,15 @@ namespace Barotrauma FileName = filename, Arguments = arguments, #if !DEBUG + CreateNoWindow = true, + UseShellExecute = false, WindowStyle = ProcessWindowStyle.Hidden #endif }; GameMain.ServerChildProcess = Process.Start(processInfo); Thread.Sleep(1000); //wait until the server is ready before connecting - GameMain.Client = new GameClient(name, "127.0.0.1:" + port.ToString(), name, ownerKey); + GameMain.Client = new GameClient(name, System.Net.IPAddress.Loopback.ToString(), Steam.SteamManager.GetSteamID(), name, ownerKey, true); } catch (Exception e) { @@ -798,7 +802,10 @@ namespace Barotrauma #else joinServerButton.Enabled = true; hostServerButton.Enabled = true; - steamWorkshopButton.Enabled = true; + if (Steam.SteamManager.USE_STEAM) + { + steamWorkshopButton.Enabled = true; + } #endif } @@ -976,6 +983,7 @@ namespace Barotrauma OverflowClip = true }; + /* TODO: allow lidgren servers from client? label = new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), TextManager.Get("ServerPort"), textAlignment: textAlignment); portBox = new GUITextBox(new RectTransform(textFieldSize, label.RectTransform, Anchor.CenterRight), textAlignment: textAlignment) { @@ -991,7 +999,7 @@ namespace Barotrauma Text = queryPort.ToString(), ToolTip = TextManager.Get("ServerQueryPortToolTip") }; - } + }*/ var maxPlayersLabel = new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), TextManager.Get("MaxPlayers"), textAlignment: textAlignment); var buttonContainer = new GUILayoutGroup(new RectTransform(textFieldSize, maxPlayersLabel.RectTransform, Anchor.CenterRight), isHorizontal: true) @@ -1027,10 +1035,11 @@ namespace Barotrauma ToolTip = TextManager.Get("PublicServerToolTip") }; + /* TODO: remove UPnP altogether? useUpnpBox = new GUITickBox(new RectTransform(tickBoxSize, parent.RectTransform), TextManager.Get("AttemptUPnP")) { ToolTip = TextManager.Get("AttemptUPnPToolTip") - }; + };*/ new GUIButton(new RectTransform(new Vector2(0.4f, 0.1f), menuTabs[(int)Tab.HostServer].RectTransform, Anchor.BottomRight) { diff --git a/Barotrauma/BarotraumaClient/Source/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/Source/Screens/NetLobbyScreen.cs index 038ee390d..e54e31b3b 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/NetLobbyScreen.cs @@ -1383,7 +1383,7 @@ namespace Barotrauma OnClicked = (btn, userdata) => { if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) ClosePlayerFrame(btn, userdata); return true; } }; - Vector2 frameSize = GameMain.Client.HasPermission(ClientPermissions.ManagePermissions) ? new Vector2(.24f, .34f) : new Vector2(.24f, .14f); + Vector2 frameSize = GameMain.Client.HasPermission(ClientPermissions.ManagePermissions) ? new Vector2(.24f, .44f) : new Vector2(.24f, .24f); var playerFrameInner = new GUIFrame(new RectTransform(frameSize, playerFrame.RectTransform, Anchor.Center)); var paddedPlayerFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.85f), playerFrameInner.RectTransform, Anchor.Center)) @@ -1530,13 +1530,29 @@ namespace Barotrauma } var buttonAreaUpper = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), paddedPlayerFrame.RectTransform), isHorizontal: true); + var buttonAreaMiddle = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), paddedPlayerFrame.RectTransform), isHorizontal: true); var buttonAreaLower = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), paddedPlayerFrame.RectTransform), isHorizontal: true); - + + if (selectedClient.SteamID != 0 && Steam.SteamManager.IsInitialized) + { + var viewSteamProfileButton = new GUIButton(new RectTransform(new Vector2(0.8f, 1.0f), buttonAreaUpper.RectTransform, Anchor.TopCenter), + TextManager.Get("ViewSteamProfile")) + { + UserData = selectedClient + }; + + viewSteamProfileButton.OnClicked = (bt, userdata) => + { + Steam.SteamManager.Instance.Overlay.OpenUrl("https://steamcommunity.com/profiles/" + selectedClient.SteamID.ToString()); + return true; + }; + } + if (!myClient) { if (GameMain.Client.HasPermission(ClientPermissions.Ban)) { - var banButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonAreaUpper.RectTransform), + var banButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonAreaMiddle.RectTransform), TextManager.Get("Ban")) { UserData = selectedClient @@ -1544,7 +1560,7 @@ namespace Barotrauma banButton.OnClicked = (bt, userdata) => { BanPlayer(selectedClient); return true; }; banButton.OnClicked += ClosePlayerFrame; - var rangebanButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonAreaUpper.RectTransform), + var rangebanButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonAreaMiddle.RectTransform), TextManager.Get("BanRange")) { UserData = selectedClient @@ -1578,7 +1594,7 @@ namespace Barotrauma kickButton.OnClicked += ClosePlayerFrame; } - new GUITickBox(new RectTransform(new Vector2(0.25f, 1.0f), buttonAreaUpper.RectTransform, Anchor.TopRight), + new GUITickBox(new RectTransform(new Vector2(0.25f, 1.0f), buttonAreaMiddle.RectTransform, Anchor.TopRight), TextManager.Get("Mute")) { IgnoreLayoutGroups = true, diff --git a/Barotrauma/BarotraumaClient/Source/Screens/ServerListScreen.cs b/Barotrauma/BarotraumaClient/Source/Screens/ServerListScreen.cs index 342a73ee0..679665b32 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/ServerListScreen.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; +using System.Text; using System.Threading; namespace Barotrauma @@ -45,7 +46,7 @@ namespace Barotrauma private readonly GUITickBox filterEmpty; private string sortedBy; - + private readonly GUIButton serverPreviewToggleButton; //a timer for preventing the client from spamming the refresh button faster than AllowedRefreshInterval @@ -332,7 +333,14 @@ namespace Barotrauma { if (ipBox.UserData is ServerInfo selectedServer) { - JoinServer(selectedServer.IP + ":" + selectedServer.Port, selectedServer.ServerName); + if (selectedServer.LobbyID == 0) + { + JoinServer(selectedServer.IP + ":" + selectedServer.Port, selectedServer.ServerName); + } + else + { + Steam.SteamManager.JoinLobby(selectedServer.LobbyID, true); + } } else if (!string.IsNullOrEmpty(ipBox.Text)) { @@ -474,6 +482,15 @@ namespace Barotrauma RefreshServers(); } + public override void Deselect() + { + base.Deselect(); + if (SteamManager.IsInitialized && SteamManager.Instance.LobbyList != null) + { + SteamManager.Instance.LobbyList.OnLobbiesUpdated = null; + } + } + private void FilterServers() { serverList.Content.RemoveChild(serverList.Content.FindChild("noresults")); @@ -671,16 +688,24 @@ namespace Barotrauma private void AddToServerList(ServerInfo serverInfo) { - var serverFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.06f), serverList.Content.RectTransform) { MinSize = new Point(0, 35) }, + var serverFrame = serverList.Content.FindChild(d => (d.UserData is ServerInfo info) && + (info.LobbyID==serverInfo.LobbyID && info.IP==serverInfo.IP && info.Port==serverInfo.Port)); + + if (serverFrame == null) + { + serverFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.06f), serverList.Content.RectTransform) { MinSize = new Point(0, 35) }, style: "InnerFrame", color: Color.White * 0.5f) - { - UserData = serverInfo - }; - new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 1.0f), serverFrame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - Stretch = true, - //RelativeSpacing = 0.02f - }; + { + UserData = serverInfo + }; + new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 1.0f), serverFrame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true, + //RelativeSpacing = 0.02f + }; + } + serverFrame.UserData = serverInfo; + UpdateServerInfo(serverInfo); SortList(sortedBy, toggle: false); @@ -901,13 +926,17 @@ namespace Barotrauma return true; } - private IEnumerable ConnectToServer(string ip, string serverName) + private IEnumerable ConnectToServer(string endpoint, string serverName) { + string serverIP = null; + UInt64 serverSteamID = SteamManager.SteamIDStringToUInt64(endpoint); + if (serverSteamID == 0) { serverIP = endpoint; } + #if !DEBUG try { #endif - GameMain.Client = new GameClient(clientNameBox.Text, ip, serverName); + GameMain.Client = new GameClient(clientNameBox.Text, serverIP, serverSteamID, serverName); #if !DEBUG } catch (Exception e) @@ -961,7 +990,7 @@ namespace Barotrauma public void PingServer(ServerInfo serverInfo, int timeOut) { - if (serverInfo?.IP == null) + if (string.IsNullOrWhiteSpace(serverInfo?.IP)) { serverInfo.PingChecked = true; serverInfo.Ping = -1; @@ -969,7 +998,8 @@ namespace Barotrauma } long rtt = -1; - IPAddress address = IPAddress.Parse(serverInfo.IP); + IPAddress address = null; + IPAddress.TryParse(serverInfo.IP, out address); if (address != null) { //don't attempt to ping if the address is IPv6 and it's not supported diff --git a/Barotrauma/BarotraumaClient/Source/Screens/SteamWorkshopScreen.cs b/Barotrauma/BarotraumaClient/Source/Screens/SteamWorkshopScreen.cs index 22e6c1206..40b685ec1 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/SteamWorkshopScreen.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/SteamWorkshopScreen.cs @@ -159,6 +159,7 @@ namespace Barotrauma OnSelected = (component, userdata) => { if (GUI.MouseOn is GUIButton || GUI.MouseOn?.Parent is GUIButton) { return false; } + if (GUI.MouseOn is GUITickBox || GUI.MouseOn?.Parent is GUITickBox) { return false; } myItemList.Deselect(); if (userdata is Facepunch.Steamworks.Workshop.Item item) { @@ -190,16 +191,6 @@ namespace Barotrauma } }; - new GUIButton(new RectTransform(new Vector2(0.5f, 0.05f), leftColumn.RectTransform), TextManager.Get("CreateWorkshopItem")) - { - OnClicked = (btn, userData) => - { - CreateWorkshopItem(); - ShowCreateItemFrame(); - return true; - } - }; - createItemFrame = new GUIFrame(new RectTransform(new Vector2(0.58f, 1.0f), tabs[(int)Tab.Publish].RectTransform, Anchor.TopRight), style: "InnerFrame"); buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.08f), container.RectTransform), childAnchor: Anchor.CenterLeft); @@ -269,7 +260,11 @@ namespace Barotrauma }); SteamManager.GetPopularWorkshopItems((items) => { OnItemsReceived(items, topItemList); }, 20); SteamManager.GetPublishedWorkshopItems((items) => { OnItemsReceived(items, publishedItemList); }); + RefreshMyItemList(); + } + private void RefreshMyItemList() + { myItemList.ClearChildren(); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), myItemList.Content.RectTransform), TextManager.Get("WorkshopLabelSubmarines"), textAlignment: Alignment.Center, font: GUI.LargeFont) { @@ -278,12 +273,28 @@ namespace Barotrauma foreach (Submarine sub in Submarine.SavedSubmarines) { if (sub.HasTag(SubmarineTag.HideInMenus)) { continue; } - //ignore subs that are part of some content package string subPath = Path.GetFullPath(sub.FilePath); - if (ContentPackage.List.Any(cp => cp.Files.Any(f => f.Type == ContentType.Submarine && Path.GetFullPath(f.Path) == subPath))) + + //ignore subs that are part of the vanilla content package + if (GameMain.VanillaContent != null && + GameMain.VanillaContent.GetFilesOfType(ContentType.Submarine).Any(s => Path.GetFullPath(s) == subPath)) { continue; } + //ignore subs that are part of a workshop content package + if (ContentPackage.List.Any(cp => !string.IsNullOrEmpty(cp.SteamWorkshopUrl) && + cp.Files.Any(f => f.Type == ContentType.Submarine && Path.GetFullPath(f.Path) == subPath))) + { + continue; + } + //ignore subs that are defined in a content package with more files than just the sub + //(these will be listed in the "content packages" section) + if (ContentPackage.List.Any(cp => cp.Files.Count > 1 && + cp.Files.Any(f => f.Type == ContentType.Submarine && Path.GetFullPath(f.Path) == subPath))) + { + continue; + } + CreateMyItemFrame(sub, myItemList); } @@ -294,6 +305,8 @@ namespace Barotrauma foreach (ContentPackage contentPackage in ContentPackage.List) { if (!string.IsNullOrEmpty(contentPackage.SteamWorkshopUrl) || contentPackage.HideInWorkshopMenu) { continue; } + //don't list content packages that only define one sub (they're visible in the "Submarines" section) + if (contentPackage.Files.Count == 1 && contentPackage.Files[0].Type == ContentType.Submarine) { continue; } CreateMyItemFrame(contentPackage, myItemList); } } @@ -459,6 +472,7 @@ namespace Barotrauma OnClicked = (btn, userdata) => { item.UnSubscribe(); + subscribedItemList.RemoveChild(subscribedItemList.Content.GetChildByUserData(item)); return true; } }; @@ -787,34 +801,40 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), modificationDate.RectTransform, Anchor.CenterRight), item.Modified.ToString("dd.MM.yyyy"), textAlignment: Alignment.TopRight); } - private void CreateWorkshopItem() + /*private void CreateWorkshopItem() { - SteamManager.CreateWorkshopItemStaging(new List(), out itemEditor, out itemContentPackage); - } + SteamManager.CreateWorkshopItemStaging("ModName", out itemEditor, out itemContentPackage); + }*/ private void CreateWorkshopItem(Submarine sub) { - SteamManager.CreateWorkshopItemStaging(new List(), out itemEditor, out itemContentPackage); + string destinationFolder = Path.Combine("Mods", sub.Name); + itemContentPackage = ContentPackage.CreatePackage(sub.Name, Path.Combine(destinationFolder, SteamManager.MetadataFileName), corePackage: false); + SteamManager.CreateWorkshopItemStaging(itemContentPackage, out itemEditor); - string destinationPath = Path.Combine(SteamManager.WorkshopItemStagingFolder, "Submarines", Path.GetFileName(sub.FilePath)); - try + string submarineDir = Path.GetDirectoryName(sub.FilePath); + if (submarineDir != Path.GetDirectoryName(destinationFolder)) { - File.Copy(sub.FilePath, destinationPath); + string destinationPath = Path.Combine(destinationFolder, Path.GetFileName(sub.FilePath)); + if (!File.Exists(destinationPath)) + { + File.Move(sub.FilePath, destinationPath); + } + sub.FilePath = destinationPath; } - catch (Exception e) - { - DebugConsole.ThrowError("Failed to copy submarine file \"" + sub.FilePath + "\" to the Workshop item staging folder.", e); - return; - } - - itemContentPackage.AddFile(Path.Combine("Submarines", Path.GetFileName(sub.FilePath)), ContentType.Submarine); + + itemContentPackage.AddFile(sub.FilePath, ContentType.Submarine); itemContentPackage.Name = sub.Name; + itemContentPackage.Save(itemContentPackage.Path); + ContentPackage.List.Add(itemContentPackage); + GameMain.Config.SelectedContentPackages.Add(itemContentPackage); + itemEditor.Title = sub.Name; itemEditor.Tags.Add("Submarine"); itemEditor.Description = sub.Description; if (sub.PreviewImage != null) { - string previewImagePath = Path.GetFullPath(Path.Combine(SteamManager.WorkshopItemStagingFolder, SteamManager.PreviewImageName)); + string previewImagePath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(itemContentPackage.Path), SteamManager.PreviewImageName)); try { using (Stream s = File.Create(previewImagePath)) @@ -837,8 +857,13 @@ namespace Barotrauma } private void CreateWorkshopItem(ContentPackage contentPackage) { - SteamManager.CreateWorkshopItemStaging(new List(), out itemEditor, out itemContentPackage); - string modDirectory = ""; + //SteamManager.CreateWorkshopItemStaging(new List(), out itemEditor, out itemContentPackage); + + itemContentPackage = contentPackage; + SteamManager.CreateWorkshopItemStaging(itemContentPackage, out itemEditor); + itemEditor.Title = contentPackage.Name; + + /*string modDirectory = ""; foreach (ContentFile file in contentPackage.Files) { itemContentPackage.AddFile(file.Path, file.Type); @@ -858,12 +883,8 @@ namespace Barotrauma if (!string.IsNullOrEmpty(modDirectory)) { SaveUtil.CopyFolder(Path.Combine("Mods", modDirectory), Path.Combine(SteamManager.WorkshopItemStagingFolder, "Mods", modDirectory), copySubDirs: true); - } + }*/ - itemContentPackage.CorePackage = contentPackage.CorePackage; - itemContentPackage.Name = contentPackage.Name; - itemContentPackage.Save(itemContentPackage.Path); - itemEditor.Title = contentPackage.Name; } private void CreateWorkshopItem(Facepunch.Steamworks.Workshop.Item item) { @@ -879,8 +900,16 @@ namespace Barotrauma private void ShowCreateItemFrame() { createItemFrame.ClearChildren(); + + if (itemEditor == null) { return; } - if (itemEditor == null) return; + if (itemContentPackage == null) + { + string errorMsg = "Failed to edit workshop item (content package null)\n" + Environment.StackTrace; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("SteamWorkshopScreen.ShowCreateItemFrame:ContentPackageNull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + return; + } var createItemContent = new GUILayoutGroup(new RectTransform(new Vector2(0.92f, 0.92f), createItemFrame.RectTransform, Anchor.Center)) { @@ -987,7 +1016,7 @@ namespace Barotrauma Barotrauma.OpenFileDialog ofd = new Barotrauma.OpenFileDialog() { Multiselect = true, - InitialDirectory = Path.GetFullPath(SteamManager.WorkshopItemStagingFolder), + InitialDirectory = Path.GetFullPath(Path.GetDirectoryName(itemContentPackage.Path)), Filter = TextManager.Get("WorkshopItemPreviewImage") + "|*.png", Title = TextManager.Get("WorkshopItemPreviewImageDialogTitle") }; @@ -1018,9 +1047,19 @@ namespace Barotrauma return true; } }; - + + //if preview image has not been set, but there's a PreviewImage file inside the mod folder, use that by default + if (string.IsNullOrEmpty(itemEditor.PreviewImage)) + { + string previewImagePath = Path.Combine(Path.GetDirectoryName(itemContentPackage.Path), SteamManager.PreviewImageName); + if (File.Exists(previewImagePath)) + { + itemEditor.PreviewImage = Path.GetFullPath(previewImagePath); + } + } if (!string.IsNullOrEmpty(itemEditor.PreviewImage)) { + itemEditor.PreviewImage = Path.GetFullPath(itemEditor.PreviewImage); if (itemPreviewSprites.ContainsKey(itemEditor.PreviewImage)) { itemPreviewSprites[itemEditor.PreviewImage].Remove(); @@ -1068,7 +1107,7 @@ namespace Barotrauma new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), fileListTitle.RectTransform, Anchor.CenterRight), TextManager.Get("WorkshopItemShowFolder")) { IgnoreLayoutGroups = true, - OnClicked = (btn, userdata) => { System.Diagnostics.Process.Start(Path.GetFullPath(SteamManager.WorkshopItemStagingFolder)); return true; } + OnClicked = (btn, userdata) => { System.Diagnostics.Process.Start(Path.GetFullPath(Path.GetDirectoryName(itemContentPackage.Path))); return true; } }; createItemFileList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.35f), createItemContent.RectTransform)); RefreshCreateItemFileList(); @@ -1077,11 +1116,11 @@ namespace Barotrauma { RelativeSpacing = 0.05f }; - + new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), buttonContainer.RectTransform, Anchor.TopRight), TextManager.Get("WorkshopItemRefreshFileList")) { ToolTip = TextManager.Get("WorkshopItemRefreshFileListTooltip"), - OnClicked = (btn, userdata) => + OnClicked = (btn, userdata) => { itemContentPackage = new ContentPackage(itemContentPackage.Path); RefreshCreateItemFileList(); @@ -1096,26 +1135,27 @@ namespace Barotrauma { Barotrauma.OpenFileDialog ofd = new Barotrauma.OpenFileDialog() { - InitialDirectory = Path.GetFullPath(SteamManager.WorkshopItemStagingFolder), + InitialDirectory = Path.GetFullPath(Path.GetDirectoryName(itemContentPackage.Path)), Title = TextManager.Get("workshopitemaddfiles"), Multiselect = true }; if (ofd.ShowDialog() == DialogResult.OK) { OnAddFilesSelected(ofd.FileNames); + RefreshMyItemList(); } } catch { //use a custom prompt if OpenFileDialog fails (Linux/Mac) var msgBox = new GUIMessageBox(TextManager.Get("workshopitemaddfiles"), "", relativeSize: new Vector2(0.4f, 0.2f), - buttons: new string[] { TextManager.Get("Cancel"), TextManager.Get("OK") }); + buttons: new string[] { TextManager.Get("Cancel"), TextManager.Get("OK") }); var pathBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.5f), msgBox.Content.RectTransform, Anchor.Center) { MinSize = new Point(0, 25) }); msgBox.Buttons[0].OnClicked += msgBox.Close; msgBox.Buttons[1].OnClicked += msgBox.Close; - msgBox.Buttons[1].OnClicked += (btn2, userdata2) => + msgBox.Buttons[1].OnClicked += (btn2, userdata2) => { if (string.IsNullOrEmpty(pathBox?.Text)) { return true; } string[] filePaths = pathBox.Text.Split(','); @@ -1131,6 +1171,7 @@ namespace Barotrauma } }; + //the item has been already published if it has a non-zero ID -> allow adding a changenote if (itemEditor.Id > 0) { @@ -1181,6 +1222,9 @@ namespace Barotrauma itemEditor = null; SelectTab(Tab.Browse); deleteVerification.Close(); + createItemFrame.ClearChildren(); + itemContentPackage.SteamWorkshopUrl = ""; + itemContentPackage.Save(itemContentPackage.Path); return true; }; deleteVerification.Buttons[1].OnClicked = deleteVerification.Close; @@ -1221,7 +1265,7 @@ namespace Barotrauma private void OnPreviewImageSelected(GUIImage previewImageElement, string filePath) { - string previewImagePath = Path.GetFullPath(Path.Combine(SteamManager.WorkshopItemStagingFolder, SteamManager.PreviewImageName)); + string previewImagePath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(itemContentPackage.Path), SteamManager.PreviewImageName)); if (new FileInfo(filePath).Length > 1024 * 1024) { new GUIMessageBox(TextManager.Get("Error"), TextManager.Get("WorkshopItemPreviewImageTooLarge")); @@ -1248,33 +1292,36 @@ namespace Barotrauma if (fileNames == null) { return; } for (int i = 0; i < fileNames.Length; i++) { - string file = fileNames[i]; - if (string.IsNullOrEmpty(file)) { continue; } - file = file.Trim(); - if (!File.Exists(file)) { continue; } + string file = fileNames[i]?.Trim(); + if (string.IsNullOrEmpty(file) || !File.Exists(file)) { continue; } - string filePathRelativeToStagingFolder = UpdaterUtil.GetRelativePath(file, Path.Combine(Environment.CurrentDirectory, SteamManager.WorkshopItemStagingFolder)); - string filePathRelativeToBaseFolder = UpdaterUtil.GetRelativePath(file, Environment.CurrentDirectory); - //file is not inside the staging folder - if (filePathRelativeToStagingFolder.StartsWith("..")) + string modFolder = Path.GetDirectoryName(itemContentPackage.Path); + string filePathRelativeToModFolder = UpdaterUtil.GetRelativePath(file, Path.Combine(Environment.CurrentDirectory, modFolder)); + string destinationPath = Path.Combine(modFolder, Path.GetFileName(file)); + + //file is not inside the mod folder, we need to move it + if (filePathRelativeToModFolder.StartsWith("..") || + Path.GetPathRoot(Environment.CurrentDirectory) != Path.GetPathRoot(file)) { - //submarines can be included in the content package directly - string basePath = Path.GetDirectoryName(filePathRelativeToBaseFolder.Replace("..", "")); - if (basePath == "Submarines") + string tryPath = destinationPath; + //add a number to the filename if a file with the same name already exists + i = 2; + while (File.Exists(destinationPath)) + { + destinationPath = Path.Combine(modFolder, $"{Path.GetFileNameWithoutExtension(file)} ({i}){Path.GetExtension(file)}"); + i++; + } + try { - string destinationPath = Path.Combine(SteamManager.WorkshopItemStagingFolder, "Submarines", Path.GetFileName(file)); File.Copy(file, destinationPath); - itemContentPackage.AddFile(filePathRelativeToBaseFolder, ContentType.Submarine); } - else + catch (Exception e) { - itemContentPackage.AddFile(filePathRelativeToBaseFolder, ContentType.None); + DebugConsole.ThrowError("Copying the file \""+file+"\" to the mod folder failed.", e); + return; } } - else - { - itemContentPackage.AddFile(filePathRelativeToStagingFolder, ContentType.None); - } + itemContentPackage.AddFile(destinationPath, ContentType.None); } itemContentPackage.Save(itemContentPackage.Path); RefreshCreateItemFileList(); @@ -1289,9 +1336,9 @@ namespace Barotrauma foreach (ContentFile contentFile in itemContentPackage.Files) { bool illegalPath = !ContentPackage.IsModFilePathAllowed(contentFile); - string pathInStagingFolder = Path.Combine(SteamManager.WorkshopItemStagingFolder, contentFile.Path); - bool fileInStagingFolder = File.Exists(pathInStagingFolder); - bool fileExists = illegalPath ? File.Exists(contentFile.Path) : fileInStagingFolder; + //string pathInStagingFolder = Path.Combine(SteamManager.WorkshopItemStagingFolder, contentFile.Path); + //bool fileInStagingFolder = File.Exists(pathInStagingFolder); + bool fileExists = File.Exists(contentFile.Path); var fileFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.12f), createItemFileList.Content.RectTransform) { MinSize = new Point(0, 20) }, style: "ListBoxElement") @@ -1309,7 +1356,7 @@ namespace Barotrauma { Selected = fileExists && !illegalPath, Enabled = false, - ToolTip = TextManager.Get(fileInStagingFolder ? "WorkshopItemFileIncluded" : "WorkshopItemFileNotIncluded") + ToolTip = TextManager.Get(illegalPath ? "WorkshopItemFileNotIncluded" : "WorkshopItemFileIncluded") }; var nameText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), content.RectTransform, Anchor.CenterLeft), contentFile.Path, font: GUI.SmallFont) @@ -1352,6 +1399,7 @@ namespace Barotrauma itemContentPackage.RemoveFile(contentFile); itemContentPackage.Save(itemContentPackage.Path); RefreshCreateItemFileList(); + RefreshMyItemList(); return true; } }; @@ -1408,8 +1456,8 @@ namespace Barotrauma if (errorMsg == null) { new GUIMessageBox( - TextManager.Get("Error"), - TextManager.GetWithVariable("WorkshopItemPublishFailed", "[itemname]", TextManager.EnsureUTF8(item.Title)) + item.Error); + TextManager.Get("Error"), + TextManager.GetWithVariable("WorkshopItemPublishFailed", "[itemname]", TextManager.EnsureUTF8(item.Title)) + " " + item.Error); } else { @@ -1418,6 +1466,7 @@ namespace Barotrauma } createItemFrame.ClearChildren(); + RefreshItemLists(); SelectTab(Tab.Browse); } diff --git a/Barotrauma/BarotraumaClient/Source/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/Source/Screens/SubEditorScreen.cs index 31a310ecb..1bcf46154 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/SubEditorScreen.cs @@ -646,8 +646,8 @@ namespace Barotrauma SoundPlayer.OverrideMusicType = "none"; SoundPlayer.OverrideMusicDuration = null; - GameMain.SoundManager.SetCategoryGainMultiplier("default", 0.0f); - GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", 0.0f); + GameMain.SoundManager.SetCategoryGainMultiplier("default", 0.0f, 0); + GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", 0.0f, 0); linkedSubBox.ClearChildren(); foreach (Submarine sub in Submarine.SavedSubmarines) @@ -688,8 +688,8 @@ namespace Barotrauma if (WiringMode) SetWiringMode(false); SoundPlayer.OverrideMusicType = null; - GameMain.SoundManager.SetCategoryGainMultiplier("default", GameMain.Config.SoundVolume); - GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", GameMain.Config.SoundVolume); + GameMain.SoundManager.SetCategoryGainMultiplier("default", GameMain.Config.SoundVolume, 0); + GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", GameMain.Config.SoundVolume, 0); if (dummyCharacter != null) { @@ -1000,7 +1000,8 @@ namespace Barotrauma submarineDescriptionCharacterCount = new GUITextBlock(new RectTransform(new Vector2(.5f, 1f), descriptionHeaderGroup.RectTransform), string.Empty, textAlignment: Alignment.TopRight); var descriptionContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.25f), leftColumn.RectTransform)); - descriptionBox = new GUITextBox(new RectTransform(Vector2.One, descriptionContainer.Content.RectTransform, Anchor.Center), font: GUI.SmallFont, wrap: true); + descriptionBox = new GUITextBox(new RectTransform(Vector2.One, descriptionContainer.Content.RectTransform, Anchor.Center), font: GUI.SmallFont, wrap: true, textAlignment: Alignment.TopLeft); + descriptionBox.Padding = new Vector4(10 * GUI.Scale); descriptionBox.OnTextChanged += (textBox, text) => { @@ -1189,11 +1190,13 @@ namespace Barotrauma var contentPackList = new GUIListBox(new RectTransform(new Vector2(0.5f, 1.0f - contentPackagesLabel.RectTransform.RelativeSize.Y), horizontalArea.RectTransform, Anchor.BottomRight)); - List contentPacks = Submarine.MainSub.RequiredContentPackages.ToList(); foreach (ContentPackage contentPack in ContentPackage.List) - { - if (!contentPacks.Contains(contentPack.Name)) contentPacks.Add(contentPack.Name); + { + //don't show content packages that only define submarine files + //(it doesn't make sense to require another sub to be installed to install this one) + if (contentPack.Files.All(cp => cp.Type == ContentType.Submarine)) { continue; } + if (!contentPacks.Contains(contentPack.Name)) { contentPacks.Add(contentPack.Name); } } foreach (string contentPackageName in contentPacks) @@ -2371,11 +2374,7 @@ namespace Barotrauma sub.UpdateTransform(); } - spriteBatch.Begin(SpriteSortMode.BackToFront, - BlendState.AlphaBlend, - null, null, null, null, - cam.Transform); - + spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.AlphaBlend, transformMatrix: cam.Transform); graphics.Clear(new Color(0.051f, 0.149f, 0.271f, 1.0f)); if (GameMain.DebugDraw) { @@ -2383,7 +2382,12 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, new Vector2(cam.WorldView.X, -Submarine.MainSub.HiddenSubPosition.Y), new Vector2(cam.WorldView.Right, -Submarine.MainSub.HiddenSubPosition.Y), Color.White * 0.5f, 1.0f, (int)(2.0f / cam.Zoom)); } - Submarine.Draw(spriteBatch, true); + Submarine.DrawBack(spriteBatch, editing: true); + + spriteBatch.End(); + spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.AlphaBlend, transformMatrix: cam.Transform); + + Submarine.DrawFront(spriteBatch, editing: true); if (!CharacterMode && !WiringMode && GUI.MouseOn == null) { diff --git a/Barotrauma/BarotraumaClient/Source/Sounds/OggSound.cs b/Barotrauma/BarotraumaClient/Source/Sounds/OggSound.cs index 7f59ebd1e..16f503231 100644 --- a/Barotrauma/BarotraumaClient/Source/Sounds/OggSound.cs +++ b/Barotrauma/BarotraumaClient/Source/Sounds/OggSound.cs @@ -12,6 +12,9 @@ namespace Barotrauma.Sounds //key = sample rate, value = filter private static Dictionary muffleFilters = new Dictionary(); + private static List playbackAmplitude; + private const int AMPLITUDE_SAMPLE_COUNT = 4410; //100ms in a 44100hz file + public OggSound(SoundManager owner, string filename, bool stream) : base(owner, filename, stream, true) { if (!ToolBox.IsProperFilenameCase(filename)) @@ -32,6 +35,18 @@ namespace Barotrauma.Sounds short[] shortBuffer = new short[bufferSize]; int readSamples = reader.ReadSamples(floatBuffer, 0, bufferSize); + + playbackAmplitude = new List(); + for (int i=0;i= bufferSize) { break; } + maxAmplitude = Math.Max(maxAmplitude, Math.Abs(floatBuffer[j])); + } + playbackAmplitude.Add(maxAmplitude); + } CastBuffer(floatBuffer, shortBuffer, readSamples); @@ -61,6 +76,15 @@ namespace Barotrauma.Sounds } } + public override float GetAmplitudeAtPlaybackPos(int playbackPos) + { + if (playbackAmplitude == null || playbackAmplitude.Count == 0) { return 0.0f; } + int index = playbackPos / AMPLITUDE_SAMPLE_COUNT; + if (index < 0) { return 0.0f; } + if (index >= playbackAmplitude.Count) { index = playbackAmplitude.Count - 1; } + return playbackAmplitude[index]; + } + public override int FillStreamBuffer(int samplePos, short[] buffer) { if (!Stream) throw new Exception("Called FillStreamBuffer on a non-streamed sound!"); diff --git a/Barotrauma/BarotraumaClient/Source/Sounds/Sound.cs b/Barotrauma/BarotraumaClient/Source/Sounds/Sound.cs index a5685cbd1..68cdefd86 100644 --- a/Barotrauma/BarotraumaClient/Source/Sounds/Sound.cs +++ b/Barotrauma/BarotraumaClient/Source/Sounds/Sound.cs @@ -179,6 +179,8 @@ namespace Barotrauma.Sounds public abstract int FillStreamBuffer(int samplePos, short[] buffer); + public abstract float GetAmplitudeAtPlaybackPos(int playbackPos); + public virtual void Dispose() { if (disposed) { return; } diff --git a/Barotrauma/BarotraumaClient/Source/Sounds/SoundChannel.cs b/Barotrauma/BarotraumaClient/Source/Sounds/SoundChannel.cs index 7a33aaad2..7fedeae71 100644 --- a/Barotrauma/BarotraumaClient/Source/Sounds/SoundChannel.cs +++ b/Barotrauma/BarotraumaClient/Source/Sounds/SoundChannel.cs @@ -2,6 +2,7 @@ using OpenAL; using Microsoft.Xna.Framework; using System.Collections.Generic; +using System.Threading; namespace Barotrauma.Sounds { @@ -79,7 +80,7 @@ namespace Barotrauma.Sounds public class SoundChannel : IDisposable { - private const int STREAM_BUFFER_SIZE = 65536; + private const int STREAM_BUFFER_SIZE = 8820; private short[] streamShortBuffer; private Vector3? position; @@ -282,6 +283,35 @@ namespace Barotrauma.Sounds } } + private float streamAmplitude; + public float CurrentAmplitude + { + get + { + if (!IsPlaying) { return 0.0f; } + + uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex); + if (!IsStream) + { + int playbackPos; Al.GetSourcei(alSource, Al.SampleOffset, out playbackPos); + int alError = Al.GetError(); + if (alError != Al.NoError) + { + throw new Exception("Failed to get source's playback position: " + Al.GetErrorString(alError)); + } + return Sound.GetAmplitudeAtPlaybackPos(playbackPos); + } + else + { + float retVal = -1.0f; + Monitor.Enter(mutex); + retVal = streamAmplitude; + Monitor.Exit(mutex); + return retVal; + } + } + } + private string category; public string Category { @@ -311,10 +341,12 @@ namespace Barotrauma.Sounds private set; } private int streamSeekPos; - private bool startedPlaying; + private int buffersToRequeue; private bool reachedEndSample; + private int queueStartIndex; private readonly uint[] streamBuffers; - private readonly List emptyBuffers; + private uint[] unqueuedBuffers; + private float[] streamBufferAmplitudes; private object mutex; @@ -346,12 +378,17 @@ namespace Barotrauma.Sounds FilledByNetwork = sound is VoipSound; decayTimer = 0; streamSeekPos = 0; reachedEndSample = false; - startedPlaying = true; - - mutex = new object(); + buffersToRequeue = 4; + muffled = muffle; - lock (mutex) + if (IsStream) { + mutex = new object(); + } + + try + { + if (mutex != null) { Monitor.Enter(mutex); } if (sound.Owner.CountPlayingInstances(sound) < sound.MaxSimultaneousInstances) { ALSourceIndex = sound.Owner.AssignFreeSourceToChannel(this); @@ -390,7 +427,8 @@ namespace Barotrauma.Sounds } else { - Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Buffer, (int)sound.ALBuffer); + uint alBuffer = sound.Owner.GetCategoryMuffle(category) || muffle ? sound.ALMuffledBuffer : sound.ALBuffer; + Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Buffer, (int)alBuffer); int alError = Al.GetError(); if (alError != Al.NoError) { @@ -407,7 +445,8 @@ namespace Barotrauma.Sounds streamShortBuffer = new short[STREAM_BUFFER_SIZE]; streamBuffers = new uint[4]; - emptyBuffers = new List(); + unqueuedBuffers = new uint[4]; + streamBufferAmplitudes = new float[4]; for (int i = 0; i < 4; i++) { Al.GenBuffer(out streamBuffers[i]); @@ -423,18 +462,27 @@ namespace Barotrauma.Sounds throw new Exception("Generated streamBuffer[" + i.ToString() + "] is invalid!"); } } - Sound.Owner.InitStreamThread(); } } - + this.Position = position; this.Gain = gain; this.Looping = false; this.Near = near; this.Far = far; this.Category = category; - } + } + catch + { + throw; + } + finally + { + if (mutex != null) { Monitor.Exit(mutex); } + } + + Sound.Owner.Update(); } public bool FadingOutAndDisposing; @@ -445,8 +493,9 @@ namespace Barotrauma.Sounds public void Dispose() { - lock (mutex) + try { + if (mutex != null) { Monitor.Enter(mutex); } if (ALSourceIndex >= 0) { Al.SourceStop(Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex)); @@ -467,19 +516,17 @@ namespace Barotrauma.Sounds throw new Exception("Failed to stop streamed source: " + Al.GetErrorString(alError)); } - int buffersToUnqueue = 0; - uint[] unqueuedBuffers = null; - - buffersToUnqueue = 0; - Al.GetSourcei(alSource, Al.BuffersProcessed, out buffersToUnqueue); + int buffersToRequeue = 0; + + buffersToRequeue = 0; + Al.GetSourcei(alSource, Al.BuffersProcessed, out buffersToRequeue); alError = Al.GetError(); if (alError != Al.NoError) { throw new Exception("Failed to determine processed buffers from streamed source: " + Al.GetErrorString(alError)); } - - unqueuedBuffers = new uint[buffersToUnqueue]; - Al.SourceUnqueueBuffers(alSource, buffersToUnqueue, unqueuedBuffers); + + Al.SourceUnqueueBuffers(alSource, buffersToRequeue, unqueuedBuffers); alError = Al.GetError(); if (alError != Al.NoError) { @@ -518,14 +565,19 @@ namespace Barotrauma.Sounds ALSourceIndex = -1; } } + finally + { + if (mutex != null) { Monitor.Exit(mutex); } + } } public void UpdateStream() { if (!IsStream) throw new Exception("Called UpdateStream on a non-streamed sound channel!"); - lock (mutex) + try { + Monitor.Enter(mutex); if (!reachedEndSample) { uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex); @@ -538,48 +590,38 @@ namespace Barotrauma.Sounds { throw new Exception("Failed to determine playing state from streamed source: " + Al.GetErrorString(alError)); } - - int buffersToUnqueue = 0; - uint[] unqueuedBuffers = null; - if (!startedPlaying) - { - buffersToUnqueue = 0; - Al.GetSourcei(alSource, Al.BuffersProcessed, out buffersToUnqueue); - alError = Al.GetError(); - if (alError != Al.NoError) - { - throw new Exception("Failed to determine processed buffers from streamed source: " + Al.GetErrorString(alError)); - } - unqueuedBuffers = new uint[buffersToUnqueue+emptyBuffers.Count]; - Al.SourceUnqueueBuffers(alSource, buffersToUnqueue, unqueuedBuffers); - for (int i = 0; i < emptyBuffers.Count; i++) - { - unqueuedBuffers[buffersToUnqueue + i] = emptyBuffers[i]; - } - buffersToUnqueue += emptyBuffers.Count; - emptyBuffers.Clear(); - alError = Al.GetError(); - if (alError != Al.NoError) - { - throw new Exception("Failed to unqueue buffers from streamed source: " + Al.GetErrorString(alError)); - } - } - else + int unqueuedBufferCount; + Al.GetSourcei(alSource, Al.BuffersProcessed, out unqueuedBufferCount); + alError = Al.GetError(); + if (alError != Al.NoError) { - startedPlaying = false; - buffersToUnqueue = 4; - unqueuedBuffers = new uint[4]; - for (int i = 0; i < 4; i++) - { - unqueuedBuffers[i] = streamBuffers[i]; - } + throw new Exception("Failed to determine processed buffers from streamed source: " + Al.GetErrorString(alError)); + } + + Al.SourceUnqueueBuffers(alSource, unqueuedBufferCount, unqueuedBuffers); + alError = Al.GetError(); + if (alError != Al.NoError) + { + throw new Exception("Failed to unqueue buffers from streamed source: " + Al.GetErrorString(alError)); } - for (int i = 0; i < buffersToUnqueue; i++) + buffersToRequeue += unqueuedBufferCount; + + int iterCount = buffersToRequeue; + for (int k = 0; k < iterCount; k++) { + int index = queueStartIndex; short[] buffer = streamShortBuffer; int readSamples = Sound.FillStreamBuffer(streamSeekPos, buffer); + float readAmplitude = 0.0f; + + for (int i=0;i 120) //TODO: replace magic number { @@ -618,38 +661,54 @@ namespace Barotrauma.Sounds if (readSamples > 0) { - Al.BufferData(unqueuedBuffers[i], Sound.ALFormat, buffer, readSamples, Sound.SampleRate); + streamBufferAmplitudes[index] = readAmplitude; + + Al.BufferData(streamBuffers[index], Sound.ALFormat, buffer, readSamples, Sound.SampleRate); alError = Al.GetError(); if (alError != Al.NoError) { throw new Exception("Failed to assign data to stream buffer: " + - Al.GetErrorString(alError) + ": " + unqueuedBuffers[i].ToString() + "/" + unqueuedBuffers.Length + ", readSamples: " + readSamples); + Al.GetErrorString(alError) + ": " + streamBuffers[index].ToString() + "/" + streamBuffers.Length + ", readSamples: " + readSamples); } - Al.SourceQueueBuffer(alSource, unqueuedBuffers[i]); + Al.SourceQueueBuffer(alSource, streamBuffers[index]); + queueStartIndex = (queueStartIndex + 1) % 4; + alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to queue buffer[" + i.ToString() + "] to stream: " + Al.GetErrorString(alError)); + throw new Exception("Failed to queue streamBuffer[" + index.ToString() + "] to stream: " + Al.GetErrorString(alError)); } } - else if (readSamples < 0) - { - reachedEndSample = true; - } else { - emptyBuffers.Add((uint)unqueuedBuffers[i]); + if (readSamples < 0) + { + reachedEndSample = true; + } + break; } + buffersToRequeue--; } - + + streamAmplitude = streamBufferAmplitudes[queueStartIndex]; + Al.GetSourcei(alSource, Al.SourceState, out state); if (state != Al.Playing) { Al.SourcePlay(alSource); } } + + if (reachedEndSample) + { + streamAmplitude = 0.0f; + } + } + finally + { + Monitor.Exit(mutex); } } } diff --git a/Barotrauma/BarotraumaClient/Source/Sounds/SoundManager.cs b/Barotrauma/BarotraumaClient/Source/Sounds/SoundManager.cs index ddd10fbba..c6c65088e 100644 --- a/Barotrauma/BarotraumaClient/Source/Sounds/SoundManager.cs +++ b/Barotrauma/BarotraumaClient/Source/Sounds/SoundManager.cs @@ -100,6 +100,47 @@ namespace Barotrauma.Sounds } } } + + public float PlaybackAmplitude + { + get + { + float aggregateAmplitude = 0.0f; + //NOTE: this is obviously not entirely accurate; + //It assumes a linear falloff model, and assumes that audio + //is simply added together to produce the final result. + //Adjustments may be needed under certain scenarios. + for (int i=0;i<2;i++) + { + foreach (SoundChannel soundChannel in playingChannels[i].Where(ch => ch != null)) + { + float amplitude = soundChannel.CurrentAmplitude; + amplitude *= soundChannel.Gain; + float dist = Vector3.Distance(ListenerPosition, soundChannel.Position ?? ListenerPosition); + if (dist > soundChannel.Near) + { + amplitude *= 1.0f - Math.Min(1.0f, (dist - soundChannel.Near) / (soundChannel.Far - soundChannel.Near)); + } + aggregateAmplitude += amplitude; + } + } + return aggregateAmplitude; + } + } + + public float CompressionDynamicRangeGain { get; private set; } + + private float voipAttenuatedGain; + private double lastAttenuationTime; + public float VoipAttenuatedGain + { + get { return voipAttenuatedGain; } + set + { + lastAttenuationTime = Timing.TotalTime; + voipAttenuatedGain = value; + } + } public int LoadedSoundCount { @@ -110,7 +151,43 @@ namespace Barotrauma.Sounds get { return loadedSounds.Select(s => s.Filename).Distinct().Count(); } } - private Dictionary> categoryModifiers; + private class CategoryModifier + { + public float[] GainMultipliers; + public bool Muffle; + + public CategoryModifier(int gainMultiplierIndex, float gain, bool muffle) + { + Muffle = muffle; + GainMultipliers = new float[gainMultiplierIndex+1]; + for (int i=0;i categoryModifiers; public SoundManager() { @@ -190,6 +267,8 @@ namespace Barotrauma.Sounds ListenerPosition = Vector3.Zero; ListenerTargetVector = new Vector3(0.0f, 0.0f, 1.0f); ListenerUpVector = new Vector3(0.0f, -1.0f, 0.0f); + + CompressionDynamicRangeGain = 1.0f; } public Sound LoadSound(string filename, bool stream = false) @@ -257,11 +336,12 @@ namespace Barotrauma.Sounds { if (Disabled) { return -1; } - lock (playingChannels) + //remove a channel that has stopped + //or hasn't even been assigned + int poolIndex = (int)newChannel.Sound.SourcePoolIndex; + + lock (playingChannels[poolIndex]) { - //remove a channel that has stopped - //or hasn't even been assigned - int poolIndex = (int)newChannel.Sound.SourcePoolIndex; for (int i = 0; i < playingChannels[poolIndex].Length; i++) { if (playingChannels[poolIndex][i] == null || !playingChannels[poolIndex][i].IsPlaying) @@ -271,10 +351,10 @@ namespace Barotrauma.Sounds return i; } } - - //we couldn't get a free source to assign to this channel! - return -1; } + + //we couldn't get a free source to assign to this channel! + return -1; } #if DEBUG @@ -291,7 +371,7 @@ namespace Barotrauma.Sounds public bool IsPlaying(Sound sound) { if (Disabled) { return false; } - lock (playingChannels) + lock (playingChannels[(int)sound.SourcePoolIndex]) { for (int i = 0; i < playingChannels[(int)sound.SourcePoolIndex].Length; i++) { @@ -309,7 +389,7 @@ namespace Barotrauma.Sounds { if (Disabled) { return 0; } int count = 0; - lock (playingChannels) + lock (playingChannels[(int)sound.SourcePoolIndex]) { for (int i = 0; i < playingChannels[(int)sound.SourcePoolIndex].Length; i++) { @@ -326,7 +406,7 @@ namespace Barotrauma.Sounds public SoundChannel GetChannelFromSound(Sound sound) { if (Disabled) { return null; } - lock (playingChannels) + lock (playingChannels[(int)sound.SourcePoolIndex]) { for (int i = 0; i < playingChannels[(int)sound.SourcePoolIndex].Length; i++) { @@ -343,7 +423,7 @@ namespace Barotrauma.Sounds public void KillChannels(Sound sound) { if (Disabled) { return; } - lock (playingChannels) + lock (playingChannels[(int)sound.SourcePoolIndex]) { for (int i = 0; i < playingChannels[(int)sound.SourcePoolIndex].Length; i++) { @@ -371,22 +451,23 @@ namespace Barotrauma.Sounds } } - public void SetCategoryGainMultiplier(string category, float gain) + public void SetCategoryGainMultiplier(string category, float gain, int index=0) { if (Disabled) { return; } category = category.ToLower(); - if (categoryModifiers == null) categoryModifiers = new Dictionary>(); + if (categoryModifiers == null) categoryModifiers = new Dictionary(); if (!categoryModifiers.ContainsKey(category)) { - categoryModifiers.Add(category, new Pair(gain, false)); + categoryModifiers.Add(category, new CategoryModifier(index, gain, false)); } else { - categoryModifiers[category].First = gain; + categoryModifiers[category].SetGainMultiplier(index, gain); } - lock (playingChannels) + + for (int i = 0; i < playingChannels.Length; i++) { - for (int i = 0; i < playingChannels.Length; i++) + lock (playingChannels[i]) { for (int j = 0; j < playingChannels[i].Length; j++) { @@ -399,12 +480,24 @@ namespace Barotrauma.Sounds } } - public float GetCategoryGainMultiplier(string category) + public float GetCategoryGainMultiplier(string category, int index=-1) { if (Disabled) { return 0.0f; } category = category.ToLower(); if (categoryModifiers == null || !categoryModifiers.ContainsKey(category)) return 1.0f; - return categoryModifiers[category].First; + if (index < 0) + { + float accumulatedMultipliers = 1.0f; + for (int i=0;i>(); + if (categoryModifiers == null) categoryModifiers = new Dictionary(); if (!categoryModifiers.ContainsKey(category)) { - categoryModifiers.Add(category, new Pair(1.0f, muffle)); + categoryModifiers.Add(category, new CategoryModifier(0, 1.0f, muffle)); } else { - categoryModifiers[category].Second = muffle; + categoryModifiers[category].Muffle = muffle; } for (int i = 0; i < playingChannels.Length; i++) { - for (int j = 0; j < playingChannels[i].Length; j++) + lock (playingChannels[i]) { - if (playingChannels[i][j] != null && playingChannels[i][j].IsPlaying) + for (int j = 0; j < playingChannels[i].Length; j++) { - if (playingChannels[i][j].Category.ToLower() == category) playingChannels[i][j].Muffled = muffle; + if (playingChannels[i][j] != null && playingChannels[i][j].IsPlaying) + { + if (playingChannels[i][j].Category.ToLower() == category) playingChannels[i][j].Muffled = muffle; + } } } } @@ -441,7 +537,45 @@ namespace Barotrauma.Sounds category = category.ToLower(); if (categoryModifiers == null || !categoryModifiers.ContainsKey(category)) return false; - return categoryModifiers[category].Second; + return categoryModifiers[category].Muffle; + } + + public void Update() + { + if (GameMain.Client != null && GameMain.Config.VoipAttenuationEnabled) + { + if (Timing.TotalTime > lastAttenuationTime+0.2) + { + voipAttenuatedGain = voipAttenuatedGain * 0.9f + 0.1f; + } + } + else + { + voipAttenuatedGain = 1.0f; + } + SetCategoryGainMultiplier("default", VoipAttenuatedGain, 1); + SetCategoryGainMultiplier("ui", VoipAttenuatedGain, 1); + SetCategoryGainMultiplier("waterambience", VoipAttenuatedGain, 1); + SetCategoryGainMultiplier("music", VoipAttenuatedGain, 1); + + if (GameMain.Config.DynamicRangeCompressionEnabled) + { + float targetGain = (Math.Min(1.0f, 1.0f / PlaybackAmplitude) - 1.0f) * 0.5f + 1.0f; + if (targetGain < CompressionDynamicRangeGain) + { + //if the target gain is lower than the current gain, lower the current gain immediately to prevent clipping + CompressionDynamicRangeGain = targetGain; + } + else + { + //otherwise, let it rise back smoothly + CompressionDynamicRangeGain = (targetGain) * 0.05f + CompressionDynamicRangeGain * 0.95f; + } + } + else + { + CompressionDynamicRangeGain = 1.0f; + } } public void InitStreamThread() @@ -464,9 +598,10 @@ namespace Barotrauma.Sounds while (areStreamsPlaying) { areStreamsPlaying = false; - lock (playingChannels) + + for (int i = 0; i < playingChannels.Length; i++) { - for (int i = 0; i < playingChannels.Length; i++) + lock (playingChannels[i]) { for (int j = 0; j < playingChannels[i].Length; j++) { @@ -497,14 +632,14 @@ namespace Barotrauma.Sounds Thread.Sleep(10); //TODO: use a separate thread for network audio? } } - + public void Dispose() { if (Disabled) { return; } - lock (playingChannels) + for (int i = 0; i < playingChannels.Length; i++) { - for (int i = 0; i < playingChannels.Length; i++) + lock (playingChannels[i]) { for (int j = 0; j < playingChannels[i].Length; j++) { @@ -512,6 +647,7 @@ namespace Barotrauma.Sounds } } } + streamingThread?.Join(); for (int i = loadedSounds.Count - 1; i >= 0; i--) { diff --git a/Barotrauma/BarotraumaClient/Source/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/Source/Sounds/SoundPlayer.cs index 623535393..f3332b751 100644 --- a/Barotrauma/BarotraumaClient/Source/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/Source/Sounds/SoundPlayer.cs @@ -393,19 +393,19 @@ namespace Barotrauma if (Math.Abs(diff.X) < FireSoundRange && Math.Abs(diff.Y) < FireSoundRange) { Vector2 diffLeft = (fs.WorldPosition + new Vector2(fs.Size.X, fs.Size.Y / 2)) - listenerPos; - if (diff.X < fs.Size.X / 2.0f) diff.X = 0.0f; + if (Math.Abs(diff.X) < fs.Size.X / 2.0f) { diffLeft.X = 0.0f; } if (diffLeft.X <= 0) { float distFallOffLeft = diffLeft.Length() / FireSoundRange; if (distFallOffLeft < 0.99f) { - fireVolumeLeft[0] += (1.0f - distFallOffLeft) * (fs.Size.X / FireSoundLargeLimit); + fireVolumeLeft[0] += (1.0f - distFallOffLeft); if (fs.Size.X > FireSoundLargeLimit) fireVolumeLeft[1] += (1.0f - distFallOffLeft) * ((fs.Size.X - FireSoundLargeLimit) / FireSoundLargeLimit); } } Vector2 diffRight = (fs.WorldPosition + new Vector2(0.0f, fs.Size.Y / 2)) - listenerPos; - if (diff.X < fs.Size.X / 2.0f) diff.X = 0.0f; + if (Math.Abs(diff.X) < fs.Size.X / 2.0f) { diffRight.X = 0.0f; } if (diffRight.X >= 0) { float distFallOffRight = diffRight.Length() / FireSoundRange; diff --git a/Barotrauma/BarotraumaClient/Source/Sounds/VideoSound.cs b/Barotrauma/BarotraumaClient/Source/Sounds/VideoSound.cs index 12be94908..8f73f7e7d 100644 --- a/Barotrauma/BarotraumaClient/Source/Sounds/VideoSound.cs +++ b/Barotrauma/BarotraumaClient/Source/Sounds/VideoSound.cs @@ -32,6 +32,11 @@ namespace Barotrauma.Sounds video = vid; } + public override float GetAmplitudeAtPlaybackPos(int playbackPos) + { + throw new NotImplementedException(); + } + public override bool IsPlaying() { bool retVal = false; @@ -65,8 +70,15 @@ namespace Barotrauma.Sounds SoundChannel chn = null; lock (mutex) { - if (soundChannel != null) soundChannel.Dispose(); - chn = new SoundChannel(this, gain, null, 1.0f, 3.0f, "video", false); + if (soundChannel != null) + { + soundChannel.Dispose(); + soundChannel = null; + } + } + chn = new SoundChannel(this, gain, null, 1.0f, 3.0f, "video", false); + lock (mutex) + { soundChannel = chn; } return chn; diff --git a/Barotrauma/BarotraumaClient/Source/Sounds/VoipSound.cs b/Barotrauma/BarotraumaClient/Source/Sounds/VoipSound.cs index 5eb8ad951..bd137b800 100644 --- a/Barotrauma/BarotraumaClient/Source/Sounds/VoipSound.cs +++ b/Barotrauma/BarotraumaClient/Source/Sounds/VoipSound.cs @@ -25,7 +25,7 @@ namespace Barotrauma.Sounds } private VoipQueue queue; - public int bufferID = 0; + private int bufferID = 0; private SoundChannel soundChannel; @@ -54,6 +54,11 @@ namespace Barotrauma.Sounds } } + public float CurrentAmplitude + { + get { return soundChannel?.CurrentAmplitude ?? 0.0f; } + } + public VoipSound(SoundManager owner, VoipQueue q) : base(owner, "voip", true, true) { VoipConfig.SetupEncoding(); @@ -70,6 +75,11 @@ namespace Barotrauma.Sounds soundChannel = chn; } + public override float GetAmplitudeAtPlaybackPos(int playbackPos) + { + throw new NotImplementedException(); //TODO: implement? + } + public void SetPosition(Vector3? pos) { soundChannel.Position = pos; diff --git a/Barotrauma/BarotraumaClient/Source/Sprite/Sprite.cs b/Barotrauma/BarotraumaClient/Source/Sprite/Sprite.cs index fd8b893c0..ddf6f46b1 100644 --- a/Barotrauma/BarotraumaClient/Source/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaClient/Source/Sprite/Sprite.cs @@ -3,6 +3,7 @@ using Microsoft.Xna.Framework.Graphics; using System; using System.IO; using System.Linq; +using System.Collections.Generic; namespace Barotrauma { @@ -75,14 +76,13 @@ namespace Barotrauma } } - public void ReloadTexture() - { - var sprites = LoadedSprites.Where(s => s.Texture == texture).ToList(); - texture.Dispose(); - texture = null; + public void ReloadTexture(bool updateAllSprites = false) => ReloadTexture(updateAllSprites ? LoadedSprites.Where(s => s.Texture == texture) : new Sprite[] { this }); + public void ReloadTexture(IEnumerable spritesToUpdate) + { + texture.Dispose(); texture = TextureLoader.FromFile(FilePath, preMultipliedAlpha); - foreach (Sprite sprite in sprites) + foreach (Sprite sprite in spritesToUpdate) { sprite.texture = texture; } diff --git a/Barotrauma/BarotraumaClient/Source/Utils/ToolBox.cs b/Barotrauma/BarotraumaClient/Source/Utils/ToolBox.cs index c949e94fc..37f56a618 100644 --- a/Barotrauma/BarotraumaClient/Source/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaClient/Source/Utils/ToolBox.cs @@ -100,15 +100,19 @@ namespace Barotrauma return Color.Lerp(gradient[(int)scaledT], gradient[(int)Math.Min(scaledT + 1, gradient.Length - 1)], (scaledT - (int)scaledT)); } - public static string WrapText(string text, float lineLength, ScalableFont font, float textScale = 1.0f) //TODO: could integrate this into the ScalableFont class directly + public static string WrapText(string text, float lineLength, ScalableFont font, float textScale = 1.0f, bool playerInput = false) //TODO: could integrate this into the ScalableFont class directly { Vector2 textSize = font.MeasureString(text); if (textSize.X < lineLength) { return text; } - text = text.Replace("\n", " \n "); + if (!playerInput) + { + text = text.Replace("\n", " \n "); + } List words = new List(); string currWord = ""; + for (int i = 0; i < text.Length; i++) { if (TextManager.IsCJK(text[i].ToString())) @@ -127,6 +131,7 @@ namespace Barotrauma words.Add(currWord); currWord = ""; } + words.Add(string.Empty); } else { @@ -144,69 +149,128 @@ namespace Barotrauma Vector2 spaceSize = font.MeasureString(" ") * textScale; for (int i = 0; i < words.Count; ++i) { - if (words[i].Length == 0) + string currentWord = words[i]; + if (currentWord.Length == 0) { - //space + // space + currentWord = " "; } - else if (string.IsNullOrWhiteSpace(words[i]) && words[i] != "\n") + else if (string.IsNullOrWhiteSpace(currentWord) && currentWord != "\n") { continue; } - Vector2 size = words[i].Length == 0 ? spaceSize : font.MeasureString(words[i]) * textScale; + Vector2 size = words[i].Length == 0 ? spaceSize : font.MeasureString(currentWord) * textScale; + if (size.X > lineLength) { - if (linePos == 0.0f) + float splitSize = 0.0f; + List splitWord = new List() { string.Empty }; + int k = 0; + + for (int j = 0; j < currentWord.Length; j++) { - wrappedText.AppendLine(words[i]); - } - else - { - do + splitWord[k] += currentWord[j]; + splitSize += (font.MeasureString(currentWord[j].ToString()) * textScale).X; + + if (splitSize + linePos > lineLength) { - if (words[i].Length == 0) break; - - wrappedText.Append(words[i][0]); - words[i] = words[i].Remove(0, 1); - - linePos += size.X; - } while (words[i].Length > 0 && (size = font.MeasureString((words[i][0]).ToString()) * textScale).X + linePos < lineLength); - - wrappedText.Append("\n"); - linePos = 0.0f; - i--; + linePos = splitSize = 0.0f; + splitWord[k] = splitWord[k].Remove(splitWord[k].Length - 1) + "\n"; + j--; + splitWord.Add(string.Empty); + k++; + } } - continue; - } - - if (linePos + size.X < lineLength) - { - wrappedText.Append(words[i]); - if (words[i] == "\n") + for (int j = 0; j < splitWord.Count; j++) { - linePos = 0.0f; - } - else - { - linePos += size.X + spaceSize.X; + wrappedText.Append(splitWord[j]); } + + linePos = splitSize; } else { - wrappedText.Append("\n"); - wrappedText.Append(words[i]); + if (linePos + size.X < lineLength) + { + wrappedText.Append(currentWord); + if (currentWord == "\n") + { + linePos = 0.0f; + } + else + { + linePos += size.X; + } + } + else + { + wrappedText.Append("\n"); + wrappedText.Append(currentWord); - linePos = size.X + spaceSize.X; - } - - if (i < words.Count - 1 && !TextManager.IsCJK(words[i]) && !TextManager.IsCJK(words[i + 1])) - { - wrappedText.Append(" "); + linePos = size.X; + } } } - return wrappedText.ToString().Replace(" \n ", "\n"); - } + if (!playerInput) + { + return wrappedText.ToString().Replace(" \n ", "\n"); + } + else + { + return wrappedText.ToString(); + } + } + + public static void ParseConnectCommand(string[] args, out string name, out string endpoint, out UInt64 lobbyId) + { + name = null; endpoint = null; lobbyId = 0; + for (int i = 0; i < args.Length - 1; i++) + { + if (i < args.Length-2 && args[i].Trim().ToLower().Equals("-connect", StringComparison.InvariantCultureIgnoreCase)) + { + int j = i + 2; + + name = ""; + if (args[i + 1].Trim()[0] == '"') + { + name = args[i + 1].Trim().Substring(1); + if (!(name[name.Length - 1] == '"' && (name.Length < 2 || name[name.Length - 1] != '\\'))) + { + for (; j < args.Length - 1; j++) + { + name += " " + args[j].Trim(); + if (name[name.Length - 1] == '"' && (name.Length < 2 || name[name.Length - 1] != '\\')) + { + name = name.Substring(0, name.Length - 1).Replace("\\\"", "\""); + j++; + break; + } + } + } + else + { + name = name.Substring(0, name.Length - 1); + } + } + else + { + name = args[i + 1].Trim(); + } + endpoint = args[j].Trim(); + + break; + } + else if (args[i].Trim().ToLower().Equals("+connect_lobby", StringComparison.InvariantCultureIgnoreCase)) + { + UInt64.TryParse(args[i + 1].Trim(), out lobbyId); + endpoint = null; + name = null; + break; + } + } + } } } diff --git a/Barotrauma/BarotraumaServer/Properties/AssemblyInfo.cs b/Barotrauma/BarotraumaServer/Properties/AssemblyInfo.cs index 4e3b8e5fc..ea94e4035 100644 --- a/Barotrauma/BarotraumaServer/Properties/AssemblyInfo.cs +++ b/Barotrauma/BarotraumaServer/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.9.200.0")] -[assembly: AssemblyFileVersion("0.9.1.0")] +[assembly: AssemblyVersion("0.9.2.1")] +[assembly: AssemblyFileVersion("0.9.2.1")] diff --git a/Barotrauma/BarotraumaServer/Server.csproj b/Barotrauma/BarotraumaServer/Server.csproj index 85abb98f3..fc904e4f6 100644 --- a/Barotrauma/BarotraumaServer/Server.csproj +++ b/Barotrauma/BarotraumaServer/Server.csproj @@ -181,11 +181,13 @@ + + + - @@ -211,11 +213,13 @@ - + + + @@ -227,6 +231,9 @@ + + + @@ -237,6 +244,21 @@ + + + + + + + + + + + + + + + @@ -271,9 +293,6 @@ - - - PreserveNewest @@ -287,15 +306,15 @@ PreserveNewest + + + + + + - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/Source/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaServer/Source/Characters/CharacterInfo.cs index 8a75d42e0..467a4fe13 100644 --- a/Barotrauma/BarotraumaServer/Source/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaServer/Source/Characters/CharacterInfo.cs @@ -1,10 +1,10 @@ -using Lidgren.Network; +using Barotrauma.Networking; namespace Barotrauma { partial class CharacterInfo { - public void ServerWrite(NetBuffer msg) + public void ServerWrite(IWriteMessage msg) { msg.Write(ID); msg.Write(Name); diff --git a/Barotrauma/BarotraumaServer/Source/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/Source/Characters/CharacterNetworking.cs index 9cd263d20..bdf17c1b5 100644 --- a/Barotrauma/BarotraumaServer/Source/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/Source/Characters/CharacterNetworking.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -8,7 +7,7 @@ namespace Barotrauma { partial class Character { - public string OwnerClientIP; + public string OwnerClientEndPoint; public string OwnerClientName; public bool ClientDisconnected; public float KillDisconnectedTimer; @@ -138,7 +137,7 @@ namespace Barotrauma } } - public virtual void ServerRead(ClientNetObject type, NetBuffer msg, Client c) + public virtual void ServerRead(ClientNetObject type, IReadMessage msg, Client c) { if (GameMain.Server == null) return; @@ -255,7 +254,7 @@ namespace Barotrauma msg.ReadPadBits(); } - public virtual void ServerWrite(NetBuffer msg, Client c, object[] extraData = null) + public virtual void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { if (GameMain.Server == null) return; @@ -264,20 +263,20 @@ namespace Barotrauma switch ((NetEntityEvent.Type)extraData[0]) { case NetEntityEvent.Type.InventoryState: - msg.WriteRangedInteger(0, 3, 0); + msg.WriteRangedIntegerDeprecated(0, 3, 0); Inventory.SharedWrite(msg, extraData); break; case NetEntityEvent.Type.Control: - msg.WriteRangedInteger(0, 3, 1); + msg.WriteRangedIntegerDeprecated(0, 3, 1); Client owner = ((Client)extraData[1]); msg.Write(owner == null ? (byte)0 : owner.ID); break; case NetEntityEvent.Type.Status: - msg.WriteRangedInteger(0, 3, 2); + msg.WriteRangedIntegerDeprecated(0, 3, 2); WriteStatus(msg); break; case NetEntityEvent.Type.UpdateSkills: - msg.WriteRangedInteger(0, 3, 3); + msg.WriteRangedIntegerDeprecated(0, 3, 3); if (Info?.Job == null) { msg.Write((byte)0); @@ -302,7 +301,7 @@ namespace Barotrauma { msg.Write(ID); - NetBuffer tempBuffer = new NetBuffer(); + IWriteMessage tempBuffer = new WriteOnlyMessage(); if (this == c.Character) { @@ -383,7 +382,7 @@ namespace Barotrauma tempBuffer.WriteRangedSingle(AnimController.Collider.LinearVelocity.X, -MaxVel, MaxVel, 12); tempBuffer.WriteRangedSingle(AnimController.Collider.LinearVelocity.Y, -MaxVel, MaxVel, 12); - bool fixedRotation = AnimController.Collider.FarseerBody.FixedRotation; + bool fixedRotation = AnimController.Collider.FarseerBody.FixedRotation || !AnimController.Collider.PhysEnabled; tempBuffer.Write(fixedRotation); if (!fixedRotation) { @@ -403,19 +402,19 @@ namespace Barotrauma tempBuffer.WritePadBits(); msg.Write((byte)tempBuffer.LengthBytes); - msg.Write(tempBuffer); + msg.Write(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); } } - private void WriteStatus(NetBuffer msg) + private void WriteStatus(IWriteMessage msg) { msg.Write(IsDead); if (IsDead) { - msg.WriteRangedInteger(0, Enum.GetValues(typeof(CauseOfDeathType)).Length - 1, (int)CauseOfDeath.Type); + msg.WriteRangedIntegerDeprecated(0, Enum.GetValues(typeof(CauseOfDeathType)).Length - 1, (int)CauseOfDeath.Type); if (CauseOfDeath.Type == CauseOfDeathType.Affliction) { - msg.WriteRangedInteger(0, AfflictionPrefab.List.Count - 1, AfflictionPrefab.List.IndexOf(CauseOfDeath.Affliction)); + msg.WriteRangedIntegerDeprecated(0, AfflictionPrefab.List.Count - 1, AfflictionPrefab.List.IndexOf(CauseOfDeath.Affliction)); } if (AnimController?.LimbJoints == null) @@ -446,7 +445,7 @@ namespace Barotrauma } } - public void WriteSpawnData(NetBuffer msg) + public void WriteSpawnData(IWriteMessage msg) { if (GameMain.Server == null) return; @@ -497,4 +496,4 @@ namespace Barotrauma DebugConsole.Log("Character spawn message length: " + (msg.LengthBytes - msgLength)); } } -} \ No newline at end of file +} diff --git a/Barotrauma/BarotraumaServer/Source/DebugConsole.cs b/Barotrauma/BarotraumaServer/Source/DebugConsole.cs index b4f62239e..c68552dc6 100644 --- a/Barotrauma/BarotraumaServer/Source/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/Source/DebugConsole.cs @@ -7,6 +7,8 @@ using System.ComponentModel; using FarseerPhysics; using Barotrauma.Items.Components; using System.Threading; +using System.IO; +using System.Text; namespace Barotrauma { @@ -49,6 +51,7 @@ namespace Barotrauma } public static List QueuedCommands = new List(); + public static Thread InputThread; public static void Update() { @@ -60,6 +63,30 @@ namespace Barotrauma QueuedCommands.RemoveAt(0); } } + if (InputThread == null) + { + lock (queuedMessages) + { + while (queuedMessages.Count > 0) + { + var msg = queuedMessages.Dequeue(); + Messages.Add(msg); + if (GameSettings.SaveDebugConsoleLogs) + { + unsavedMessages.Add(msg); + if (unsavedMessages.Count >= messagesPerFile) + { + SaveLogs(); + unsavedMessages.Clear(); + } + } + } + if (Messages.Count > MaxMessages) + { + Messages.RemoveRange(0, Messages.Count - MaxMessages); + } + } + } } @@ -91,6 +118,7 @@ namespace Barotrauma while (queuedMessages.Count > 0) { ColoredText msg = queuedMessages.Dequeue(); + Messages.Add(msg); if (GameSettings.SaveDebugConsoleLogs) { unsavedMessages.Add(msg); @@ -113,6 +141,10 @@ namespace Barotrauma } RewriteInputToCommandLine(input); } + if (Messages.Count > MaxMessages) + { + Messages.RemoveRange(0, Messages.Count - MaxMessages); + } } //read player input @@ -185,6 +217,25 @@ namespace Barotrauma { //don't have anything to do here yet } +#if !DEBUG + catch (Exception exception) + { + StreamWriter sw = new StreamWriter("inputthreadcrash.log"); + + StringBuilder sb = new StringBuilder(); + sb.AppendLine("Barotrauma Dedicated Server input thread crash report (generated on " + DateTime.Now + ")"); + sb.AppendLine("\n"); + sb.AppendLine("Exception: " + exception.Message); + sb.AppendLine("Target site: " + exception.TargetSite.ToString()); + sb.AppendLine("Stack trace: "); + sb.AppendLine(exception.StackTrace); + + sw.WriteLine(sb.ToString()); + sw.Close(); + + GameMain.ShouldRun = false; + } +#endif } private static void RewriteInputToCommandLine(string input) @@ -692,11 +743,11 @@ namespace Barotrauma NewMessage(GameMain.Server.KarmaManager.TestMode ? "Karma test mode enabled." : "Karma test mode disabled.", Color.LightGreen); }); - AssignOnExecute("banip", (string[] args) => + AssignOnExecute("banendpoint", (string[] args) => { if (GameMain.Server == null || args.Length == 0) return; - ShowQuestionPrompt("Reason for banning the ip \"" + args[0] + "\"?", (reason) => + ShowQuestionPrompt("Reason for banning the endpoint \"" + args[0] + "\"?", (reason) => { ShowQuestionPrompt("Enter the duration of the ban (leave empty to ban permanently, or use the format \"[days] d [hours] h\")", (duration) => { @@ -711,7 +762,7 @@ namespace Barotrauma banDuration = parsedBanDuration; } - var clients = GameMain.Server.ConnectedClients.FindAll(c => c.IPMatches(args[0])); + var clients = GameMain.Server.ConnectedClients.FindAll(c => c.EndpointMatches(args[0])); if (clients.Count == 0) { GameMain.Server.ServerSettings.BanList.BanPlayer("Unnamed", args[0], reason, banDuration); @@ -816,7 +867,7 @@ namespace Barotrauma NewMessage("***************", Color.Cyan); foreach (Client c in GameMain.Server.ConnectedClients) { - NewMessage("- " + c.ID.ToString() + ": " + c.Name + (c.Character != null ? " playing " + c.Character.LogName : "") + ", " + c.Connection.RemoteEndPoint.Address.ToString(), Color.Cyan); + NewMessage("- " + c.ID.ToString() + ": " + c.Name + (c.Character != null ? " playing " + c.Character.LogName : "") + ", " + c.Connection.EndPointString, Color.Cyan); } NewMessage("***************", Color.Cyan); })); @@ -825,7 +876,7 @@ namespace Barotrauma GameMain.Server.SendConsoleMessage("***************", client); foreach (Client c in GameMain.Server.ConnectedClients) { - GameMain.Server.SendConsoleMessage("- " + c.ID.ToString() + ": " + c.Name + ", " + c.Connection.RemoteEndPoint.Address.ToString(), client); + GameMain.Server.SendConsoleMessage("- " + c.ID.ToString() + ": " + c.Name + ", " + c.Connection.EndPointString, client); } GameMain.Server.SendConsoleMessage("***************", client); }); @@ -865,22 +916,51 @@ namespace Barotrauma { if (GameMain.Server == null) return; TraitorManager traitorManager = GameMain.Server.TraitorManager; - if (traitorManager == null) return; - foreach (Traitor t in traitorManager.TraitorList) + if (traitorManager == null || traitorManager.Traitors == null || !traitorManager.Traitors.Any()) { - NewMessage("- Traitor " + t.Character.Name + "'s target is " + t.TargetCharacter.Name + ".", Color.Cyan); + NewMessage("There are no traitors at the moment.", Color.Cyan); + return; } - NewMessage("The code words are: " + traitorManager.codeWords + ", response: " + traitorManager.codeResponse + ".", Color.Cyan); + foreach (Traitor t in traitorManager.Traitors) + { + if (t.CurrentObjective != null) + { + NewMessage(string.Format("- Traitor {0}'s current goals are:\n{1}", t.Character.Name, t.CurrentObjective.GoalInfos), Color.Cyan); + } + else + { + NewMessage(string.Format("- Traitor {0} has no current objective.", t.Character.Name), Color.Cyan); + } + } + //NewMessage("The code words are: " + traitorManager.CodeWords + ", response: " + traitorManager.CodeResponse + ".", Color.Cyan); })); AssignOnClientRequestExecute("traitorlist", (Client client, Vector2 cursorPos, string[] args) => { TraitorManager traitorManager = GameMain.Server.TraitorManager; - if (traitorManager == null) return; - foreach (Traitor t in traitorManager.TraitorList) + if (traitorManager == null || traitorManager.Traitors == null || !traitorManager.Traitors.Any()) { - GameMain.Server.SendConsoleMessage("- Traitor " + t.Character.Name + "'s target is " + t.TargetCharacter.Name + ".", client); + GameMain.Server.SendTraitorMessage(client,"There are no traitors at the moment.", TraitorMessageType.Console); + return; } - GameMain.Server.SendConsoleMessage("The code words are: " + traitorManager.codeWords + ", response: " + traitorManager.codeResponse + ".", client); + foreach (Traitor t in traitorManager.Traitors) + { + if (t.CurrentObjective != null) + { + var traitorGoals = TextManager.FormatServerMessage(t.CurrentObjective.GoalInfos); + var traitorGoalsStart = traitorGoals.LastIndexOf('/') + 1; + GameMain.Server.SendTraitorMessage(client, string.Join("/", new[] { + traitorGoals.Substring(0, traitorGoalsStart), + $"[traitorgoals]={traitorGoals.Substring(traitorGoalsStart)}", + $"[traitorname]={t.Character.Name}", + "Traitor [traitorname]'s current goals are:\n[traitorgoals]" + }.Where(s => !string.IsNullOrEmpty(s))), TraitorMessageType.Console); + } + else + { + GameMain.Server.SendTraitorMessage(client, string.Format("- Traitor {0} has no current objective.", t.Character.Name), TraitorMessageType.Console); + } + } + //GameMain.Server.SendTraitorMessage(client, "The code words are: " + traitorManager.CodeWords + ", response: " + traitorManager.CodeResponse + ".", TraitorMessageType.Console); }); commands.Add(new Command("setpassword|setserverpassword|password", "setpassword [password]: Changes the password of the server that's being hosted.", (string[] args) => @@ -930,6 +1010,7 @@ namespace Barotrauma NewMessage("*****************", Color.Lime); NewMessage("RESTARTING SERVER", Color.Lime); NewMessage("*****************", Color.Lime); + GameServer.Log("Console command \"restart\" executed: closing the server...", ServerLog.MessageType.ServerMessage); GameMain.Instance.CloseServer(); GameMain.Instance.StartServer(); })); @@ -939,18 +1020,36 @@ namespace Barotrauma GameMain.ShouldRun = false; })); - commands.Add(new Command("say", "say [message]: Send a chat message that displays \"HOST\" as the sender.", (string[] args) => + commands.Add(new Command("say", "say [message]: Send a global chat message. When issued through the server command line, displays \"HOST\" as the sender.", (string[] args) => { string text = string.Join(" ", args); text = "HOST: " + text; GameMain.Server.SendChatMessage(text, ChatMessageType.Server); })); + AssignOnClientRequestExecute("say", + (Client client, Vector2 cursorPos, string[] args) => + { + string text = string.Join(" ", args); + text = client.Name+": " + text; + if (GameMain.Server.OwnerConnection != null && + client.Connection == GameMain.Server.OwnerConnection) + { + text = "[HOST] " + text; + } + GameMain.Server.SendChatMessage(text, ChatMessageType.Server); + }); commands.Add(new Command("msg", "msg [message]: Send a chat message with no sender specified.", (string[] args) => { string text = string.Join(" ", args); GameMain.Server.SendChatMessage(text, ChatMessageType.Server); })); + AssignOnClientRequestExecute("msg", + (Client client, Vector2 cursorPos, string[] args) => + { + string text = string.Join(" ", args); + GameMain.Server.SendChatMessage(text, ChatMessageType.Server); + }); commands.Add(new Command("servername", "servername [name]: Change the name of the server.", (string[] args) => { @@ -1117,6 +1216,11 @@ namespace Barotrauma })); #if DEBUG + commands.Add(new Command("printsendertransfers", "", (string[] args) => + { + GameMain.Server.PrintSenderTransters(); + })); + commands.Add(new Command("eventdata", "", (string[] args) => { if (args.Length == 0) return; @@ -1154,11 +1258,11 @@ namespace Barotrauma ); AssignOnClientRequestExecute( - "banip", + "banendpoint|banip", (Client client, Vector2 cursorPos, string[] args) => { if (args.Length < 1) return; - var clients = GameMain.Server.ConnectedClients.FindAll(c => c.IPMatches(args[0])); + var clients = GameMain.Server.ConnectedClients.FindAll(c => c.EndpointMatches(args[0])); TimeSpan? duration = null; if (args.Length > 1) { diff --git a/Barotrauma/BarotraumaServer/Source/Events/Missions/Mission.cs b/Barotrauma/BarotraumaServer/Source/Events/Missions/Mission.cs new file mode 100644 index 000000000..5922fb30c --- /dev/null +++ b/Barotrauma/BarotraumaServer/Source/Events/Missions/Mission.cs @@ -0,0 +1,17 @@ +using Barotrauma.Networking; + +namespace Barotrauma +{ + partial class Mission + { + partial void ShowMessageProjSpecific(int index) + { + if (index >= Headers.Count && index >= Messages.Count) return; + + string header = index < Headers.Count ? Headers[index] : ""; + string message = index < Messages.Count ? Messages[index] : ""; + + GameServer.Log(TextManager.Get("MissionInfo") + ": " + header + " - " + message, ServerLog.MessageType.ServerMessage); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/Source/GameMain.cs b/Barotrauma/BarotraumaServer/Source/GameMain.cs index 740581cda..e00a75a9e 100644 --- a/Barotrauma/BarotraumaServer/Source/GameMain.cs +++ b/Barotrauma/BarotraumaServer/Source/GameMain.cs @@ -92,6 +92,7 @@ namespace Barotrauma public void Init() { MissionPrefab.Init(); + TraitorMissionPrefab.Init(); MapEntityPrefab.Init(); MapGenerationParams.Init(); LevelGenerationParams.LoadPresets(); @@ -169,6 +170,7 @@ namespace Barotrauma bool enableUpnp = false; int maxPlayers = 10; int ownerKey = 0; + UInt64 steamId = 0; XDocument doc = XMLExtensions.TryLoadXml(ServerSettings.SettingsFile); if (doc?.Root == null) @@ -224,6 +226,10 @@ namespace Barotrauma int.TryParse(CommandLineArgs[i + 1], out ownerKey); i++; break; + case "-steamid": + UInt64.TryParse(CommandLineArgs[i + 1], out steamId); + i++; + break; } } @@ -235,12 +241,14 @@ namespace Barotrauma password, enableUpnp, maxPlayers, - ownerKey); + ownerKey, + steamId); } public void CloseServer() { - Server.Disconnect(); + Server?.Disconnect(); + ShouldRun = false; Server = null; } @@ -279,8 +287,9 @@ namespace Barotrauma while (Timing.Accumulator >= Timing.Step) { DebugConsole.Update(); - if (Screen.Selected != null) Screen.Selected.Update((float)Timing.Step); + Screen.Selected?.Update((float)Timing.Step); Server.Update((float)Timing.Step); + if (Server == null) { break; } SteamManager.Update((float)Timing.Step); CoroutineManager.Update((float)Timing.Step, (float)Timing.Step); diff --git a/Barotrauma/BarotraumaServer/Source/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaServer/Source/GameSession/GameModes/CampaignMode.cs new file mode 100644 index 000000000..90e65ad86 --- /dev/null +++ b/Barotrauma/BarotraumaServer/Source/GameSession/GameModes/CampaignMode.cs @@ -0,0 +1,15 @@ +using Barotrauma.Networking; + +namespace Barotrauma +{ + abstract partial class CampaignMode : GameMode + { + public override void ShowStartMessage() + { + if (Mission == null) return; + + Networking.GameServer.Log(TextManager.Get("Mission") + ": " + Mission.Name, Networking.ServerLog.MessageType.ServerMessage); + Networking.GameServer.Log(Mission.Description, Networking.ServerLog.MessageType.ServerMessage); + } + } +} diff --git a/Barotrauma/BarotraumaServer/Source/GameSession/GameModes/CharacterCampaignData.cs b/Barotrauma/BarotraumaServer/Source/GameSession/GameModes/CharacterCampaignData.cs index de82f0127..960917301 100644 --- a/Barotrauma/BarotraumaServer/Source/GameSession/GameModes/CharacterCampaignData.cs +++ b/Barotrauma/BarotraumaServer/Source/GameSession/GameModes/CharacterCampaignData.cs @@ -4,9 +4,11 @@ namespace Barotrauma { partial class CharacterCampaignData { + public bool HasSpawned; + partial void InitProjSpecific(Client client) { - ClientIP = client.Connection.RemoteEndPoint.Address.ToString(); + ClientEndPoint = client.Connection.EndPointString; SteamID = client.SteamID; CharacterInfo = client.CharacterInfo; } @@ -19,7 +21,7 @@ namespace Barotrauma } else { - return ClientIP == client.Connection.RemoteEndPoint.Address.ToString(); + return ClientEndPoint == client.Connection.EndPointString; } } diff --git a/Barotrauma/BarotraumaServer/Source/GameSession/GameModes/MissionMode.cs b/Barotrauma/BarotraumaServer/Source/GameSession/GameModes/MissionMode.cs new file mode 100644 index 000000000..81fd38fb4 --- /dev/null +++ b/Barotrauma/BarotraumaServer/Source/GameSession/GameModes/MissionMode.cs @@ -0,0 +1,13 @@ +namespace Barotrauma +{ + partial class MissionMode : GameMode + { + public override void ShowStartMessage() + { + if (mission == null) return; + + Networking.GameServer.Log(TextManager.Get("Mission") + ": " + mission.Name, Networking.ServerLog.MessageType.ServerMessage); + Networking.GameServer.Log(mission.Description, Networking.ServerLog.MessageType.ServerMessage); + } + } +} diff --git a/Barotrauma/BarotraumaServer/Source/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/Source/GameSession/GameModes/MultiPlayerCampaign.cs index 35fbb7ea0..ca74513f9 100644 --- a/Barotrauma/BarotraumaServer/Source/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/Source/GameSession/GameModes/MultiPlayerCampaign.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -153,7 +152,7 @@ namespace Barotrauma return assignedJobs; } - public void ServerWrite(NetBuffer msg, Client c) + public void ServerWrite(IWriteMessage msg, Client c) { System.Diagnostics.Debug.Assert(map.Locations.Count < UInt16.MaxValue); @@ -191,7 +190,7 @@ namespace Barotrauma } } - public void ServerRead(NetBuffer msg, Client sender) + public void ServerRead(IReadMessage msg, Client sender) { UInt16 selectedLocIndex = msg.ReadUInt16(); byte selectedMissionIndex = msg.ReadByte(); diff --git a/Barotrauma/BarotraumaServer/Source/Items/Components/Door.cs b/Barotrauma/BarotraumaServer/Source/Items/Components/Door.cs index 0dd7a2a65..bfa85f702 100644 --- a/Barotrauma/BarotraumaServer/Source/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaServer/Source/Items/Components/Door.cs @@ -22,7 +22,7 @@ namespace Barotrauma.Items.Components } } - public override void ServerWrite(Lidgren.Network.NetBuffer msg, Client c, object[] extraData = null) + public override void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { base.ServerWrite(msg, c, extraData); diff --git a/Barotrauma/BarotraumaServer/Source/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaServer/Source/Items/Components/Machines/Deconstructor.cs index 3d3aa6f4b..49db8e5ba 100644 --- a/Barotrauma/BarotraumaServer/Source/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaServer/Source/Items/Components/Machines/Deconstructor.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using Lidgren.Network; using System.Linq; using System.Xml.Linq; @@ -7,7 +6,7 @@ namespace Barotrauma.Items.Components { partial class Deconstructor : Powered, IServerSerializable, IClientSerializable { - public void ServerRead(ClientNetObject type, NetBuffer msg, Client c) + public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) { bool active = msg.ReadBoolean(); @@ -19,7 +18,7 @@ namespace Barotrauma.Items.Components } } - public void ServerWrite(NetBuffer msg, Client c, object[] extraData = null) + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { msg.Write(IsActive); msg.Write(progressTimer); diff --git a/Barotrauma/BarotraumaServer/Source/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaServer/Source/Items/Components/Machines/Engine.cs index da691fd3a..a5c5697c7 100644 --- a/Barotrauma/BarotraumaServer/Source/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaServer/Source/Items/Components/Machines/Engine.cs @@ -3,19 +3,18 @@ using System; using System.Globalization; using System.Xml.Linq; using Barotrauma.Networking; -using Lidgren.Network; namespace Barotrauma.Items.Components { partial class Engine : Powered, IServerSerializable, IClientSerializable { - public void ServerWrite(NetBuffer msg, Client c, object[] extraData = null) + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { //force can only be adjusted at 10% intervals -> no need for more accuracy than this - msg.WriteRangedInteger(-10, 10, (int)(targetForce / 10.0f)); + msg.WriteRangedIntegerDeprecated(-10, 10, (int)(targetForce / 10.0f)); } - public void ServerRead(ClientNetObject type, NetBuffer msg, Client c) + public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) { float newTargetForce = msg.ReadRangedInteger(-10, 10) * 10.0f; diff --git a/Barotrauma/BarotraumaServer/Source/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaServer/Source/Items/Components/Machines/Fabricator.cs index 9203d533a..f5f6f4bdb 100644 --- a/Barotrauma/BarotraumaServer/Source/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaServer/Source/Items/Components/Machines/Fabricator.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -10,7 +9,7 @@ namespace Barotrauma.Items.Components { partial class Fabricator : Powered, IServerSerializable, IClientSerializable { - public void ServerRead(ClientNetObject type, NetBuffer msg, Client c) + public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) { int itemIndex = msg.ReadRangedInteger(-1, fabricationRecipes.Count - 1); @@ -32,10 +31,10 @@ namespace Barotrauma.Items.Components } } - public void ServerWrite(NetBuffer msg, Client c, object[] extraData = null) + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { int itemIndex = fabricatedItem == null ? -1 : fabricationRecipes.IndexOf(fabricatedItem); - msg.WriteRangedInteger(-1, fabricationRecipes.Count - 1, itemIndex); + msg.WriteRangedIntegerDeprecated(-1, fabricationRecipes.Count - 1, itemIndex); UInt16 userID = fabricatedItem == null || user == null ? (UInt16)0 : user.ID; msg.Write(userID); } diff --git a/Barotrauma/BarotraumaServer/Source/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaServer/Source/Items/Components/Machines/Pump.cs index abc219b61..f5f114a84 100644 --- a/Barotrauma/BarotraumaServer/Source/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaServer/Source/Items/Components/Machines/Pump.cs @@ -8,7 +8,7 @@ namespace Barotrauma.Items.Components { partial class Pump : Powered, IServerSerializable, IClientSerializable { - public void ServerRead(ClientNetObject type, Lidgren.Network.NetBuffer msg, Client c) + public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) { float newFlowPercentage = msg.ReadRangedInteger(-10, 10) * 10.0f; bool newIsActive = msg.ReadBoolean(); @@ -32,10 +32,10 @@ namespace Barotrauma.Items.Components item.CreateServerEvent(this); } - public void ServerWrite(Lidgren.Network.NetBuffer msg, Client c, object[] extraData = null) + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { //flowpercentage can only be adjusted at 10% intervals -> no need for more accuracy than this - msg.WriteRangedInteger(-10, 10, (int)(flowPercentage / 10.0f)); + msg.WriteRangedIntegerDeprecated(-10, 10, (int)(flowPercentage / 10.0f)); msg.Write(IsActive); } } diff --git a/Barotrauma/BarotraumaServer/Source/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaServer/Source/Items/Components/Machines/Reactor.cs index 5d6439226..1fdfb5d1f 100644 --- a/Barotrauma/BarotraumaServer/Source/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaServer/Source/Items/Components/Machines/Reactor.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using Lidgren.Network; using System; namespace Barotrauma.Items.Components @@ -11,7 +10,7 @@ namespace Barotrauma.Items.Components private float? nextServerLogWriteTime; private float lastServerLogWriteTime; - public void ServerRead(ClientNetObject type, NetBuffer msg, Client c) + public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) { bool autoTemp = msg.ReadBoolean(); bool shutDown = msg.ReadBoolean(); @@ -42,7 +41,7 @@ namespace Barotrauma.Items.Components unsentChanges = true; } - public void ServerWrite(NetBuffer msg, Client c, object[] extraData = null) + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { msg.Write(autoTemp); msg.Write(shutDown); diff --git a/Barotrauma/BarotraumaServer/Source/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaServer/Source/Items/Components/Power/PowerContainer.cs index 5815f7929..76fbd282e 100644 --- a/Barotrauma/BarotraumaServer/Source/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaServer/Source/Items/Components/Power/PowerContainer.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Xml.Linq; @@ -8,7 +7,7 @@ namespace Barotrauma.Items.Components { partial class PowerContainer : Powered, IDrawableComponent, IServerSerializable, IClientSerializable { - public void ServerRead(ClientNetObject type, NetBuffer msg, Client c) + public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) { float newRechargeSpeed = msg.ReadRangedInteger(0, 10) / 10.0f * maxRechargeSpeed; @@ -21,9 +20,9 @@ namespace Barotrauma.Items.Components item.CreateServerEvent(this); } - public void ServerWrite(NetBuffer msg, Client c, object[] extraData = null) + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { - msg.WriteRangedInteger(0, 10, (int)(rechargeSpeed / MaxRechargeSpeed * 10)); + msg.WriteRangedIntegerDeprecated(0, 10, (int)(rechargeSpeed / MaxRechargeSpeed * 10)); float chargeRatio = MathHelper.Clamp(charge / capacity, 0.0f, 1.0f); msg.WriteRangedSingle(chargeRatio, 0.0f, 1.0f, 8); diff --git a/Barotrauma/BarotraumaServer/Source/Items/Components/Repairable.cs b/Barotrauma/BarotraumaServer/Source/Items/Components/Repairable.cs index e0421a39f..443638156 100644 --- a/Barotrauma/BarotraumaServer/Source/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaServer/Source/Items/Components/Repairable.cs @@ -1,19 +1,45 @@ using Barotrauma.Networking; -using Lidgren.Network; namespace Barotrauma.Items.Components { partial class Repairable : ItemComponent, IServerSerializable, IClientSerializable { - public void ServerRead(ClientNetObject type, NetBuffer msg, Client c) + void InitProjSpecific() { - if (c.Character == null) return; - StartRepairing(c.Character); + //let the clients know the initial deterioration delay + item.CreateServerEvent(this); } - public void ServerWrite(NetBuffer msg, Client c, object[] extraData = null) + public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + { + if (c.Character == null) return; + var requestedFixAction = (FixActions)msg.ReadRangedInteger(0, 2); + if (requestedFixAction != FixActions.None) + { + if (!c.Character.IsTraitor && requestedFixAction == FixActions.Sabotage) + { + if (GameSettings.VerboseLogging) + { + DebugConsole.Log($"Non traitor \"{c.Character.Name}\" attempted to sabotage item."); + } + requestedFixAction = FixActions.Repair; + } + + if (CurrentFixer == null || CurrentFixer == c.Character && requestedFixAction != currentFixerAction) + { + StartRepairing(c.Character, requestedFixAction); + item.CreateServerEvent(this); + } + } + } + + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { msg.Write(deteriorationTimer); + msg.Write(deteriorateAlwaysResetTimer); + msg.Write(DeteriorateAlways); + msg.Write(CurrentFixer == c.Character); + msg.WriteRangedInteger((int)currentFixerAction, 0, 2); } } } diff --git a/Barotrauma/BarotraumaServer/Source/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaServer/Source/Items/Components/Signal/ConnectionPanel.cs index cb6f58536..75655d62e 100644 --- a/Barotrauma/BarotraumaServer/Source/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaServer/Source/Items/Components/Signal/ConnectionPanel.cs @@ -1,6 +1,5 @@ using Barotrauma.Networking; using FarseerPhysics; -using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -11,7 +10,7 @@ namespace Barotrauma.Items.Components { partial class ConnectionPanel : ItemComponent, IServerSerializable, IClientSerializable { - public void ServerRead(ClientNetObject type, NetBuffer msg, Client c) + public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) { List[] wires = new List[Connections.Count]; @@ -175,9 +174,9 @@ namespace Barotrauma.Items.Components } } - public void ServerWrite(NetBuffer msg, Client c, object[] extraData = null) + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { ClientWrite(msg, extraData); } } -} \ No newline at end of file +} diff --git a/Barotrauma/BarotraumaServer/Source/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaServer/Source/Items/Components/Signal/CustomInterface.cs index 0d963239c..7786d2621 100644 --- a/Barotrauma/BarotraumaServer/Source/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaServer/Source/Items/Components/Signal/CustomInterface.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using Lidgren.Network; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; @@ -8,7 +7,7 @@ namespace Barotrauma.Items.Components { partial class CustomInterface : ItemComponent, IClientSerializable, IServerSerializable { - public void ServerRead(ClientNetObject type, NetBuffer msg, Client c) + public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) { bool[] elementStates = new bool[customInterfaceElementList.Count]; for (int i = 0; i < customInterfaceElementList.Count; i++) @@ -44,7 +43,7 @@ namespace Barotrauma.Items.Components item.CreateServerEvent(this); } - public void ServerWrite(NetBuffer msg, Client c, object[] extraData = null) + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { //extradata contains an array of buttons clicked by a client (or nothing if nothing was clicked) for (int i = 0; i < customInterfaceElementList.Count; i++) diff --git a/Barotrauma/BarotraumaServer/Source/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaServer/Source/Items/Components/Signal/Wire.cs index 192b3eb90..92730a790 100644 --- a/Barotrauma/BarotraumaServer/Source/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaServer/Source/Items/Components/Signal/Wire.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -23,14 +22,14 @@ namespace Barotrauma.Items.Components } - public void ServerWrite(NetBuffer msg, Client c, object[] extraData = null) + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { int eventIndex = (int)extraData[2]; int nodeStartIndex = eventIndex * MaxNodesPerNetworkEvent; int nodeCount = MathHelper.Clamp(nodes.Count - nodeStartIndex, 0, MaxNodesPerNetworkEvent); - msg.WriteRangedInteger(0, (int)Math.Ceiling(MaxNodeCount / (float)MaxNodesPerNetworkEvent), eventIndex); - msg.WriteRangedInteger(0, MaxNodesPerNetworkEvent, nodeCount); + msg.WriteRangedIntegerDeprecated(0, (int)Math.Ceiling(MaxNodeCount / (float)MaxNodesPerNetworkEvent), eventIndex); + msg.WriteRangedIntegerDeprecated(0, MaxNodesPerNetworkEvent, nodeCount); for (int i = nodeStartIndex; i < nodeStartIndex + nodeCount; i++) { msg.Write(nodes[i].X); diff --git a/Barotrauma/BarotraumaServer/Source/Items/Inventory.cs b/Barotrauma/BarotraumaServer/Source/Items/Inventory.cs index dde6cbbad..af6943965 100644 --- a/Barotrauma/BarotraumaServer/Source/Items/Inventory.cs +++ b/Barotrauma/BarotraumaServer/Source/Items/Inventory.cs @@ -1,6 +1,5 @@ using Barotrauma.Items.Components; using Barotrauma.Networking; -using Lidgren.Network; using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Linq; @@ -9,7 +8,7 @@ namespace Barotrauma { partial class Inventory : IServerSerializable, IClientSerializable { - public void ServerRead(ClientNetObject type, NetBuffer msg, Client c) + public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) { List prevItems = new List(Items); ushort[] newItemIDs = new ushort[capacity]; @@ -142,7 +141,7 @@ namespace Barotrauma } } - public void ServerWrite(NetBuffer msg, Client c, object[] extraData = null) + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { SharedWrite(msg, extraData); } diff --git a/Barotrauma/BarotraumaServer/Source/Items/Item.cs b/Barotrauma/BarotraumaServer/Source/Items/Item.cs index 2092cba17..2c52c7bdc 100644 --- a/Barotrauma/BarotraumaServer/Source/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/Source/Items/Item.cs @@ -1,6 +1,5 @@ using Barotrauma.Items.Components; using Barotrauma.Networking; -using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Linq; @@ -9,7 +8,7 @@ namespace Barotrauma { partial class Item : MapEntity, IDamageable, ISerializableEntity, IServerSerializable, IClientSerializable { - public void ServerWrite(NetBuffer msg, Client c, object[] extraData = null) + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { string errorMsg = ""; if (extraData == null || extraData.Length == 0 || !(extraData[0] is NetEntityEvent.Type)) @@ -26,7 +25,7 @@ namespace Barotrauma { errorMsg = "Failed to write a network event for the item \"" + Name + "\" - event type not set."; } - msg.WriteRangedInteger(0, Enum.GetValues(typeof(NetEntityEvent.Type)).Length - 1, (int)NetEntityEvent.Type.Invalid); + msg.WriteRangedIntegerDeprecated(0, Enum.GetValues(typeof(NetEntityEvent.Type)).Length - 1, (int)NetEntityEvent.Type.Invalid); DebugConsole.Log(errorMsg); GameAnalyticsManager.AddErrorEventOnce("Item.ServerWrite:InvalidData" + Name, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); return; @@ -35,7 +34,7 @@ namespace Barotrauma int initialWritePos = msg.LengthBits; NetEntityEvent.Type eventType = (NetEntityEvent.Type)extraData[0]; - msg.WriteRangedInteger(0, Enum.GetValues(typeof(NetEntityEvent.Type)).Length - 1, (int)eventType); + msg.WriteRangedIntegerDeprecated(0, Enum.GetValues(typeof(NetEntityEvent.Type)).Length - 1, (int)eventType); switch (eventType) { case NetEntityEvent.Type.ComponentState: @@ -55,7 +54,7 @@ namespace Barotrauma errorMsg = "Failed to write a component state event for the item \"" + Name + "\" - component \"" + components[componentIndex] + "\" is not server serializable."; break; } - msg.WriteRangedInteger(0, components.Count - 1, componentIndex); + msg.WriteRangedIntegerDeprecated(0, components.Count - 1, componentIndex); (components[componentIndex] as IServerSerializable).ServerWrite(msg, c, extraData); break; case NetEntityEvent.Type.InventoryState: @@ -75,7 +74,7 @@ namespace Barotrauma errorMsg = "Failed to write an inventory state event for the item \"" + Name + "\" - component \"" + components[containerIndex] + "\" is not server serializable."; break; } - msg.WriteRangedInteger(0, components.Count - 1, containerIndex); + msg.WriteRangedIntegerDeprecated(0, components.Count - 1, containerIndex); (components[containerIndex] as ItemContainer).Inventory.ServerWrite(msg, c); break; case NetEntityEvent.Type.Status: @@ -92,7 +91,7 @@ namespace Barotrauma byte targetLimbIndex = targetLimb != null && targetCharacter != null ? (byte)Array.IndexOf(targetCharacter.AnimController.Limbs, targetLimb) : (byte)255; msg.Write((byte)components.IndexOf(targetComponent)); - msg.WriteRangedInteger(0, Enum.GetValues(typeof(ActionType)).Length - 1, (int)actionType); + msg.WriteRangedIntegerDeprecated(0, Enum.GetValues(typeof(ActionType)).Length - 1, (int)actionType); msg.Write(targetID); msg.Write(targetLimbIndex); } @@ -107,7 +106,7 @@ namespace Barotrauma Character targetCharacter = FindEntityByID(targetID) as Character; byte targetLimbIndex = targetLimb != null && targetCharacter != null ? (byte)Array.IndexOf(targetCharacter.AnimController.Limbs, targetLimb) : (byte)255; - msg.WriteRangedInteger(0, Enum.GetValues(typeof(ActionType)).Length - 1, (int)actionType); + msg.WriteRangedIntegerDeprecated(0, Enum.GetValues(typeof(ActionType)).Length - 1, (int)actionType); msg.Write((byte)(targetComponent == null ? 255 : components.IndexOf(targetComponent))); msg.Write(targetID); msg.Write(targetLimbIndex); @@ -131,16 +130,16 @@ namespace Barotrauma if (!string.IsNullOrEmpty(errorMsg)) { //something went wrong - rewind the write position and write invalid event type to prevent creating an unreadable event - msg.ReadBits(msg.Data, 0, initialWritePos); + msg.BitPosition = initialWritePos; msg.LengthBits = initialWritePos; - msg.WriteRangedInteger(0, Enum.GetValues(typeof(NetEntityEvent.Type)).Length - 1, (int)NetEntityEvent.Type.Invalid); + msg.WriteRangedIntegerDeprecated(0, Enum.GetValues(typeof(NetEntityEvent.Type)).Length - 1, (int)NetEntityEvent.Type.Invalid); DebugConsole.Log(errorMsg); GameAnalyticsManager.AddErrorEventOnce("Item.ServerWrite:" + errorMsg, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); } } - public void ServerRead(ClientNetObject type, NetBuffer msg, Client c) + public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) { NetEntityEvent.Type eventType = (NetEntityEvent.Type)msg.ReadRangedInteger(0, Enum.GetValues(typeof(NetEntityEvent.Type)).Length - 1); @@ -198,7 +197,7 @@ namespace Barotrauma } } - public void WriteSpawnData(NetBuffer msg) + public void WriteSpawnData(IWriteMessage msg) { if (GameMain.Server == null) return; @@ -329,14 +328,14 @@ namespace Barotrauma } } - public void ServerWritePosition(NetBuffer msg, Client c, object[] extraData = null) + public void ServerWritePosition(IWriteMessage msg, Client c, object[] extraData = null) { msg.Write(ID); - NetBuffer tempBuffer = new NetBuffer(); + IWriteMessage tempBuffer = new WriteOnlyMessage(); body.ServerWrite(tempBuffer, c, extraData); msg.Write((byte)tempBuffer.LengthBytes); - msg.Write(tempBuffer); + msg.Write(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); msg.WritePadBits(); } diff --git a/Barotrauma/BarotraumaServer/Source/Map/Hull.cs b/Barotrauma/BarotraumaServer/Source/Map/Hull.cs index b94b26693..8f802cd06 100644 --- a/Barotrauma/BarotraumaServer/Source/Map/Hull.cs +++ b/Barotrauma/BarotraumaServer/Source/Map/Hull.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -49,7 +48,7 @@ namespace Barotrauma } } - public void ServerWrite(NetBuffer message, Client c, object[] extraData = null) + public void ServerWrite(IWriteMessage message, Client c, object[] extraData = null) { message.WriteRangedSingle(MathHelper.Clamp(waterVolume / Volume, 0.0f, 1.5f), 0.0f, 1.5f, 8); message.WriteRangedSingle(MathHelper.Clamp(OxygenPercentage, 0.0f, 100.0f), 0.0f, 100.0f, 8); @@ -57,7 +56,7 @@ namespace Barotrauma message.Write(FireSources.Count > 0); if (FireSources.Count > 0) { - message.WriteRangedInteger(0, 16, Math.Min(FireSources.Count, 16)); + message.WriteRangedIntegerDeprecated(0, 16, Math.Min(FireSources.Count, 16)); for (int i = 0; i < Math.Min(FireSources.Count, 16); i++) { var fireSource = FireSources[i]; @@ -73,7 +72,7 @@ namespace Barotrauma } //used when clients use the water/fire console commands - public void ServerRead(ClientNetObject type, NetBuffer msg, Client c) + public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) { float newWaterVolume = msg.ReadRangedSingle(0.0f, 1.5f, 8) * Volume; diff --git a/Barotrauma/BarotraumaServer/Source/Map/Structure.cs b/Barotrauma/BarotraumaServer/Source/Map/Structure.cs index bdf6d9748..4cdc37634 100644 --- a/Barotrauma/BarotraumaServer/Source/Map/Structure.cs +++ b/Barotrauma/BarotraumaServer/Source/Map/Structure.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using Lidgren.Network; namespace Barotrauma { @@ -10,8 +9,9 @@ namespace Barotrauma GameMain.Server.KarmaManager.OnStructureHealthChanged(this, attacker, damageAmount); } - public void ServerWrite(NetBuffer msg, Client c, object[] extraData = null) + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { + msg.Write((byte)Sections.Length); for (int i = 0; i < Sections.Length; i++) { msg.WriteRangedSingle(Sections[i].damage / Health, 0.0f, 1.0f, 8); diff --git a/Barotrauma/BarotraumaServer/Source/Map/Submarine.cs b/Barotrauma/BarotraumaServer/Source/Map/Submarine.cs index f96459998..4acc60be1 100644 --- a/Barotrauma/BarotraumaServer/Source/Map/Submarine.cs +++ b/Barotrauma/BarotraumaServer/Source/Map/Submarine.cs @@ -1,17 +1,16 @@ using Barotrauma.Networking; -using Lidgren.Network; namespace Barotrauma { partial class Submarine { - public void ServerWrite(NetBuffer msg, Client c, object[] extraData = null) + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { msg.Write(ID); - NetBuffer tempBuffer = new NetBuffer(); + IWriteMessage tempBuffer = new WriteOnlyMessage(); subBody.Body.ServerWrite(tempBuffer, c, extraData); msg.Write((byte)tempBuffer.LengthBytes); - msg.Write(tempBuffer); + msg.Write(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); msg.WritePadBits(); } } diff --git a/Barotrauma/BarotraumaServer/Source/Networking/BanList.cs b/Barotrauma/BarotraumaServer/Source/Networking/BanList.cs index 65551cb8a..35a48bc46 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/BanList.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/BanList.cs @@ -1,5 +1,4 @@ -using Lidgren.Network; -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -118,10 +117,23 @@ namespace Barotrauma.Networking public bool IsBanned(IPAddress IP, ulong steamID) { - bannedPlayers.RemoveAll(bp => bp.ExpirationTime.HasValue && DateTime.Now > bp.ExpirationTime.Value); + if (IPAddress.IsLoopback(IP)) { return false; } return bannedPlayers.Any(bp => bp.CompareTo(IP) || (steamID > 0 && bp.SteamID == steamID)); } + public bool IsBanned(IPAddress IP) + { + if (IPAddress.IsLoopback(IP)) { return false; } + bannedPlayers.RemoveAll(bp => bp.ExpirationTime.HasValue && DateTime.Now > bp.ExpirationTime.Value); + return bannedPlayers.Any(bp => bp.CompareTo(IP)); + } + + public bool IsBanned(ulong steamID) + { + bannedPlayers.RemoveAll(bp => bp.ExpirationTime.HasValue && DateTime.Now > bp.ExpirationTime.Value); + return bannedPlayers.Any(bp => (steamID > 0 && bp.SteamID == steamID)); + } + public void BanPlayer(string name, IPAddress ip, string reason, TimeSpan? duration) { string ipStr = ip.IsIPv4MappedToIPv6 ? ip.MapToIPv4().ToString() : ip.ToString(); @@ -262,7 +274,7 @@ namespace Barotrauma.Networking } } - public void ServerAdminWrite(NetBuffer outMsg, Client c) + public void ServerAdminWrite(IWriteMessage outMsg, Client c) { if (!c.HasPermission(ClientPermissions.Ban)) { @@ -273,7 +285,7 @@ namespace Barotrauma.Networking outMsg.Write(c.Connection == GameMain.Server.OwnerConnection); outMsg.WritePadBits(); - outMsg.WriteVariableInt32(bannedPlayers.Count); + outMsg.WriteVariableUInt32((UInt32)bannedPlayers.Count); for (int i = 0; i < bannedPlayers.Count; i++) { BannedPlayer bannedPlayer = bannedPlayers[i]; @@ -289,14 +301,14 @@ namespace Barotrauma.Networking } } - public bool ServerAdminRead(NetBuffer incMsg, Client c) + public bool ServerAdminRead(IReadMessage incMsg, Client c) { if (!c.HasPermission(ClientPermissions.Ban)) { UInt16 removeCount = incMsg.ReadUInt16(); - incMsg.Position += removeCount * 4 * 8; + incMsg.BitPosition += removeCount * 4 * 8; UInt16 rangeBanCount = incMsg.ReadUInt16(); - incMsg.Position += rangeBanCount * 4 * 8; + incMsg.BitPosition += rangeBanCount * 4 * 8; return false; } else diff --git a/Barotrauma/BarotraumaServer/Source/Networking/ChatMessage.cs b/Barotrauma/BarotraumaServer/Source/Networking/ChatMessage.cs index a063fb756..84044b2af 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/ChatMessage.cs @@ -1,5 +1,4 @@ using Barotrauma.Items.Components; -using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Linq; @@ -9,7 +8,7 @@ namespace Barotrauma.Networking { partial class ChatMessage { - public static void ServerRead(NetIncomingMessage msg, Client c) + public static void ServerRead(IReadMessage msg, Client c) { c.KickAFKTimer = 0.0f; @@ -61,22 +60,24 @@ namespace Barotrauma.Networking } float similarity = 0.0f; - //don't do message similarity checks on order messages - if (orderMsg == null) + for (int i = 0; i < c.LastSentChatMessages.Count; i++) { - for (int i = 0; i < c.LastSentChatMessages.Count; i++) + float closeFactor = 1.0f / (c.LastSentChatMessages.Count - i); + + if (string.IsNullOrEmpty(txt)) { - float closeFactor = 1.0f / (c.LastSentChatMessages.Count - i); - if (string.IsNullOrEmpty(txt)) - { - similarity += closeFactor; - } - else - { - int levenshteinDist = ToolBox.LevenshteinDistance(txt, c.LastSentChatMessages[i]); - similarity += Math.Max((txt.Length - levenshteinDist) / (float)txt.Length * closeFactor, 0.0f); - } + similarity += closeFactor; } + else + { + int levenshteinDist = ToolBox.LevenshteinDistance(txt, c.LastSentChatMessages[i]); + similarity += Math.Max((txt.Length - levenshteinDist) / (float)txt.Length * closeFactor, 0.0f); + } + } + //order/report messages can be sent a little faster than normal messages without triggering the spam filter + if (orderMsg != null) + { + similarity *= 0.25f; } bool isOwner = GameMain.Server.OwnerConnection != null && c.Connection == GameMain.Server.OwnerConnection; @@ -153,7 +154,7 @@ namespace Barotrauma.Networking return length; } - public virtual void ServerWrite(NetOutgoingMessage msg, Client c) + public virtual void ServerWrite(IWriteMessage msg, Client c) { msg.Write((byte)ServerNetObject.CHAT_MESSAGE); msg.Write(NetStateID); @@ -168,4 +169,4 @@ namespace Barotrauma.Networking } } } -} \ No newline at end of file +} diff --git a/Barotrauma/BarotraumaServer/Source/Networking/Client.cs b/Barotrauma/BarotraumaServer/Source/Networking/Client.cs index 8a91ceaef..f4f30b70b 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/Client.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/Client.cs @@ -1,5 +1,4 @@ -using Lidgren.Network; -using System; +using System; using System.Collections.Generic; using System.Linq; @@ -7,8 +6,6 @@ namespace Barotrauma.Networking { partial class Client : IDisposable { - public ulong SteamID; - public bool VoiceEnabled = true; public UInt16 LastRecvClientListUpdate = 0; @@ -61,9 +58,11 @@ namespace Barotrauma.Networking public float DeleteDisconnectedTimer; public CharacterInfo CharacterInfo; - public NetConnection Connection { get; set; } + public NetworkConnection Connection { get; set; } public bool SpectateOnly; + + public int KarmaKickCount; private float karma = 100.0f; public float Karma @@ -108,28 +107,32 @@ namespace Barotrauma.Networking NeedsMidRoundSync = false; } - public static bool IsValidName(string name, GameServer server) + public static bool IsValidName(string name, ServerSettings serverSettings) { char[] disallowedChars = new char[] { ';', ',', '<', '>', '/', '\\', '[', ']', '"', '?' }; if (name.Any(c => disallowedChars.Contains(c))) return false; foreach (char character in name) { - if (!server.ServerSettings.AllowedClientNameChars.Any(charRange => (int)character >= charRange.First && (int)character <= charRange.Second)) return false; + if (!serverSettings.AllowedClientNameChars.Any(charRange => (int)character >= charRange.First && (int)character <= charRange.Second)) return false; } return true; } - public bool IPMatches(string ip) + public bool EndpointMatches(string endpoint) { - if (Connection?.RemoteEndPoint == null) { return false; } - if (Connection.RemoteEndPoint.Address.IsIPv4MappedToIPv6 && - Connection.RemoteEndPoint.Address.MapToIPv4().ToString() == ip) + if (Connection is LidgrenConnection lidgrenConn) { - return true; + if (lidgrenConn.IPEndPoint?.Address == null) { return false; } + if ((lidgrenConn.IPEndPoint?.Address.IsIPv4MappedToIPv6 ?? false) && + lidgrenConn.IPEndPoint?.Address.MapToIPv4().ToString() == endpoint) + { + return true; + } } - return Connection.RemoteEndPoint.Address.ToString() == ip; + + return Connection.EndPointString == endpoint; } public void SetPermissions(ClientPermissions permissions, List permittedConsoleCommands) diff --git a/Barotrauma/BarotraumaServer/Source/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaServer/Source/Networking/EntitySpawner.cs index 45eb98b16..07997e710 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/EntitySpawner.cs @@ -13,7 +13,7 @@ namespace Barotrauma } } - public void ServerWrite(Lidgren.Network.NetBuffer message, Client client, object[] extraData = null) + public void ServerWrite(IWriteMessage message, Client client, object[] extraData = null) { if (GameMain.Server == null) return; diff --git a/Barotrauma/BarotraumaServer/Source/Networking/FileTransfer/FileSender.cs b/Barotrauma/BarotraumaServer/Source/Networking/FileTransfer/FileSender.cs index 88ad64ba2..d205980e9 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/FileTransfer/FileSender.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/FileTransfer/FileSender.cs @@ -1,5 +1,4 @@ -using Lidgren.Network; -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.IO; @@ -12,11 +11,11 @@ namespace Barotrauma.Networking { public class FileTransferOut { - private byte[] data; + private readonly byte[] data; - private DateTime startingTime; + private readonly DateTime startingTime; - private NetConnection connection; + private readonly NetworkConnection connection; public FileTransferStatus Status; @@ -40,7 +39,7 @@ namespace Barotrauma.Networking public float Progress { - get { return SentOffset / (float)Data.Length; } + get { return KnownReceivedOffset / (float)Data.Length; } } public float WaitTimer @@ -54,20 +53,24 @@ namespace Barotrauma.Networking get { return data; } } + public bool Acknowledged; + public int SentOffset { get; set; } - public NetConnection Connection + public int KnownReceivedOffset; + + public NetworkConnection Connection { get { return connection; } } - public int SequenceChannel; + public int ID; - public FileTransferOut(NetConnection recipient, FileTransferType fileType, string filePath) + public FileTransferOut(NetworkConnection recipient, FileTransferType fileType, string filePath) { connection = recipient; @@ -75,6 +78,10 @@ namespace Barotrauma.Networking FilePath = filePath; FileName = Path.GetFileName(filePath); + Acknowledged = false; + SentOffset = 0; + KnownReceivedOffset = 0; + Status = FileTransferStatus.NotStarted; startingTime = DateTime.Now; @@ -105,26 +112,26 @@ namespace Barotrauma.Networking public FileTransferDelegate OnStarted; public FileTransferDelegate OnEnded; - private List activeTransfers; + private readonly List activeTransfers; - private int chunkLen; + private readonly int chunkLen; - private NetPeer peer; + private readonly ServerPeer peer; public List ActiveTransfers { get { return activeTransfers; } } - public FileSender(NetworkMember networkMember) + public FileSender(ServerPeer serverPeer, int mtu) { - peer = networkMember.NetPeer; - chunkLen = peer.Configuration.MaximumTransmissionUnit - 100; + peer = serverPeer; + chunkLen = mtu - 100; activeTransfers = new List(); } - public FileTransferOut StartTransfer(NetConnection recipient, FileTransferType fileType, string filePath) + public FileTransferOut StartTransfer(NetworkConnection recipient, FileTransferType fileType, string filePath) { if (activeTransfers.Count >= MaxTransferCount) { @@ -147,11 +154,11 @@ namespace Barotrauma.Networking { transfer = new FileTransferOut(recipient, fileType, filePath) { - SequenceChannel = 1 + ID = 1 }; - while (activeTransfers.Any(t => t.Connection == recipient && t.SequenceChannel == transfer.SequenceChannel)) + while (activeTransfers.Any(t => t.Connection == recipient && t.ID == transfer.ID)) { - transfer.SequenceChannel++; + transfer.ID++; } activeTransfers.Add(transfer); } @@ -168,10 +175,10 @@ namespace Barotrauma.Networking public void Update(float deltaTime) { - activeTransfers.RemoveAll(t => t.Connection.Status != NetConnectionStatus.Connected); + activeTransfers.RemoveAll(t => t.Connection.Status != NetworkConnectionStatus.Connected); var endedTransfers = activeTransfers.FindAll(t => - t.Connection.Status != NetConnectionStatus.Connected || + t.Connection.Status != NetworkConnectionStatus.Connected || t.Status == FileTransferStatus.Finished || t.Status == FileTransferStatus.Canceled || t.Status == FileTransferStatus.Error); @@ -187,78 +194,95 @@ namespace Barotrauma.Networking transfer.WaitTimer -= deltaTime; if (transfer.WaitTimer > 0.0f) continue; - if (!transfer.Connection.CanSendImmediately(NetDeliveryMethod.ReliableOrdered, transfer.SequenceChannel)) continue; - transfer.WaitTimer = 0.05f;// 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; + IWriteMessage message; - //first message; send length, chunk length, file name etc - if (transfer.SentOffset == 0) + try { - message = peer.CreateMessage(); - message.Write((byte)ServerPacketHeader.FILE_TRANSFER); - - //if the recipient is the owner of the server (= a client running the server from the main exe) - //we don't need to send anything, the client can just read the file directly - if (transfer.Connection == GameMain.Server.OwnerConnection) + //first message; send length, file name etc + //wait for acknowledgement before sending data + if (!transfer.Acknowledged) { - message.Write((byte)FileTransferMessageType.TransferOnSameMachine); - message.Write((byte)transfer.FileType); - message.Write(transfer.FilePath); - GameMain.Server.CompressOutgoingMessage(message); - transfer.Connection.SendMessage(message, NetDeliveryMethod.ReliableOrdered, transfer.SequenceChannel); - transfer.Status = FileTransferStatus.Finished; + message = new WriteOnlyMessage(); + message.Write((byte)ServerPacketHeader.FILE_TRANSFER); + + //if the recipient is the owner of the server (= a client running the server from the main exe) + //we don't need to send anything, the client can just read the file directly + if (transfer.Connection == GameMain.Server.OwnerConnection) + { + message.Write((byte)FileTransferMessageType.TransferOnSameMachine); + message.Write((byte)transfer.ID); + message.Write((byte)transfer.FileType); + message.Write(transfer.FilePath); + peer.Send(message, transfer.Connection, DeliveryMethod.Unreliable); + transfer.Status = FileTransferStatus.Finished; + } + else + { + message.Write((byte)FileTransferMessageType.Initiate); + message.Write((byte)transfer.ID); + message.Write((byte)transfer.FileType); + //message.Write((ushort)chunkLen); + message.Write(transfer.Data.Length); + message.Write(transfer.FileName); + peer.Send(message, transfer.Connection, DeliveryMethod.Unreliable); + + 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(" ID: " + transfer.ID); + } + } return; } - else + + message = new WriteOnlyMessage(); + message.Write((byte)ServerPacketHeader.FILE_TRANSFER); + message.Write((byte)FileTransferMessageType.Data); + + message.Write((byte)transfer.ID); + message.Write(transfer.SentOffset); + + byte[] sendBytes = new byte[sendByteCount]; + Array.Copy(transfer.Data, transfer.SentOffset, sendBytes, 0, sendByteCount); + + message.Write((ushort)sendByteCount); + message.Write(sendBytes, 0, sendByteCount); + + transfer.SentOffset += sendByteCount; + if (transfer.SentOffset > transfer.KnownReceivedOffset + chunkLen * 5 || + transfer.SentOffset >= transfer.Data.Length) { - message.Write((byte)FileTransferMessageType.Initiate); - message.Write((byte)transfer.FileType); - message.Write((ushort)chunkLen); - message.Write((ulong)transfer.Data.Length); - message.Write(transfer.FileName); - GameMain.Server.CompressOutgoingMessage(message); - transfer.Connection.SendMessage(message, NetDeliveryMethod.ReliableOrdered, transfer.SequenceChannel); - - transfer.Status = FileTransferStatus.Sending; - - if (GameSettings.VerboseLogging) - { - DebugConsole.Log("Sending file transfer initiation message: "); - DebugConsole.Log(" File: " + transfer.FileName); - DebugConsole.Log(" Size: " + transfer.Data.Length); - DebugConsole.Log(" Sequence channel: " + transfer.SequenceChannel); - } + transfer.SentOffset = transfer.KnownReceivedOffset; } + + peer.Send(message, transfer.Connection, DeliveryMethod.Unreliable); } - message = peer.CreateMessage(1 + 1 + sendByteCount); - message.Write((byte)ServerPacketHeader.FILE_TRANSFER); - message.Write((byte)FileTransferMessageType.Data); - - byte[] sendBytes = new byte[sendByteCount]; - Array.Copy(transfer.Data, transfer.SentOffset, sendBytes, 0, sendByteCount); - - message.Write(sendBytes); - - GameMain.Server.CompressOutgoingMessage(message); - transfer.Connection.SendMessage(message, NetDeliveryMethod.ReliableOrdered, transfer.SequenceChannel); - transfer.SentOffset += sendByteCount; + catch (Exception e) + { + DebugConsole.ThrowError("FileSender threw an exception when trying to send data", e); + GameAnalyticsManager.AddErrorEventOnce( + "FileSender.Update:Exception", + GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + "FileSender threw an exception when trying to send data:\n" + e.Message + "\n" + e.StackTrace); + transfer.Status = FileTransferStatus.Error; + break; + } 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; - } } } @@ -272,18 +296,35 @@ namespace Barotrauma.Networking GameMain.Server.SendCancelTransferMsg(transfer); } - public void ReadFileRequest(NetIncomingMessage inc, Client client) + public void ReadFileRequest(IReadMessage inc, Client client) { 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); + byte transferId = inc.ReadByte(); + var matchingTransfer = activeTransfers.Find(t => t.Connection == inc.Sender && t.ID == transferId); if (matchingTransfer != null) CancelTransfer(matchingTransfer); return; } + else if (messageType == (byte)FileTransferMessageType.Data) + { + byte transferId = inc.ReadByte(); + var matchingTransfer = activeTransfers.Find(t => t.Connection == inc.Sender && t.ID == transferId); + if (matchingTransfer != null) + { + matchingTransfer.Acknowledged = true; + int offset = inc.ReadInt32(); + matchingTransfer.KnownReceivedOffset = offset > matchingTransfer.KnownReceivedOffset ? offset : matchingTransfer.KnownReceivedOffset; + if (matchingTransfer.SentOffset < matchingTransfer.KnownReceivedOffset) { matchingTransfer.SentOffset = matchingTransfer.KnownReceivedOffset; } + + if (matchingTransfer.KnownReceivedOffset >= matchingTransfer.Data.Length) + { + matchingTransfer.Status = FileTransferStatus.Finished; + } + } + } byte fileType = inc.ReadByte(); switch (fileType) @@ -295,17 +336,17 @@ namespace Barotrauma.Networking if (requestedSubmarine != null) { - StartTransfer(inc.SenderConnection, FileTransferType.Submarine, requestedSubmarine.FilePath); + StartTransfer(inc.Sender, FileTransferType.Submarine, requestedSubmarine.FilePath); } break; case (byte)FileTransferType.CampaignSave: if (GameMain.GameSession != null && - !ActiveTransfers.Any(t => t.Connection == inc.SenderConnection && t.FileType == FileTransferType.CampaignSave)) + !ActiveTransfers.Any(t => t.Connection == inc.Sender && t.FileType == FileTransferType.CampaignSave)) { - StartTransfer(inc.SenderConnection, FileTransferType.CampaignSave, GameMain.GameSession.SavePath); + StartTransfer(inc.Sender, FileTransferType.CampaignSave, GameMain.GameSession.SavePath); if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) { - client.LastCampaignSaveSendTime = new Pair(campaign.LastSaveID, (float)NetTime.Now); + client.LastCampaignSaveSendTime = new Pair(campaign.LastSaveID, (float)Lidgren.Network.NetTime.Now); } } break; diff --git a/Barotrauma/BarotraumaServer/Source/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/Source/Networking/GameServer.cs index 9bf787280..49b25cc3d 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/GameServer.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +#define ALLOW_BOT_TRAITORS +using Barotrauma.Items.Components; using Lidgren.Network; using Microsoft.Xna.Framework; using RestSharp; @@ -11,6 +12,7 @@ using System.IO.Compression; using System.IO; using Barotrauma.Steam; using System.Xml.Linq; +using System.Threading; namespace Barotrauma.Networking { @@ -35,7 +37,8 @@ namespace Barotrauma.Networking //is the server running private bool started; - private NetServer server; + private ServerPeer serverPeer; + public ServerPeer ServerPeer { get { return serverPeer; } } private DateTime refreshMasterTimer; private TimeSpan refreshMasterInterval = new TimeSpan(0, 0, 60); @@ -64,6 +67,15 @@ namespace Barotrauma.Networking private ServerEntityEventManager entityEventManager; private FileSender fileSender; +#if DEBUG + public void PrintSenderTransters() + { + foreach (var transfer in fileSender.ActiveTransfers) + { + DebugConsole.NewMessage(transfer.FileName + " " + transfer.Progress.ToString()); + } + } +#endif public override List ConnectedClients { @@ -84,16 +96,16 @@ namespace Barotrauma.Networking get { return updateInterval; } } + public int Port => serverSettings?.Port ?? 0; + //only used when connected to steam - public int QueryPort - { - get; - set; - } + public int QueryPort => serverSettings?.QueryPort ?? 0; - public NetConnection OwnerConnection { get; private set; } + public NetworkConnection OwnerConnection { get; private set; } + private int? ownerKey; + private UInt64? ownerSteamId; - public GameServer(string name, int port, int queryPort = 0, bool isPublic = false, string password = "", bool attemptUPnP = false, int maxPlayers = 10, int ownerKey = 0) + public GameServer(string name, int port, int queryPort = 0, bool isPublic = false, string password = "", bool attemptUPnP = false, int maxPlayers = 10, int? ownKey = null, UInt64? steamId = null) { name = name.Replace(":", ""); name = name.Replace(";", ""); @@ -104,35 +116,18 @@ namespace Barotrauma.Networking this.name = name; - this.ownerKey = ownerKey; - LastClientListUpdateID = 0; - NetPeerConfiguration = new NetPeerConfiguration("barotrauma"); - NetPeerConfiguration.Port = port; - Port = port; - QueryPort = queryPort; - - if (attemptUPnP) - { - NetPeerConfiguration.EnableUPnP = true; - } - serverSettings = new ServerSettings(this, name, port, queryPort, maxPlayers, isPublic, attemptUPnP); if (!string.IsNullOrEmpty(password)) { serverSettings.SetPassword(password); } - - NetPeerConfiguration.MaximumConnections = maxPlayers * 2; //double the lidgren connections for unauthenticated players - NetPeerConfiguration.DisableMessageType(NetIncomingMessageType.DebugMessage | - NetIncomingMessageType.WarningMessage | NetIncomingMessageType.Receipt | - NetIncomingMessageType.ErrorMessage | NetIncomingMessageType.Error | - NetIncomingMessageType.UnconnectedData); + ownerKey = ownKey; + + ownerSteamId = steamId; - NetPeerConfiguration.EnableMessageType(NetIncomingMessageType.ConnectionApproval); - entityEventManager = new ServerEntityEventManager(this); CoroutineManager.StartCoroutine(StartServer(isPublic)); @@ -144,16 +139,30 @@ namespace Barotrauma.Networking try { Log("Starting the server...", ServerLog.MessageType.ServerMessage); - server = new NetServer(NetPeerConfiguration); - NetPeer = server; + if (!ownerSteamId.HasValue || ownerSteamId.Value == 0) + { + Log("Using Lidgren networking", ServerLog.MessageType.ServerMessage); + serverPeer = new LidgrenServerPeer(ownerKey, serverSettings); + } + else + { + Log("Using SteamP2P", ServerLog.MessageType.ServerMessage); + serverPeer = new SteamP2PServerPeer(ownerSteamId.Value, serverSettings); + } - fileSender = new FileSender(this); + serverPeer.OnInitializationComplete = OnInitializationComplete; + serverPeer.OnMessageReceived = ReadDataMessage; + serverPeer.OnDisconnect = OnClientDisconnect; + serverPeer.OnShutdown = GameMain.Instance.CloseServer; + serverPeer.OnOwnerDetermined = OnOwnerDetermined; + + fileSender = new FileSender(serverPeer, MsgConstants.MTU); fileSender.OnEnded += FileTransferChanged; fileSender.OnStarted += FileTransferChanged; - server.Start(); + serverPeer.Start(); - VoipServer = new VoipServer(server); + VoipServer = new VoipServer(serverPeer); } catch (Exception e) { @@ -166,33 +175,24 @@ namespace Barotrauma.Networking if (error) { - if (server != null) server.Shutdown("Error while starting the server"); + if (serverPeer != null) serverPeer.Close("Error while starting the server"); Environment.Exit(-1); yield return CoroutineStatus.Success; } - if (NetPeerConfiguration.EnableUPnP) - { - InitUPnP(); - //DateTime upnpTimeout = DateTime.Now + new TimeSpan(0,0,5); - while (DiscoveringUPnP())// && upnpTimeout>DateTime.Now) + if (serverPeer is LidgrenServerPeer) + { + if (SteamManager.USE_STEAM) { - yield return null; + registeredToMaster = SteamManager.CreateServer(this, isPublic); + } + if (isPublic && !GameMain.Config.UseSteamMatchmaking) + { + CoroutineManager.StartCoroutine(RegisterToMasterServer()); } - - FinishUPnP(); - } - - if (SteamManager.USE_STEAM) - { - registeredToMaster = SteamManager.CreateServer(this, isPublic); - } - if (isPublic && !GameMain.Config.UseSteamMatchmaking) - { - CoroutineManager.StartCoroutine(RegisterToMasterServer()); } TickRate = serverSettings.TickRate; @@ -208,6 +208,106 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Success; } + private void OnOwnerDetermined(NetworkConnection connection) + { + OwnerConnection = connection; + + var ownerClient = ConnectedClients.Find(c => c.Connection == connection); + if (ownerClient == null) + { + DebugConsole.ThrowError("Owner client not found! Can't set permissions"); + return; + } + ownerClient.SetPermissions(ClientPermissions.All, DebugConsole.Commands); + UpdateClientPermissions(ownerClient); + } + + public void NotifyCrash() + { + var tempList = ConnectedClients.Where(c => c.Connection != OwnerConnection).ToList(); + foreach (var c in tempList) + { + DisconnectClient(c.Connection, DisconnectReason.ServerCrashed.ToString(), DisconnectReason.ServerCrashed.ToString()); + } + if (OwnerConnection != null) + { + var conn = OwnerConnection; OwnerConnection = null; + DisconnectClient(conn, DisconnectReason.ServerCrashed.ToString(), DisconnectReason.ServerCrashed.ToString()); + } + Thread.Sleep(500); + } + + private void OnInitializationComplete(NetworkConnection connection) + { + string clName = connection.Name; + Client newClient = new Client(clName, GetNewClientID()); + newClient.InitClientSync(); + newClient.Connection = connection; + newClient.SteamID = connection.SteamID; + ConnectedClients.Add(newClient); + + var previousPlayer = previousPlayers.Find(p => p.MatchesClient(newClient)); + if (previousPlayer != null) + { + newClient.Karma = previousPlayer.Karma; + newClient.KarmaKickCount = previousPlayer.KarmaKickCount; + foreach (Client c in previousPlayer.KickVoters) + { + if (!connectedClients.Contains(c)) { continue; } + newClient.AddKickVote(c); + } + } + + LastClientListUpdateID++; + + if (newClient.Connection == OwnerConnection) + { + newClient.GivePermission(ClientPermissions.All); + newClient.PermittedConsoleCommands.AddRange(DebugConsole.Commands); + + GameMain.Server.UpdateClientPermissions(newClient); + GameMain.Server.SendConsoleMessage("Granted all permissions to " + newClient.Name + ".", newClient); + } + + GameMain.Server.SendChatMessage($"ServerMessage.JoinedServer~[client]={clName}", ChatMessageType.Server, null); + serverSettings.ServerDetailsChanged = true; + + if (previousPlayer != null && previousPlayer.Name != newClient.Name) + { + GameMain.Server.SendChatMessage($"ServerMessage.PreviousClientName~[client]={clName}~[previousname]={previousPlayer.Name}", ChatMessageType.Server, null); + previousPlayer.Name = newClient.Name; + } + + var savedPermissions = serverSettings.ClientPermissions.Find(cp => + cp.SteamID > 0 ? + cp.SteamID == newClient.SteamID : + newClient.EndpointMatches(cp.EndPoint)); + + if (savedPermissions != null) + { + newClient.SetPermissions(savedPermissions.Permissions, savedPermissions.PermittedCommands); + } + else + { + var defaultPerms = PermissionPreset.List.Find(p => p.Name == "None"); + if (defaultPerms != null) + { + newClient.SetPermissions(defaultPerms.Permissions, defaultPerms.PermittedCommands); + } + else + { + newClient.SetPermissions(ClientPermissions.None, new List()); + } + } + } + + private void OnClientDisconnect(NetworkConnection connection, string disconnectMsg) + { + Client connectedClient = connectedClients.Find(c => c.Connection == connection); + + DisconnectClient(connectedClient, reason: disconnectMsg); + } + private IEnumerable RegisterToMasterServer() { if (restClient == null) @@ -339,18 +439,7 @@ namespace Barotrauma.Networking if (!started) return; base.Update(deltaTime); - - foreach (UnauthenticatedClient unauthClient in unauthenticatedClients) - { - unauthClient.AuthTimer -= deltaTime; - if (unauthClient.AuthTimer <= 0.0f) - { - unauthClient.Connection.Disconnect("Connection timed out"); - } - } - - unauthenticatedClients.RemoveAll(uc => uc.AuthTimer <= 0.0f); - + fileSender.Update(deltaTime); KarmaManager.UpdateClients(ConnectedClients, deltaTime); @@ -382,7 +471,7 @@ namespace Barotrauma.Networking Client owner = connectedClients.Find(c => c.InGame && !c.NeedsMidRoundSync && c.Name == character.OwnerClientName && - c.IPMatches(character.OwnerClientIP)); + c.EndpointMatches(character.OwnerClientEndPoint)); if (owner != null && (!serverSettings.AllowSpectating || !owner.SpectateOnly)) { @@ -390,6 +479,11 @@ namespace Barotrauma.Networking } } + if (TraitorManager != null) + { + TraitorManager.Update(deltaTime); + } + bool isCrewDead = connectedClients.All(c => c.Character == null || c.Character.IsDead || c.Character.IsUnconscious); @@ -473,9 +567,7 @@ namespace Barotrauma.Networking initiatedStartGame = false; } } - else if (Screen.Selected == GameMain.NetLobbyScreen && - !gameStarted && - !initiatedStartGame) + else if (Screen.Selected == GameMain.NetLobbyScreen && !gameStarted && !initiatedStartGame) { if (serverSettings.AutoRestart) { @@ -545,96 +637,12 @@ namespace Barotrauma.Networking KickClient(c, "DisconnectMessage.AFK"); } - NetIncomingMessage inc = null; - while ((inc = server.ReadMessage()) != null) - { - try - { - switch (inc.MessageType) - { - case NetIncomingMessageType.Data: - ReadDataMessage(inc); - break; - case NetIncomingMessageType.StatusChanged: - switch (inc.SenderConnection.Status) - { - case NetConnectionStatus.Disconnected: - var connectedClient = connectedClients.Find(c => c.Connection == inc.SenderConnection); - /*if (connectedClient != null && !disconnectedClients.Contains(connectedClient)) - { - connectedClient.deleteDisconnectedTimer = NetConfig.DeleteDisconnectedTime; - disconnectedClients.Add(connectedClient); - } - */ - if (connectedClient != null) - { - DisconnectClient(inc.SenderConnection, $"ServerMessage.HasDisconnected~[client]={connectedClient.Name}"); - } - else - { - DisconnectClient(inc.SenderConnection, string.Empty); - } - - break; - } - break; - case NetIncomingMessageType.ConnectionApproval: - var msgContent = inc.SenderConnection.RemoteHailMessage ?? inc; - ClientPacketHeader packetHeader = (ClientPacketHeader)msgContent.ReadByte(); - if (packetHeader == ClientPacketHeader.REQUEST_AUTH || - packetHeader == ClientPacketHeader.REQUEST_STEAMAUTH) - { - HandleOwnership(msgContent, inc.SenderConnection); - } - - DebugConsole.Log(packetHeader.ToString()); - if (inc.SenderConnection != OwnerConnection && serverSettings.BanList.IsBanned(inc.SenderEndPoint.Address, 0)) - { - inc.SenderConnection.Deny(DisconnectReason.Banned.ToString()); - } - else if (ConnectedClients.Count >= serverSettings.MaxPlayers) - { - inc.SenderConnection.Deny(DisconnectReason.ServerFull.ToString()); - } - else - { - if (packetHeader == ClientPacketHeader.REQUEST_AUTH) - { - inc.SenderConnection.Approve(); - HandleClientAuthRequest(inc.SenderConnection); - } - else if (packetHeader == ClientPacketHeader.REQUEST_STEAMAUTH) - { - ReadClientSteamAuthRequest(msgContent, inc.SenderConnection, out ulong clientSteamID); - if (inc.SenderConnection != OwnerConnection && serverSettings.BanList.IsBanned(null, clientSteamID)) - { - inc.SenderConnection.Deny(DisconnectReason.Banned.ToString()); - } - else - { - inc.SenderConnection.Approve(); - } - } - } - break; - } - } - - catch (Exception e) - { - string errorMsg = "Server failed to read an incoming message. {" + e + "}\n" + e.StackTrace; - GameAnalyticsManager.AddErrorEventOnce("GameServer.Update:ClientReadException" + e.TargetSite.ToString(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); - if (GameSettings.VerboseLogging) - { - DebugConsole.ThrowError(errorMsg); - } - } - } + serverPeer.Update(deltaTime); // if update interval has passed if (updateTimer < DateTime.Now) { - if (server.ConnectionsCount > 0) + if (ConnectedClients.Count > 0) { foreach (Client c in ConnectedClients) { @@ -689,29 +697,13 @@ namespace Barotrauma.Networking } } - private void ReadDataMessage(NetIncomingMessage inc) + private void ReadDataMessage(NetworkConnection sender, IReadMessage inc) { - var connectedClient = connectedClients.Find(c => c.Connection == inc.SenderConnection); - if (inc.SenderConnection != OwnerConnection && serverSettings.BanList.IsBanned(inc.SenderEndPoint.Address, connectedClient == null ? 0 : connectedClient.SteamID)) - { - KickClient(inc.SenderConnection, "You have been banned from the server."); - return; - } + var connectedClient = connectedClients.Find(c => c.Connection == sender); ClientPacketHeader header = (ClientPacketHeader)inc.ReadByte(); switch (header) { - case ClientPacketHeader.REQUEST_AUTH: - HandleOwnership(inc, inc.SenderConnection); - HandleClientAuthRequest(inc.SenderConnection); - break; - case ClientPacketHeader.REQUEST_STEAMAUTH: - HandleOwnership(inc, inc.SenderConnection); - ReadClientSteamAuthRequest(inc, inc.SenderConnection, out _); - break; - case ClientPacketHeader.REQUEST_INIT: - ClientInitRequest(inc); - break; case ClientPacketHeader.RESPONSE_STARTGAME: if (connectedClient != null) { @@ -798,7 +790,7 @@ namespace Barotrauma.Networking } } - private void HandleClientError(NetIncomingMessage inc, Client c) + private void HandleClientError(IReadMessage inc, Client c) { string errorStr = "Unhandled error report"; @@ -869,12 +861,13 @@ namespace Barotrauma.Networking return userID; } - private void ClientReadLobby(NetIncomingMessage inc) + private void ClientReadLobby(IReadMessage inc) { - Client c = ConnectedClients.Find(x => x.Connection == inc.SenderConnection); + Client c = ConnectedClients.Find(x => x.Connection == inc.Sender); if (c == null) { - inc.SenderConnection.Disconnect("You're not a connected client."); + //TODO: remove? + //inc.Sender.Disconnect("You're not a connected client."); return; } @@ -930,12 +923,13 @@ namespace Barotrauma.Networking } } - private void ClientReadIngame(NetIncomingMessage inc) + private void ClientReadIngame(IReadMessage inc) { - Client c = ConnectedClients.Find(x => x.Connection == inc.SenderConnection); + Client c = ConnectedClients.Find(x => x.Connection == inc.Sender); if (c == null) { - inc.SenderConnection.Disconnect("You're not a connected client."); + //TODO: remove? + //inc.SenderConnection.Disconnect("You're not a connected client."); return; } @@ -1037,7 +1031,7 @@ namespace Barotrauma.Networking serverSettings.Voting.ServerRead(inc, c); break; case ClientNetObject.SPECTATING_POS: - c.SpectatePos = new Vector2(inc.ReadFloat(), inc.ReadFloat()); + c.SpectatePos = new Vector2(inc.ReadSingle(), inc.ReadSingle()); break; default: return; @@ -1048,12 +1042,13 @@ namespace Barotrauma.Networking } } - private void ClientReadServerCommand(NetIncomingMessage inc) + private void ClientReadServerCommand(IReadMessage inc) { - Client sender = ConnectedClients.Find(x => x.Connection == inc.SenderConnection); + Client sender = ConnectedClients.Find(x => x.Connection == inc.Sender); if (sender == null) { - inc.SenderConnection.Disconnect("You're not a connected client."); + //TODO: remove? + //inc.SenderConnection.Disconnect("You're not a connected client."); return; } @@ -1069,7 +1064,8 @@ namespace Barotrauma.Networking //clients are allowed to end the round by talking with the watchman in multiplayer //campaign even if they don't have the special permission - if (command == ClientPermissions.ManageRound && inc.PeekBoolean() && + bool peekBool = inc.ReadBoolean(); inc.BitPosition--; + if (command == ClientPermissions.ManageRound && peekBool && GameMain.GameSession?.GameMode is MultiPlayerCampaign mpCampaign) { if (!mpCampaign.AllowedToEndRound(sender.Character) && !sender.HasPermission(command)) @@ -1180,7 +1176,7 @@ namespace Barotrauma.Networking } } - NetOutgoingMessage msg = server.CreateMessage(); + IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ServerPacketHeader.CAMPAIGN_SETUP_INFO); msg.Write((UInt16)saveFiles.Count()); foreach (string saveFile in saveFiles) @@ -1188,7 +1184,7 @@ namespace Barotrauma.Networking msg.Write(saveFile); } - server.SendMessage(msg, sender.Connection, NetDeliveryMethod.ReliableUnordered); + serverPeer.Send(msg, sender.Connection, DeliveryMethod.Reliable); } else { @@ -1262,7 +1258,7 @@ namespace Barotrauma.Networking { c.Character.ClientDisconnected = true; } - + ClientWriteLobby(c); if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign && @@ -1274,16 +1270,16 @@ namespace Barotrauma.Networking { //the save was sent less than 5 second ago, don't attempt to resend yet //(the client may have received it but hasn't acked us yet) - if (c.LastCampaignSaveSendTime.Second > NetTime.Now - 5.0f) + if (c.LastCampaignSaveSendTime.Second > Lidgren.Network.NetTime.Now - 5.0f) { return; - } + } } - + if (!fileSender.ActiveTransfers.Any(t => t.Connection == c.Connection && t.FileType == FileTransferType.CampaignSave)) { fileSender.StartTransfer(c.Connection, FileTransferType.CampaignSave, GameMain.GameSession.SavePath); - c.LastCampaignSaveSendTime = new Pair(campaign.LastSaveID, (float)NetTime.Now); + c.LastCampaignSaveSendTime = new Pair(campaign.LastSaveID, (float)Lidgren.Network.NetTime.Now); } } } @@ -1292,7 +1288,7 @@ namespace Barotrauma.Networking /// /// Write info that the client needs when joining the server /// - private void ClientWriteInitial(Client c, NetBuffer outmsg) + private void ClientWriteInitial(Client c, IWriteMessage outmsg) { if (GameSettings.VerboseLogging) { @@ -1314,39 +1310,7 @@ namespace Barotrauma.Networking c.WritePermissions(outmsg); } - - private const int COMPRESSION_THRESHOLD = 500; - public void CompressOutgoingMessage(NetOutgoingMessage outmsg) - { - if (outmsg.LengthBytes > COMPRESSION_THRESHOLD) - { - byte[] data = outmsg.Data; - using (MemoryStream stream = new MemoryStream()) - { - stream.Write(data, 0, outmsg.LengthBytes); - stream.Position = 0; - using (MemoryStream compressed = new MemoryStream()) - { - using (DeflateStream deflate = new DeflateStream(compressed, CompressionLevel.Fastest, false)) - { - stream.CopyTo(deflate); - } - - byte[] newData = compressed.ToArray(); - - outmsg.Data = newData; - outmsg.LengthBytes = newData.Length; - outmsg.Position = outmsg.LengthBits; - } - } - outmsg.Write((byte)1); //is compressed - } - else - { - outmsg.WritePadBits(); outmsg.Write((byte)0); //isn't compressed - } - } - + private void ClientWriteIngame(Client c) { //don't send position updates to characters who are still midround syncing @@ -1374,7 +1338,7 @@ namespace Barotrauma.Networking float updateInterval = character.GetPositionUpdateInterval(c); c.PositionUpdateLastSent.TryGetValue(character.ID, out float lastSent); - if (lastSent > NetTime.Now - updateInterval) { continue; } + if (lastSent > Lidgren.Network.NetTime.Now - updateInterval) { continue; } if (!c.PendingPositionUpdates.Contains(character)) c.PendingPositionUpdates.Enqueue(character); } @@ -1392,15 +1356,15 @@ namespace Barotrauma.Networking if (item.PositionUpdateInterval == float.PositiveInfinity) { continue; } float updateInterval = item.GetPositionUpdateInterval(c); c.PositionUpdateLastSent.TryGetValue(item.ID, out float lastSent); - if (lastSent > NetTime.Now - item.PositionUpdateInterval) { continue; } + if (lastSent > Lidgren.Network.NetTime.Now - item.PositionUpdateInterval) { continue; } if (!c.PendingPositionUpdates.Contains(item)) c.PendingPositionUpdates.Enqueue(item); } } - NetOutgoingMessage outmsg = server.CreateMessage(); + IWriteMessage outmsg = new WriteOnlyMessage(); outmsg.Write((byte)ServerPacketHeader.UPDATE_INGAME); - outmsg.Write((float)NetTime.Now); + outmsg.Write((float)Lidgren.Network.NetTime.Now); outmsg.Write((byte)ServerNetObject.SYNC_IDS); outmsg.Write(c.LastSentChatMsgID); //send this to client so they know which chat messages weren't received by the server @@ -1426,7 +1390,7 @@ namespace Barotrauma.Networking continue; } - NetBuffer tempBuffer = new NetBuffer(); + IWriteMessage tempBuffer = new ReadWriteMessage(); tempBuffer.Write((byte)ServerNetObject.ENTITY_POSITION); if (entity is Item) { @@ -1438,24 +1402,24 @@ namespace Barotrauma.Networking } //no more room in this packet - if (outmsg.LengthBytes + tempBuffer.LengthBytes > NetPeerConfiguration.MaximumTransmissionUnit - 20) + if (outmsg.LengthBytes + tempBuffer.LengthBytes > MsgConstants.MTU - 20) { break; } - outmsg.Write(tempBuffer); + outmsg.Write(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); outmsg.WritePadBits(); - c.PositionUpdateLastSent[entity.ID] = (float)NetTime.Now; + c.PositionUpdateLastSent[entity.ID] = (float)Lidgren.Network.NetTime.Now; c.PendingPositionUpdates.Dequeue(); } positionUpdateBytes = outmsg.LengthBytes - positionUpdateBytes; outmsg.Write((byte)ServerNetObject.END_OF_MESSAGE); - if (outmsg.LengthBytes > NetPeerConfiguration.MaximumTransmissionUnit) + if (outmsg.LengthBytes > MsgConstants.MTU) { - string errorMsg = "Maximum packet size exceeded (" + outmsg.LengthBytes + " > " + NetPeerConfiguration.MaximumTransmissionUnit + ")\n"; + string errorMsg = "Maximum packet size exceeded (" + outmsg.LengthBytes + " > " + MsgConstants.MTU + ")\n"; errorMsg += " Client list size: " + clientListBytes + " bytes\n" + " Chat message size: " + chatMessageBytes + " bytes\n" + @@ -1463,17 +1427,16 @@ namespace Barotrauma.Networking DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame1:PacketSizeExceeded" + outmsg.LengthBytes, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); } - - CompressOutgoingMessage(outmsg); - server.SendMessage(outmsg, c.Connection, NetDeliveryMethod.Unreliable); + + serverPeer.Send(outmsg, c.Connection, DeliveryMethod.Unreliable); //--------------------------------------------------------------------------- for (int i = 0; i < NetConfig.MaxEventPacketsPerUpdate; i++) { - outmsg = server.CreateMessage(); + outmsg = new WriteOnlyMessage(); outmsg.Write((byte)ServerPacketHeader.UPDATE_INGAME); - outmsg.Write((float)NetTime.Now); + outmsg.Write((float)Lidgren.Network.NetTime.Now); int eventManagerBytes = outmsg.LengthBytes; entityEventManager.Write(c, outmsg, out List sentEvents); @@ -1486,9 +1449,9 @@ namespace Barotrauma.Networking outmsg.Write((byte)ServerNetObject.END_OF_MESSAGE); - if (outmsg.LengthBytes > NetPeerConfiguration.MaximumTransmissionUnit) + if (outmsg.LengthBytes > MsgConstants.MTU) { - string errorMsg = "Maximum packet size exceeded (" + outmsg.LengthBytes + " > " + NetPeerConfiguration.MaximumTransmissionUnit + ")\n"; + string errorMsg = "Maximum packet size exceeded (" + outmsg.LengthBytes + " > " + MsgConstants.MTU + ")\n"; errorMsg += " Event size: " + eventManagerBytes + " bytes\n"; @@ -1504,13 +1467,12 @@ namespace Barotrauma.Networking DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame2:PacketSizeExceeded" + outmsg.LengthBytes, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); } - - CompressOutgoingMessage(outmsg); - server.SendMessage(outmsg, c.Connection, NetDeliveryMethod.Unreliable); + + serverPeer.Send(outmsg, c.Connection, DeliveryMethod.Unreliable); } } - private void WriteClientList(Client c, NetOutgoingMessage outmsg) + private void WriteClientList(Client c, IWriteMessage outmsg) { bool hasChanged = NetIdUtils.IdMoreRecent(LastClientListUpdateID, c.LastRecvClientListUpdate); if (!hasChanged) return; @@ -1522,6 +1484,7 @@ namespace Barotrauma.Networking foreach (Client client in connectedClients) { outmsg.Write(client.ID); + outmsg.Write(client.SteamID); outmsg.Write(client.Name); outmsg.Write(client.Character == null || !gameStarted ? (ushort)0 : client.Character.ID); outmsg.Write(client.Muted); @@ -1534,7 +1497,7 @@ namespace Barotrauma.Networking { bool isInitialUpdate = false; - NetOutgoingMessage outmsg = server.CreateMessage(); + IWriteMessage outmsg = new WriteOnlyMessage(); outmsg.Write((byte)ServerPacketHeader.UPDATE_LOBBY); outmsg.Write((byte)ServerNetObject.SYNC_IDS); @@ -1546,11 +1509,11 @@ namespace Barotrauma.Networking outmsg.Write(GameMain.NetLobbyScreen.LastUpdateID); - NetBuffer settingsBuf = new NetBuffer(); + IWriteMessage settingsBuf = new ReadWriteMessage(); serverSettings.ServerWrite(settingsBuf, c); outmsg.Write((UInt16)settingsBuf.LengthBytes); - outmsg.Write(settingsBuf.Data,0,settingsBuf.LengthBytes); + outmsg.Write(settingsBuf.Buffer,0,settingsBuf.LengthBytes); outmsg.Write(c.LastRecvLobbyUpdate < 1); if (c.LastRecvLobbyUpdate < 1) @@ -1571,9 +1534,9 @@ namespace Barotrauma.Networking outmsg.Write(serverSettings.AllowSpectating); - outmsg.WriteRangedInteger(0, 2, (int)serverSettings.TraitorsEnabled); + outmsg.WriteRangedIntegerDeprecated(0, 2, (int)serverSettings.TraitorsEnabled); - outmsg.WriteRangedInteger(0, Enum.GetValues(typeof(MissionType)).Length - 1, (GameMain.NetLobbyScreen.MissionTypeIndex)); + outmsg.WriteRangedIntegerDeprecated(0, Enum.GetValues(typeof(MissionType)).Length - 1, (GameMain.NetLobbyScreen.MissionTypeIndex)); outmsg.Write((byte)GameMain.NetLobbyScreen.SelectedModeIndex); outmsg.Write(GameMain.NetLobbyScreen.LevelSeed); @@ -1615,16 +1578,14 @@ namespace Barotrauma.Networking WriteChatMessages(outmsg, c); outmsg.Write((byte)ServerNetObject.END_OF_MESSAGE); - - CompressOutgoingMessage(outmsg); - + if (isInitialUpdate) { //the initial update may be very large if the host has a large number //of submarine files, so the message may have to be fragmented //unreliable messages don't play nicely with fragmenting, so we'll send the message reliably - server.SendMessage(outmsg, c.Connection, NetDeliveryMethod.ReliableUnordered); + serverPeer.Send(outmsg, c.Connection, DeliveryMethod.Reliable); //and assume the message was received, so we don't have to keep resending //these large initial messages until the client acknowledges receiving them @@ -1634,21 +1595,21 @@ namespace Barotrauma.Networking } else { - if (outmsg.LengthBytes > NetPeerConfiguration.MaximumTransmissionUnit) + if (outmsg.LengthBytes > MsgConstants.MTU) { - DebugConsole.ThrowError("Maximum packet size exceeded (" + outmsg.LengthBytes + " > " + NetPeerConfiguration.MaximumTransmissionUnit + ")"); + DebugConsole.ThrowError("Maximum packet size exceeded (" + outmsg.LengthBytes + " > " + MsgConstants.MTU + ")"); } - server.SendMessage(outmsg, c.Connection, NetDeliveryMethod.Unreliable); + serverPeer.Send(outmsg, c.Connection, DeliveryMethod.Unreliable); } } - private void WriteChatMessages(NetOutgoingMessage outmsg, Client c) + private void WriteChatMessages(IWriteMessage outmsg, Client c) { c.ChatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, c.LastRecvChatMsgID)); for (int i = 0; i < c.ChatMsgQueue.Count && i < ChatMessage.MaxMessagesPerPacket; i++) { - if (outmsg.LengthBytes + c.ChatMsgQueue[i].EstimateLengthBytesServer(c) > NetPeerConfiguration.MaximumTransmissionUnit - 5) + if (outmsg.LengthBytes + c.ChatMsgQueue[i].EstimateLengthBytesServer(c) > MsgConstants.MTU - 5) { //not enough room in this packet return; @@ -1704,7 +1665,7 @@ namespace Barotrauma.Networking if (connectedClients.Any()) { - NetOutgoingMessage msg = server.CreateMessage(); + IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ServerPacketHeader.QUERY_STARTGAME); msg.Write(selectedSub.Name); @@ -1715,10 +1676,11 @@ namespace Barotrauma.Networking msg.Write(selectedShuttle.MD5Hash.Hash); connectedClients.ForEach(c => c.ReadyToStart = false); - - CompressOutgoingMessage(msg); - - server.SendMessage(msg, connectedClients.Select(c => c.Connection).ToList(), NetDeliveryMethod.ReliableUnordered, 0); + + foreach (NetworkConnection conn in connectedClients.Select(c => c.Connection)) + { + serverPeer.Send(msg, conn, DeliveryMethod.Reliable); + } //give the clients a few seconds to request missing sub/shuttle files before starting the round float waitForResponseTimer = 5.0f; @@ -1870,7 +1832,7 @@ namespace Barotrauma.Networking bots.Add(botInfo); } AssignBotJobs(bots, teamID); - + WayPoint[] assignedWayPoints = WayPoint.SelectCrewSpawnPoints(characterInfos, Submarine.MainSubs[n]); for (int i = 0; i < teamClients.Count; i++) { @@ -1884,11 +1846,12 @@ namespace Barotrauma.Networking } else { + characterData.HasSpawned = true; characterData.SpawnInventoryItems(spawnedCharacter.Info, spawnedCharacter.Inventory); } teamClients[i].Character = spawnedCharacter; - spawnedCharacter.OwnerClientIP = teamClients[i].Connection.RemoteEndPoint.Address.ToString(); + spawnedCharacter.OwnerClientEndPoint = teamClients[i].Connection.EndPointString; spawnedCharacter.OwnerClientName = teamClients[i].Name; } @@ -1917,22 +1880,15 @@ namespace Barotrauma.Networking if (serverSettings.TraitorsEnabled == YesNoMaybe.Yes || (serverSettings.TraitorsEnabled == YesNoMaybe.Maybe && Rand.Range(0.0f, 1.0f) < 0.5f)) { - List characters = new List(); - foreach (Client client in ConnectedClients) + if (!(GameMain.GameSession?.GameMode is CampaignMode)) { - if (client.Character != null) characters.Add(client.Character); - } - - int max = Math.Max(serverSettings.TraitorUseRatio ? (int)Math.Round(characters.Count * serverSettings.TraitorRatio, 1) : 1, 1); - int traitorCount = Rand.Range(1, max + 1); - TraitorManager = new TraitorManager(this, traitorCount); - - if (TraitorManager.TraitorList.Count > 0) - { - for (int i = 0; i < TraitorManager.TraitorList.Count; i++) + List characters = new List(); + foreach (Client client in ConnectedClients) { - Log(TraitorManager.TraitorList[i].Character.Name + " is the traitor and the target is " + TraitorManager.TraitorList[i].TargetCharacter.Name, ServerLog.MessageType.ServerMessage); + if (client.Character != null) characters.Add(client.Character); } + TraitorManager = new TraitorManager(); + TraitorManager.Start(this); } } @@ -1968,7 +1924,7 @@ namespace Barotrauma.Networking private void SendStartMessage(int seed, Submarine selectedSub, GameModePreset selectedMode, Client client) { - NetOutgoingMessage msg = server.CreateMessage(); + IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ServerPacketHeader.STARTGAME); msg.Write(seed); @@ -2000,31 +1956,20 @@ namespace Barotrauma.Networking msg.Write(serverSettings.AllowDisguises); msg.Write(serverSettings.AllowRewiring); - Traitor traitor = null; - if (TraitorManager != null && TraitorManager.TraitorList.Count > 0) - traitor = TraitorManager.TraitorList.Find(t => t.Character == client.Character); - if (traitor != null) - { - msg.Write(true); - msg.Write(traitor.TargetCharacter.Name); - } - else - { - msg.Write(false); - } - msg.Write(serverSettings.AllowRagdollButton); serverSettings.WriteMonsterEnabled(msg); - - CompressOutgoingMessage(msg); - - server.SendMessage(msg, client.Connection, NetDeliveryMethod.ReliableUnordered); + + serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } public void EndGame() { - if (!gameStarted) { return; } + if (!gameStarted) + { + return; + } + if (GameSettings.VerboseLogging) { Log("Ending the round...\n" + Environment.StackTrace, ServerLog.MessageType.ServerMessage); @@ -2035,13 +1980,20 @@ namespace Barotrauma.Networking Log("Ending the round...", ServerLog.MessageType.ServerMessage); } - string endMessage = "The round has ended." + '\n'; + var traitorEndMessage = TraitorManager?.GetEndMessage() ?? ""; + var traitorEndMessageStart = traitorEndMessage.LastIndexOf('/') + 1; - if (TraitorManager != null) - { - endMessage += TraitorManager.GetEndMessage(); - } + var roundSummary = TextManager.FormatServerMessage("RoundSummaryRoundHasEnded", new string[] {"[traitorinfo]"}, new string[] {"[endsummary.traitorinfo]" /*TraitorManager != null ? TraitorManager.GetEndMessage() : ""*/}); + var roundSummaryStart = roundSummary.LastIndexOf('/') + 1; + string endMessage = string.Join("/", new[] { + traitorEndMessage.Substring(0, traitorEndMessageStart), + "[endsummary.traitorinfo]=" + traitorEndMessage.Substring(traitorEndMessageStart), + roundSummary.Substring(0, roundSummaryStart), + "[endsummary]=" + roundSummary.Substring(roundSummaryStart), + "[endsummary]\n\n[endsummary.traitorinfo]" + }.Where(s => !string.IsNullOrEmpty(s))); + Mission mission = GameMain.GameSession.Mission; GameMain.GameSession.GameMode.End(endMessage); @@ -2075,20 +2027,15 @@ namespace Barotrauma.Networking if (connectedClients.Count > 0) { - NetOutgoingMessage msg = server.CreateMessage(); + IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ServerPacketHeader.ENDGAME); msg.Write(endMessage); msg.Write(mission != null && mission.Completed); msg.Write(GameMain.GameSession?.WinningTeam == null ? (byte)0 : (byte)GameMain.GameSession.WinningTeam); - - CompressOutgoingMessage(msg); - if (server.ConnectionsCount > 0) - { - server.SendMessage(msg, server.Connections, NetDeliveryMethod.ReliableOrdered, 0); - } - + foreach (Client client in connectedClients) { + serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); client.Character = null; client.HasSpawned = false; client.InGame = false; @@ -2122,16 +2069,20 @@ namespace Barotrauma.Networking //so the client will be informed what their actual name is LastClientListUpdateID++; - if (!Client.IsValidName(newName, this)) + if (c.Connection != OwnerConnection) { - SendDirectChatMessage("Could not change your name to \"" + newName + "\" (the name contains disallowed symbols).", c, ChatMessageType.MessageBox); - return false; - } - if (c.Connection != OwnerConnection && Homoglyphs.Compare(newName.ToLower(), Name.ToLower())) - { - SendDirectChatMessage("Could not change your name to \"" + newName + "\" (too similar to the server's name).", c, ChatMessageType.MessageBox); - return false; + if (!Client.IsValidName(newName, serverSettings)) + { + SendDirectChatMessage("Could not change your name to \"" + newName + "\" (the name contains disallowed symbols).", c, ChatMessageType.MessageBox); + return false; + } + if (Homoglyphs.Compare(newName.ToLower(), Name.ToLower())) + { + SendDirectChatMessage("Could not change your name to \"" + newName + "\" (too similar to the server's name).", c, ChatMessageType.MessageBox); + return false; + } } + Client nameTaken = ConnectedClients.Find(c2 => c != c2 && Homoglyphs.Compare(c2.Name.ToLower(), newName.ToLower())); if (nameTaken != null) { @@ -2141,6 +2092,7 @@ namespace Barotrauma.Networking SendChatMessage("Player \"" + c.Name + "\" has changed their name to \"" + newName + "\".", ChatMessageType.Server); c.Name = newName; + c.Connection.Name = newName; return true; } @@ -2155,7 +2107,7 @@ namespace Barotrauma.Networking KickClient(client, reason); } - public void KickClient(NetConnection conn, string reason) + public void KickClient(NetworkConnection conn, string reason) { if (conn == OwnerConnection) return; @@ -2163,10 +2115,20 @@ namespace Barotrauma.Networking KickClient(client, reason); } - public void KickClient(Client client, string reason) + public void KickClient(Client client, string reason, bool resetKarma = false) { if (client == null || client.Connection == OwnerConnection) return; + if (resetKarma) + { + var previousPlayer = previousPlayers.Find(p => p.MatchesClient(client)); + if (previousPlayer != null) + { + previousPlayer.Karma = Math.Max(previousPlayer.Karma, 50.0f); + } + client.Karma = Math.Max(client.Karma, 50.0f); + } + string msg = DisconnectReason.Kicked.ToString(); string logMsg = $"ServerMessage.KickedFromServer~[client]={client.Name}"; DisconnectClient(client, logMsg, msg, reason); @@ -2191,18 +2153,30 @@ namespace Barotrauma.Networking public void BanClient(Client client, string reason, bool range = false, TimeSpan? duration = null) { - if (client == null) return; - if (client.Connection == OwnerConnection) return; + if (client == null || client.Connection == OwnerConnection) { return; } + + var previousPlayer = previousPlayers.Find(p => p.MatchesClient(client)); + if (previousPlayer != null) + { + //reset karma to a neutral value, so if/when the ban is revoked the client wont get immediately punished by low karma again + previousPlayer.Karma = Math.Max(previousPlayer.Karma, 50.0f); + } + client.Karma = Math.Max(client.Karma, 50.0f); string targetMsg = DisconnectReason.Banned.ToString(); DisconnectClient(client, $"ServerMessage.BannedFromServer~[client]={client.Name}", targetMsg, reason); if (client.SteamID == 0 || range) { - string ip = client.Connection.RemoteEndPoint.Address.IsIPv4MappedToIPv6 ? - client.Connection.RemoteEndPoint.Address.MapToIPv4().ToString() : - client.Connection.RemoteEndPoint.Address.ToString(); - if (range) { ip = serverSettings.BanList.ToRange(ip); } + string ip = ""; + if (client.Connection is LidgrenConnection lidgrenConn) + { + ip = lidgrenConn.IPEndPoint.Address.IsIPv4MappedToIPv6 ? + lidgrenConn.IPEndPoint.Address.MapToIPv4().ToString() : + lidgrenConn.IPEndPoint.Address.ToString(); + if (range) { ip = serverSettings.BanList.ToRange(ip); } + } + serverSettings.BanList.BanPlayer(client.Name, ip, reason, duration); } if (client.SteamID > 0) @@ -2223,11 +2197,12 @@ namespace Barotrauma.Networking } } - public void DisconnectClient(NetConnection senderConnection, string msg = "", string targetmsg = "") + public void DisconnectClient(NetworkConnection senderConnection, string msg = "", string targetmsg = "") { if (senderConnection == OwnerConnection) { - DebugConsole.NewMessage("Owner disconnected: closing server", Color.Yellow); + DebugConsole.NewMessage("Owner disconnected: closing the server...", Color.Yellow); + Log("Owner disconnected: closing the server...", ServerLog.MessageType.ServerMessage); GameMain.ShouldRun = false; } Client client = connectedClients.Find(x => x.Connection == senderConnection); @@ -2258,7 +2233,7 @@ namespace Barotrauma.Networking targetmsg += $"/\n/ServerMessage.Reason/: /{reason}"; } - if (client.SteamID > 0) { SteamManager.StopAuthSession(client.SteamID); } + if (client.SteamID != 0) { SteamManager.StopAuthSession(client.SteamID); } var previousPlayer = previousPlayers.Find(p => p.MatchesClient(client)); if (previousPlayer == null) @@ -2268,20 +2243,21 @@ namespace Barotrauma.Networking } previousPlayer.Name = client.Name; previousPlayer.Karma = client.Karma; + previousPlayer.KarmaKickCount = client.KarmaKickCount; previousPlayer.KickVoters.Clear(); foreach (Client c in connectedClients) { if (client.HasKickVoteFrom(c)) { previousPlayer.KickVoters.Add(c); } } - - client.Connection.Disconnect(targetmsg); + + serverPeer.Disconnect(client.Connection, targetmsg); client.Dispose(); connectedClients.Remove(client); KarmaManager.OnClientDisconnected(client); UpdateVoteStatus(); - + SendChatMessage(msg, ChatMessageType.Server); UpdateCrewFrame(); @@ -2353,15 +2329,19 @@ namespace Barotrauma.Networking default: if (command != "") { - if (command == name.ToLowerInvariant()) + if (command.ToLower() == name.ToLower()) { //a private message to the host + if (OwnerConnection != null) + { + targetClient = connectedClients.Find(c => c.Connection == OwnerConnection); + } } else { targetClient = connectedClients.Find(c => - command == c.Name.ToLowerInvariant() || - (c.Character != null && command == c.Character.Name.ToLowerInvariant())); + command.ToLower() == c.Name.ToLower() || + command.ToLower() == c.Character?.Name?.ToLower()); if (targetClient == null) { @@ -2370,13 +2350,7 @@ namespace Barotrauma.Networking var chatMsg = ChatMessage.Create( "", $"ServerMessage.PlayerNotFound~[player]={command}", ChatMessageType.Error, null); - - chatMsg.NetStateID = senderClient.ChatMsgQueue.Count > 0 ? - (ushort)(senderClient.ChatMsgQueue.Last().NetStateID + 1) : - (ushort)(senderClient.LastRecvChatMsgID + 1); - - senderClient.ChatMsgQueue.Add(chatMsg); - senderClient.LastChatMsgQueueID = chatMsg.NetStateID; + SendDirectChatMessage(chatMsg, senderClient); } else { @@ -2532,8 +2506,7 @@ namespace Barotrauma.Networking if (type.Value != ChatMessageType.MessageBox) { string myReceivedMessage = type == ChatMessageType.Server || type == ChatMessageType.Error ? TextManager.GetServerMessage(message) : message; - if (!string.IsNullOrWhiteSpace(myReceivedMessage) && - (targetClient == null || senderClient == null)) + if (!string.IsNullOrWhiteSpace(myReceivedMessage)) { AddChatMessage(myReceivedMessage, (ChatMessageType)type, senderName, senderCharacter); } @@ -2580,23 +2553,22 @@ namespace Barotrauma.Networking (transfer.Status == FileTransferStatus.Sending || transfer.Status == FileTransferStatus.Finished) && recipient.LastCampaignSaveSendTime != null) { - recipient.LastCampaignSaveSendTime.Second = (float)NetTime.Now; + recipient.LastCampaignSaveSendTime.Second = (float)Lidgren.Network.NetTime.Now; } } public void SendCancelTransferMsg(FileSender.FileTransferOut transfer) { - NetOutgoingMessage msg = server.CreateMessage(); + IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ServerPacketHeader.FILE_TRANSFER); msg.Write((byte)FileTransferMessageType.Cancel); - msg.Write((byte)transfer.SequenceChannel); - CompressOutgoingMessage(msg); - server.SendMessage(msg, transfer.Connection, NetDeliveryMethod.ReliableOrdered, transfer.SequenceChannel); + msg.Write((byte)transfer.ID); + serverPeer.Send(msg, transfer.Connection, DeliveryMethod.ReliableOrdered); } public void UpdateVoteStatus() { - if (server.Connections.Count == 0 || connectedClients.Count == 0) return; + if (connectedClients.Count == 0) return; Client.UpdateKickVotes(connectedClients); @@ -2633,15 +2605,16 @@ namespace Barotrauma.Networking { if (!recipients.Any()) { return; } - NetOutgoingMessage msg = server.CreateMessage(); + IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ServerPacketHeader.UPDATE_LOBBY); msg.Write((byte)ServerNetObject.VOTE); serverSettings.Voting.ServerWrite(msg); msg.Write((byte)ServerNetObject.END_OF_MESSAGE); - - CompressOutgoingMessage(msg); - - server.SendMessage(msg, recipients.Select(c => c.Connection).ToList(), NetDeliveryMethod.ReliableUnordered, 0); + + foreach (var c in recipients) + { + serverPeer.Send(msg, c.Connection, DeliveryMethod.Reliable); + } } public void UpdateClientPermissions(Client client) @@ -2660,24 +2633,23 @@ namespace Barotrauma.Networking } else { - serverSettings.ClientPermissions.RemoveAll(cp => client.IPMatches(cp.IP)); + serverSettings.ClientPermissions.RemoveAll(cp => client.EndpointMatches(cp.EndPoint)); if (client.Permissions != ClientPermissions.None) { serverSettings.ClientPermissions.Add(new ServerSettings.SavedClientPermission( client.Name, - client.Connection.RemoteEndPoint.Address, + client.Connection.EndPointString, client.Permissions, client.PermittedConsoleCommands)); } } - var msg = server.CreateMessage(); + IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ServerPacketHeader.PERMISSIONS); client.WritePermissions(msg); - CompressOutgoingMessage(msg); //send the message to the client whose permissions are being modified and the clients who are allowed to modify permissions - List recipients = new List() { client.Connection }; + List recipients = new List() { client.Connection }; foreach (Client otherClient in connectedClients) { if (otherClient.HasPermission(ClientPermissions.ManagePermissions) && !recipients.Contains(otherClient.Connection)) @@ -2685,9 +2657,9 @@ namespace Barotrauma.Networking recipients.Add(otherClient.Connection); } } - if (recipients.Any()) + foreach (NetworkConnection c in recipients) { - server.SendMessage(msg, recipients, NetDeliveryMethod.ReliableUnordered, 0); + serverPeer.Send(msg, c, DeliveryMethod.Reliable); } serverSettings.SaveClientPermissions(); @@ -2711,27 +2683,41 @@ namespace Barotrauma.Networking if (client.GivenAchievements.Contains(achievementIdentifier)) return; client.GivenAchievements.Add(achievementIdentifier); - var msg = server.CreateMessage(); + IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ServerPacketHeader.ACHIEVEMENT); msg.Write(achievementIdentifier); + + serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); + } - CompressOutgoingMessage(msg); + public void SendTraitorMessage(Client client, string message, TraitorMessageType messageType) + { + if (client == null) { return; } + if (!TraitorManager.IsTraitor(client.Character) && client.Connection != OwnerConnection) + { + return; + } + var msg = new WriteOnlyMessage(); + msg.Write((byte)ServerPacketHeader.TRAITOR_MESSAGE); + msg.Write((byte)messageType); + msg.Write(message); - server.SendMessage(msg, client.Connection, NetDeliveryMethod.ReliableUnordered); + serverPeer.Send(msg, client.Connection, DeliveryMethod.ReliableOrdered); } public void UpdateCheatsEnabled() { if (!connectedClients.Any()) { return; } - var msg = server.CreateMessage(); + IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ServerPacketHeader.CHEATS_ENABLED); msg.Write(DebugConsole.CheatsEnabled); msg.WritePadBits(); - - CompressOutgoingMessage(msg); - - server.SendMessage(msg, connectedClients.Select(c => c.Connection).ToList(), NetDeliveryMethod.ReliableUnordered, 0); + + foreach (Client c in connectedClients) + { + serverPeer.Send(msg, c.Connection, DeliveryMethod.Reliable); + } } public void SetClientCharacter(Client client, Character newCharacter) @@ -2742,7 +2728,7 @@ namespace Barotrauma.Networking if (client.Character != null) { client.Character.IsRemotePlayer = false; - client.Character.OwnerClientIP = null; + client.Character.OwnerClientEndPoint = null; client.Character.OwnerClientName = null; } @@ -2764,7 +2750,7 @@ namespace Barotrauma.Networking newCharacter.LastNetworkUpdateID = client.Character.LastNetworkUpdateID; } - newCharacter.OwnerClientIP = client.Connection.RemoteEndPoint.Address.ToString(); + newCharacter.OwnerClientEndPoint = client.Connection.EndPointString; newCharacter.OwnerClientName = client.Name; newCharacter.IsRemotePlayer = true; newCharacter.Enabled = true; @@ -2773,7 +2759,7 @@ namespace Barotrauma.Networking } } - private void UpdateCharacterInfo(NetIncomingMessage message, Client sender) + private void UpdateCharacterInfo(IReadMessage message, Client sender) { sender.SpectateOnly = message.ReadBoolean() && (serverSettings.AllowSpectating || sender.Connection == OwnerConnection); if (sender.SpectateOnly) @@ -3037,73 +3023,71 @@ namespace Barotrauma.Networking } } + public Tuple FindPreviousClientData(Client client) + { + var player = previousPlayers.Find(p => p.MatchesClient(client)); + if (player != null) + { + return Tuple.Create(player.SteamID, player.EndPoint); + } + return null; + } + public override void Disconnect() { - serverSettings.BanList.Save(); - serverSettings.SaveSettings(); - SteamManager.CloseServer(); - - if (registeredToMaster) + if (started) { - if (restClient != null) + started = false; + + serverSettings.BanList.Save(); + serverSettings.SaveSettings(); + + if (registeredToMaster) { - var request = new RestRequest("masterserver2.php", Method.GET); - request.AddParameter("action", "removeserver"); - request.AddParameter("serverport", Port); - restClient.Execute(request); - restClient = null; + if (restClient != null) + { + var request = new RestRequest("masterserver2.php", Method.GET); + request.AddParameter("action", "removeserver"); + request.AddParameter("serverport", Port); + restClient.Execute(request); + restClient = null; + } } + + if (serverSettings.SaveServerLogs) + { + Log("Shutting down the server...", ServerLog.MessageType.ServerMessage); + serverSettings.ServerLog.Save(); + } + + GameAnalyticsManager.AddDesignEvent("GameServer:ShutDown"); + serverPeer?.Close(DisconnectReason.ServerShutdown.ToString()); + + SteamManager.CloseServer(); } - - if (serverSettings.SaveServerLogs) - { - Log("Shutting down the server...", ServerLog.MessageType.ServerMessage); - serverSettings.ServerLog.Save(); - } - - GameAnalyticsManager.AddDesignEvent("GameServer:ShutDown"); - server.Shutdown(DisconnectReason.ServerShutdown.ToString()); - } - - void InitUPnP() - { - server.UPnP.ForwardPort(NetPeerConfiguration.Port, "barotrauma"); - if (Steam.SteamManager.USE_STEAM) - { - server.UPnP.ForwardPort(QueryPort, "barotrauma"); - } - } - - bool DiscoveringUPnP() - { - return server.UPnP.Status == UPnPStatus.Discovering; - } - - void FinishUPnP() - { - //do nothing } } partial class PreviousPlayer { public string Name; - public string IP; + public string EndPoint; public UInt64 SteamID; public float Karma; + public int KarmaKickCount; public readonly List KickVoters = new List(); public PreviousPlayer(Client c) { Name = c.Name; - IP = c.Connection?.RemoteEndPoint?.Address?.ToString() ?? ""; + EndPoint = c.Connection?.EndPointString ?? ""; SteamID = c.SteamID; } public bool MatchesClient(Client c) { if (c.SteamID > 0 && SteamID > 0) { return c.SteamID == SteamID; } - return c.IPMatches(IP); + return c.EndpointMatches(EndPoint); } } } diff --git a/Barotrauma/BarotraumaServer/Source/Networking/GameServerLogin.cs b/Barotrauma/BarotraumaServer/Source/Networking/GameServerLogin.cs deleted file mode 100644 index f45e1fc36..000000000 --- a/Barotrauma/BarotraumaServer/Source/Networking/GameServerLogin.cs +++ /dev/null @@ -1,539 +0,0 @@ -using Lidgren.Network; -using Microsoft.Xna.Framework; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace Barotrauma.Networking -{ - class UnauthenticatedClient - { - public readonly NetConnection Connection; - public readonly ulong SteamID; - public Facepunch.Steamworks.ServerAuth.Status? SteamAuthStatus = null; - public readonly int Nonce; - - public int FailedAttempts; - - public float AuthTimer; - - public UnauthenticatedClient(NetConnection connection, int nonce, ulong steamID = 0) - { - Connection = connection; - SteamID = steamID; - Nonce = nonce; - AuthTimer = 10.0f; - FailedAttempts = 0; - } - } - - partial class GameServer : NetworkMember - { - private Int32 ownerKey = 0; - - List unauthenticatedClients = new List(); - - private void ReadClientSteamAuthRequest(NetIncomingMessage inc, NetConnection senderConnection, out ulong clientSteamID) - { - clientSteamID = 0; - if (!Steam.SteamManager.USE_STEAM) - { - DebugConsole.Log("Received a Steam auth request from " + senderConnection.RemoteEndPoint + ". Steam authentication not required, handling auth normally."); - //not using steam, handle auth normally - HandleClientAuthRequest(senderConnection, 0); - return; - } - - if (senderConnection == OwnerConnection) - { - //the client is the owner of the server, no need for authentication - //(it would fail with a "duplicate request" error anyway) - HandleClientAuthRequest(senderConnection, 0); - return; - } - - clientSteamID = inc.ReadUInt64(); - int authTicketLength = inc.ReadInt32(); - inc.ReadBytes(authTicketLength, out byte[] authTicketData); - - DebugConsole.Log("Received a Steam auth request"); - DebugConsole.Log(" Steam ID: "+ clientSteamID); - DebugConsole.Log(" Auth ticket length: " + authTicketLength); - DebugConsole.Log(" Auth ticket data: " + - ((authTicketData == null) ? "null" : ToolBox.LimitString(string.Concat(authTicketData.Select(b => b.ToString("X2"))), 16))); - - if (senderConnection != OwnerConnection && - serverSettings.BanList.IsBanned(senderConnection.RemoteEndPoint.Address, clientSteamID)) - { - return; - } - ulong steamID = clientSteamID; - if (unauthenticatedClients.Any(uc => uc.Connection == inc.SenderConnection)) - { - var steamAuthedClient = unauthenticatedClients.Find(uc => - uc.Connection == inc.SenderConnection && - uc.SteamID == steamID && - uc.SteamAuthStatus == Facepunch.Steamworks.ServerAuth.Status.OK); - if (steamAuthedClient != null) - { - DebugConsole.Log("Client already authenticated, sending AUTH_RESPONSE again..."); - HandleClientAuthRequest(inc.SenderConnection, steamID); - } - DebugConsole.Log("Steam authentication already pending..."); - return; - } - - if (authTicketData == null) - { - DebugConsole.Log("Invalid request"); - return; - } - - unauthenticatedClients.RemoveAll(uc => uc.Connection == senderConnection); - int nonce = CryptoRandom.Instance.Next(); - var unauthClient = new UnauthenticatedClient(senderConnection, nonce, clientSteamID) - { - AuthTimer = 20 - }; - unauthenticatedClients.Add(unauthClient); - - if (!Steam.SteamManager.StartAuthSession(authTicketData, clientSteamID)) - { - unauthenticatedClients.Remove(unauthClient); - if (GameMain.Config.RequireSteamAuthentication) - { - unauthClient.Connection.Disconnect(DisconnectReason.SteamAuthenticationFailed.ToString()); - Log("Disconnected unauthenticated client (Steam ID: " + steamID + "). Steam authentication failed.", ServerLog.MessageType.ServerMessage); - } - else - { - DebugConsole.Log("Steam authentication failed, skipping to basic auth..."); - HandleClientAuthRequest(senderConnection); - return; - } - } - - return; - } - - public void OnAuthChange(ulong steamID, ulong ownerID, Facepunch.Steamworks.ServerAuth.Status status) - { - DebugConsole.Log("************ OnAuthChange"); - DebugConsole.Log(" Steam ID: " + steamID); - DebugConsole.Log(" Owner ID: " + ownerID); - DebugConsole.Log(" Status: " + status); - - UnauthenticatedClient unauthClient = unauthenticatedClients.Find(uc => uc.SteamID == ownerID); - if (unauthClient != null) - { - unauthClient.SteamAuthStatus = status; - switch (status) - { - case Facepunch.Steamworks.ServerAuth.Status.OK: - ////steam authentication done, check password next - Log("Successfully authenticated client via Steam (Steam ID: " + steamID + ").", ServerLog.MessageType.ServerMessage); - HandleClientAuthRequest(unauthClient.Connection, unauthClient.SteamID); - break; - default: - unauthenticatedClients.Remove(unauthClient); - if (GameMain.Config.RequireSteamAuthentication) - { - Log("Disconnected unauthenticated client (Steam ID: " + steamID + "). Steam authentication failed, (" + status + ").", ServerLog.MessageType.ServerMessage); - unauthClient.Connection.Disconnect(DisconnectReason.SteamAuthenticationFailed.ToString() + "/ (" + status.ToString() + ")"); - } - else - { - DebugConsole.Log("Steam authentication failed (" + status.ToString() + "), skipping to basic auth..."); - HandleClientAuthRequest(unauthClient.Connection); - return; - } - break; - } - return; - } - else - { - DebugConsole.Log(" No unauthenticated clients found with the Steam ID " + steamID); - } - - //kick connected client if status becomes invalid (e.g. VAC banned, not connected to steam) - /*if (status != Facepunch.Steamworks.ServerAuth.Status.OK && GameMain.Config.RequireSteamAuthentication) - { - var connectedClient = connectedClients.Find(c => c.SteamID == ownerID); - if (connectedClient != null) - { - Log("Disconnecting client " + connectedClient.Name + " (Steam ID: " + steamID + "). Steam authentication no longer valid (" + status + ").", ServerLog.MessageType.ServerMessage); - KickClient(connectedClient, $"DisconnectMessage.SteamAuthNoLongerValid~[status]={status.ToString()}"); - } - }*/ - } - - private bool IsServerOwner(NetIncomingMessage inc, NetConnection senderConnection) - { - string address = senderConnection.RemoteEndPoint.Address.MapToIPv4().ToString(); - int incKey = inc.ReadInt32(); - - if (ownerKey == 0) - { - return false; //ownership key has been destroyed or has never existed - } - if (address.ToString() != "127.0.0.1") - { - return false; //not localhost - } - - if (incKey != ownerKey) - { - return false; //incorrect owner key, how did this even happen - } - return true; - } - - private void HandleOwnership(NetIncomingMessage inc, NetConnection senderConnection) - { - DebugConsole.Log("HandleOwnership (" + senderConnection.RemoteEndPoint.Address + ")"); - if (IsServerOwner(inc, senderConnection)) - { - ownerKey = 0; //destroy owner key so nobody else can take ownership of the server - OwnerConnection = senderConnection; - DebugConsole.NewMessage("Successfully set up server owner", Color.Lime); - } - } - - private void HandleClientAuthRequest(NetConnection connection, ulong steamID = 0) - { - DebugConsole.Log("HandleClientAuthRequest (steamID " + steamID + ")"); - - if (GameMain.Config.RequireSteamAuthentication && connection != OwnerConnection && steamID == 0) - { - DebugConsole.Log("Disconnecting " + connection.RemoteEndPoint + ", Steam authentication required."); - connection.Disconnect(DisconnectReason.SteamAuthenticationRequired.ToString()); - return; - } - - //client wants to know if server requires password - if (ConnectedClients.Find(c => c.Connection == connection) != null) - { - //this client has already been authenticated - return; - } - - UnauthenticatedClient unauthClient = unauthenticatedClients.Find(uc => uc.Connection == connection); - if (unauthClient == null) - { - DebugConsole.Log("Unauthed client, generating a nonce..."); - //new client, generate nonce and add to unauth queue - if (ConnectedClients.Count >= serverSettings.MaxPlayers) - { - //server is full, can't allow new connection - connection.Disconnect(DisconnectReason.ServerFull.ToString()); - if (steamID > 0) { Steam.SteamManager.StopAuthSession(steamID); } - return; - } - - int nonce = CryptoRandom.Instance.Next(); - unauthClient = new UnauthenticatedClient(connection, nonce, steamID); - unauthenticatedClients.Add(unauthClient); - } - unauthClient.AuthTimer = 10.0f; - //if the client is already in the queue, getting another unauth request means that our response was lost; resend - NetOutgoingMessage nonceMsg = server.CreateMessage(); - nonceMsg.Write((byte)ServerPacketHeader.AUTH_RESPONSE); - if (serverSettings.HasPassword && connection != OwnerConnection) - { - nonceMsg.Write(true); //true = password - nonceMsg.Write((Int32)unauthClient.Nonce); //here's nonce, encrypt with this - } - else - { - nonceMsg.Write(false); //false = no password - } - CompressOutgoingMessage(nonceMsg); - DebugConsole.Log("Sending auth response..."); - server.SendMessage(nonceMsg, connection, NetDeliveryMethod.Unreliable); - } - - private void ClientInitRequest(NetIncomingMessage inc) - { - DebugConsole.Log("Received client init request"); - if (ConnectedClients.Find(c => c.Connection == inc.SenderConnection) != null) - { - //this client was already authenticated - //another init request means they didn't get any update packets yet - DebugConsole.Log("Client already connected, ignoring..."); - return; - } - - UnauthenticatedClient unauthClient = unauthenticatedClients.Find(uc => uc.Connection == inc.SenderConnection); - if (unauthClient == null) - { - //client did not ask for nonce first, can't authorize - inc.SenderConnection.Disconnect(DisconnectReason.AuthenticationRequired.ToString()); - if (unauthClient.SteamID > 0) { Steam.SteamManager.StopAuthSession(unauthClient.SteamID); } - return; - } - - if (serverSettings.HasPassword && inc.SenderConnection != OwnerConnection) - { - //decrypt message and compare password - string clPw = inc.ReadString(); - if (!serverSettings.IsPasswordCorrect(clPw, unauthClient.Nonce)) - { - unauthClient.FailedAttempts++; - if (unauthClient.FailedAttempts > 3) - { - //disconnect and ban after too many failed attempts - serverSettings.BanList.BanPlayer("Unnamed", unauthClient.Connection.RemoteEndPoint.Address, "DisconnectMessage.TooManyFailedLogins", duration: null); - DisconnectUnauthClient(inc, unauthClient, DisconnectReason.TooManyFailedLogins, ""); - - Log(inc.SenderConnection.RemoteEndPoint.Address.ToString() + " has been banned from the server (too many wrong passwords)", ServerLog.MessageType.Error); - DebugConsole.NewMessage(inc.SenderConnection.RemoteEndPoint.Address.ToString() + " has been banned from the server (too many wrong passwords)", Color.Red); - return; - } - else - { - //not disconnecting the player here, because they'll still use the same connection and nonce if they try logging in again - NetOutgoingMessage reject = server.CreateMessage(); - reject.Write((byte)ServerPacketHeader.AUTH_FAILURE); - reject.Write("Wrong password! You have " + Convert.ToString(4 - unauthClient.FailedAttempts) + " more attempts before you're banned from the server."); - Log(inc.SenderConnection.RemoteEndPoint.Address.ToString() + " failed to join the server (incorrect password)", ServerLog.MessageType.Error); - DebugConsole.NewMessage(inc.SenderConnection.RemoteEndPoint.Address.ToString() + " failed to join the server (incorrect password)", Color.Red); - CompressOutgoingMessage(reject); - server.SendMessage(reject, unauthClient.Connection, NetDeliveryMethod.Unreliable); - unauthClient.AuthTimer = 10.0f; - return; - } - } - } - string clVersion = inc.ReadString(); - - UInt16 contentPackageCount = inc.ReadUInt16(); - List contentPackageNames = new List(); - List contentPackageHashes = new List(); - for (int i = 0; i < contentPackageCount; i++) - { - string packageName = inc.ReadString(); - string packageHash = inc.ReadString(); - contentPackageNames.Add(packageName); - contentPackageHashes.Add(packageHash); - if (contentPackageCount == 0) - { - DebugConsole.Log("Client is using content package " + - (packageName ?? "null") + " (" + (packageHash ?? "null" + ")")); - } - } - - if (contentPackageCount == 0) - { - DebugConsole.Log("Client did not list any content packages."); - } - - string clName = Client.SanitizeName(inc.ReadString()); - if (string.IsNullOrWhiteSpace(clName)) - { - DisconnectUnauthClient(inc, unauthClient, DisconnectReason.NoName, ""); - - Log(inc.SenderConnection.RemoteEndPoint.Address.ToString() + " couldn't join the server (no name given)", ServerLog.MessageType.Error); - DebugConsole.NewMessage(inc.SenderConnection.RemoteEndPoint.Address.ToString() + " couldn't join the server (no name given)", Color.Red); - return; - } - - bool? isCompatibleVersion = IsCompatible(clVersion, GameMain.Version.ToString()); - if (isCompatibleVersion.HasValue && !isCompatibleVersion.Value) - { - DisconnectUnauthClient(inc, unauthClient, DisconnectReason.InvalidVersion, - $"DisconnectMessage.InvalidVersion~[version]={GameMain.Version.ToString()}~[clientversion]={clVersion}"); - - Log(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (incompatible game version)", ServerLog.MessageType.Error); - DebugConsole.NewMessage(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (incompatible game version)", Color.Red); - return; - } - - //check if the client is missing any of the content packages the server requires - List missingPackages = new List(); - foreach (ContentPackage contentPackage in GameMain.SelectedPackages) - { - if (!contentPackage.HasMultiplayerIncompatibleContent) continue; - bool packageFound = false; - for (int i = 0; i < contentPackageCount; i++) - { - if (contentPackageNames[i] == contentPackage.Name && contentPackageHashes[i] == contentPackage.MD5hash.Hash) - { - packageFound = true; - break; - } - } - if (!packageFound) missingPackages.Add(contentPackage); - } - - if (missingPackages.Count == 1) - { - DisconnectUnauthClient(inc, unauthClient, DisconnectReason.MissingContentPackage, $"DisconnectMessage.MissingContentPackage~[missingcontentpackage]={GetPackageStr(missingPackages[0])}"); - Log(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (missing content package " + GetPackageStr(missingPackages[0]) + ")", ServerLog.MessageType.Error); - return; - } - else if (missingPackages.Count > 1) - { - List packageStrs = new List(); - missingPackages.ForEach(cp => packageStrs.Add(GetPackageStr(cp))); - DisconnectUnauthClient(inc, unauthClient, DisconnectReason.MissingContentPackage, $"DisconnectMessage.MissingContentPackages~[missingcontentpackages]={string.Join(", ", packageStrs)}"); - Log(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (missing content packages " + string.Join(", ", packageStrs) + ")", ServerLog.MessageType.Error); - return; - } - - string GetPackageStr(ContentPackage contentPackage) - { - return "\"" + contentPackage.Name + "\" (hash " + contentPackage.MD5hash.ShortHash + ")"; - } - - //check if the client is using any contentpackages that are not compatible with the server - List> incompatiblePackages = new List>(); - for (int i = 0; i < contentPackageNames.Count; i++) - { - if (!GameMain.Config.SelectedContentPackages.Any(cp => cp.Name == contentPackageNames[i] && cp.MD5hash.Hash == contentPackageHashes[i])) - { - incompatiblePackages.Add(new Pair(contentPackageNames[i], contentPackageHashes[i])); - } - } - - if (incompatiblePackages.Count == 1) - { - DisconnectUnauthClient(inc, unauthClient, DisconnectReason.IncompatibleContentPackage, - $"DisconnectMessage.IncompatibleContentPackage~[incompatiblecontentpackage]={GetPackageStr2(incompatiblePackages[0])}"); - Log(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (incompatible content package " + GetPackageStr2(incompatiblePackages[0]) + ")", ServerLog.MessageType.Error); - return; - } - else if (incompatiblePackages.Count > 1) - { - List packageStrs = new List(); - incompatiblePackages.ForEach(cp => packageStrs.Add(GetPackageStr2(cp))); - DisconnectUnauthClient(inc, unauthClient, DisconnectReason.IncompatibleContentPackage, - $"DisconnectMessage.IncompatibleContentPackages~[incompatiblecontentpackages]={string.Join(", ", packageStrs)}"); - Log(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (incompatible content packages " + string.Join(", ", packageStrs) + ")", ServerLog.MessageType.Error); - return; - } - - string GetPackageStr2(Pair nameAndHash) - { - return "\"" + nameAndHash.First + "\" (hash " + Md5Hash.GetShortHash(nameAndHash.Second) + ")"; - } - - if (inc.SenderConnection != OwnerConnection && !serverSettings.Whitelist.IsWhiteListed(clName, inc.SenderConnection.RemoteEndPoint.Address)) - { - DisconnectUnauthClient(inc, unauthClient, DisconnectReason.NotOnWhitelist, ""); - Log(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (not in whitelist)", ServerLog.MessageType.Error); - DebugConsole.NewMessage(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (not in whitelist)", Color.Red); - return; - } - if (!Client.IsValidName(clName, this)) - { - DisconnectUnauthClient(inc, unauthClient, DisconnectReason.InvalidName, ""); - Log(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (invalid name)", ServerLog.MessageType.Error); - DebugConsole.NewMessage(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (invalid name)", Color.Red); - return; - } - if (inc.SenderConnection != OwnerConnection && Homoglyphs.Compare(clName.ToLower(), Name.ToLower())) - { - DisconnectUnauthClient(inc, unauthClient, DisconnectReason.NameTaken, ""); - Log(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (name taken by the server)", ServerLog.MessageType.Error); - DebugConsole.NewMessage(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (name taken by the server)", Color.Red); - return; - } - Client nameTaken = ConnectedClients.Find(c => Homoglyphs.Compare(c.Name.ToLower(), clName.ToLower())); - if (nameTaken != null) - { - if (nameTaken.Connection.RemoteEndPoint.Address.ToString() == inc.SenderEndPoint.Address.ToString()) - { - //both name and IP address match, replace this player's connection - nameTaken.Connection.Disconnect(DisconnectReason.SessionTaken.ToString()); - nameTaken.Connection = unauthClient.Connection; - nameTaken.InitClientSync(); //reinitialize sync ids because this is a new connection - unauthenticatedClients.Remove(unauthClient); - unauthClient = null; - return; - } - else - { - //can't authorize this client - DisconnectUnauthClient(inc, unauthClient, DisconnectReason.NameTaken, ""); - Log(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (name already taken)", ServerLog.MessageType.Error); - DebugConsole.NewMessage(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (name already taken)", Color.Red); - return; - } - } - - //new client - Client newClient = new Client(clName, GetNewClientID()); - newClient.InitClientSync(); - newClient.Connection = unauthClient.Connection; - newClient.SteamID = unauthClient.SteamID; - unauthenticatedClients.Remove(unauthClient); - unauthClient = null; - ConnectedClients.Add(newClient); - - var previousPlayer = previousPlayers.Find(p => p.MatchesClient(newClient)); - if (previousPlayer != null) - { - newClient.Karma = previousPlayer.Karma; - foreach (Client c in previousPlayer.KickVoters) - { - if (!connectedClients.Contains(c)) { continue; } - newClient.AddKickVote(c); - } - } - - LastClientListUpdateID++; - - if (newClient.Connection == OwnerConnection) - { - newClient.GivePermission(ClientPermissions.All); - newClient.PermittedConsoleCommands.AddRange(DebugConsole.Commands); - - GameMain.Server.UpdateClientPermissions(newClient); - GameMain.Server.SendConsoleMessage("Granted all permissions to " + newClient.Name + ".", newClient); - } - - GameMain.Server.SendChatMessage($"ServerMessage.JoinedServer~[client]={clName}", ChatMessageType.Server, null); - serverSettings.ServerDetailsChanged = true; - - if (previousPlayer != null && previousPlayer.Name != newClient.Name) - { - GameMain.Server.SendChatMessage($"ServerMessage.PreviousClientName~[client]={clName}~[previousname]={previousPlayer.Name}", ChatMessageType.Server, null); - previousPlayer.Name = newClient.Name; - } - - var savedPermissions = serverSettings.ClientPermissions.Find(cp => - cp.SteamID > 0 ? - cp.SteamID == newClient.SteamID : - newClient.IPMatches(cp.IP)); - - if (savedPermissions != null) - { - newClient.SetPermissions(savedPermissions.Permissions, savedPermissions.PermittedCommands); - } - else - { - var defaultPerms = PermissionPreset.List.Find(p => p.Name == "None"); - if (defaultPerms != null) - { - newClient.SetPermissions(defaultPerms.Permissions, defaultPerms.PermittedCommands); - } - else - { - newClient.SetPermissions(ClientPermissions.None, new List()); - } - } - } - - private void DisconnectUnauthClient(NetIncomingMessage inc, UnauthenticatedClient unauthClient, DisconnectReason reason, string message) - { - inc.SenderConnection.Disconnect(reason.ToString() + "/ " + TextManager.GetServerMessage(message)); - if (unauthClient.SteamID > 0) { Steam.SteamManager.StopAuthSession(unauthClient.SteamID); } - if (unauthClient != null) - { - unauthenticatedClients.Remove(unauthClient); - } - } - } -} diff --git a/Barotrauma/BarotraumaServer/Source/Networking/KarmaManager.cs b/Barotrauma/BarotraumaServer/Source/Networking/KarmaManager.cs index fc0ea45a7..37c1f8cce 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/KarmaManager.cs @@ -66,7 +66,15 @@ namespace Barotrauma foreach (Client bannedClient in bannedClients) { - GameMain.Server.BanClient(bannedClient, $"KarmaBanned~[banthreshold]={(int)KickBanThreshold}", duration: TimeSpan.FromSeconds(GameMain.Server.ServerSettings.AutoBanTime)); + if (bannedClient.KarmaKickCount < KicksBeforeBan) + { + GameMain.Server.KickClient(bannedClient, $"KarmaKicked~[banthreshold]={(int)KickBanThreshold}", resetKarma: true); + } + else + { + GameMain.Server.BanClient(bannedClient, $"KarmaBanned~[banthreshold]={(int)KickBanThreshold}", duration: TimeSpan.FromSeconds(GameMain.Server.ServerSettings.AutoBanTime)); + } + bannedClient.KarmaKickCount++; } } @@ -79,7 +87,7 @@ namespace Barotrauma if (TestMode) { string msg = - karmaChange < 0 ? $"You karma has decreased to {client.Karma}" : $"You karma has increased to {client.Karma}"; + karmaChange < 0 ? $"Your karma has decreased to {client.Karma}" : $"Your karma has increased to {client.Karma}"; if (!string.IsNullOrEmpty(debugKarmaChangeReason)) { msg += $". Reason: {debugKarmaChangeReason}"; @@ -100,17 +108,17 @@ namespace Barotrauma private void UpdateClient(Client client, float deltaTime) { - if (client.Karma > KarmaDecayThreshold) + if (client.Character != null && !client.Character.Removed && !client.Character.IsDead) { - client.Karma -= KarmaDecay * deltaTime; - } - else if (client.Karma < KarmaIncreaseThreshold) - { - client.Karma += KarmaIncrease * deltaTime; - } + if (client.Karma > KarmaDecayThreshold) + { + client.Karma -= KarmaDecay * deltaTime; + } + else if (client.Karma < KarmaIncreaseThreshold) + { + client.Karma += KarmaIncrease * deltaTime; + } - if (client.Character != null && !client.Character.Removed) - { //increase the strength of the herpes affliction in steps instead of linearly //otherwise clients could determine their exact karma value from the strength float herpesStrength = 0.0f; @@ -129,6 +137,10 @@ namespace Barotrauma else if (existingAffliction != null) { existingAffliction.Strength = herpesStrength; + if (herpesStrength <= 0.0f) + { + client.Character.CharacterHealth.ReduceAffliction(null, "invertcontrols", 100.0f); + } } //check if the client has disconnected an excessive number of wires @@ -182,19 +194,29 @@ namespace Barotrauma if (target.IsDead || target.Removed) { return; } bool isEnemy = target.AIController is EnemyAIController || target.TeamID != attacker.TeamID; - if (GameMain.Server.TraitorManager != null) + if (GameMain.Server.TraitorManager?.Traitors != null) { - if (GameMain.Server.TraitorManager.TraitorList.Any(t => t.Character == target)) + if (GameMain.Server.TraitorManager.Traitors.Any(t => t.Character == target)) { //traitors always count as enemies isEnemy = true; } - if (GameMain.Server.TraitorManager.TraitorList.Any(t => t.Character == attacker && t.TargetCharacter == target)) + if (GameMain.Server.TraitorManager.Traitors.Any(t => + t.Character == attacker && + t.CurrentObjective != null && + t.CurrentObjective.IsEnemy(target))) { //target counts as an enemy to the traitor isEnemy = true; } } + + //attacking/healing clowns has a smaller effect on karma + if (target.HasEquippedItem("clownmask") && + target.HasEquippedItem("clowncostume")) + { + damage *= 0.5f; + } if (appliedAfflictions != null) { @@ -205,7 +227,7 @@ namespace Barotrauma } } - if (target.AIController is EnemyAIController || target.TeamID != attacker.TeamID) + if (isEnemy) { if (damage > 0) { @@ -242,6 +264,19 @@ namespace Barotrauma if (damageAmount > 0) { if (StructureDamageKarmaDecrease <= 0.0f) { return; } + + if (GameMain.Server.TraitorManager?.Traitors != null) + { + if (GameMain.Server.TraitorManager.Traitors.Any(t => + t.Character == attacker && + t.CurrentObjective != null && + t.CurrentObjective.HasGoalsOfType())) + { + //traitor tasked to flood the sub -> damaging structures is ok + return; + } + } + Client client = GameMain.Server.ConnectedClients.Find(c => c.Character == attacker); if (client != null) { @@ -327,6 +362,13 @@ namespace Barotrauma Client client = GameMain.Server.ConnectedClients.Find(c => c.Character == target); if (client == null) { return; } + //all penalties/rewards are halved when wearing a clown costume + if (target.HasEquippedItem("clownmask") && + target.HasEquippedItem("clowncostume")) + { + amount *= 0.5f; + } + client.Karma += amount; if (TestMode) { diff --git a/Barotrauma/BarotraumaServer/Source/Networking/NetEntityEvent/ServerEntityEventManager.cs b/Barotrauma/BarotraumaServer/Source/Networking/NetEntityEvent/ServerEntityEventManager.cs index 9c22f1d2e..2a1710789 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/NetEntityEvent/ServerEntityEventManager.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/NetEntityEvent/ServerEntityEventManager.cs @@ -1,5 +1,4 @@ using Barotrauma.Extensions; -using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -32,7 +31,7 @@ namespace Barotrauma.Networking #endif } - public void Write(NetBuffer msg, Client recipient) + public void Write(IWriteMessage msg, Client recipient) { serializable.ServerWrite(msg, recipient, Data); } @@ -66,7 +65,7 @@ namespace Barotrauma.Networking public readonly Client Sender; public readonly UInt16 CharacterStateID; - public readonly NetBuffer Data; + public readonly ReadWriteMessage Data; public readonly Character Character; @@ -74,7 +73,7 @@ namespace Barotrauma.Networking public bool IsProcessed; - public BufferedEvent(Client sender, Character senderCharacter, UInt16 characterStateID, IClientSerializable targetEntity, NetBuffer data) + public BufferedEvent(Client sender, Character senderCharacter, UInt16 characterStateID, IClientSerializable targetEntity, ReadWriteMessage data) { this.Sender = sender; this.Character = senderCharacter; @@ -300,7 +299,7 @@ namespace Barotrauma.Networking /// /// Writes all the events that the client hasn't received yet into the outgoing message /// - public void Write(Client client, NetOutgoingMessage msg) + public void Write(Client client, IWriteMessage msg) { Write(client, msg, out _); } @@ -308,7 +307,7 @@ namespace Barotrauma.Networking /// /// Writes all the events that the client hasn't received yet into the outgoing message /// - public void Write(Client client, NetOutgoingMessage msg, out List sentEvents) + public void Write(Client client, IWriteMessage msg, out List sentEvents) { List eventsToSync = null; if (client.NeedsMidRoundSync) @@ -371,7 +370,7 @@ namespace Barotrauma.Networking foreach (NetEntityEvent entityEvent in sentEvents) { (entityEvent as ServerEntityEvent).Sent = true; - client.EntityEventLastSent[entityEvent.ID] = NetTime.Now; + client.EntityEventLastSent[entityEvent.ID] = Lidgren.Network.NetTime.Now; } } @@ -399,9 +398,10 @@ namespace Barotrauma.Networking //find the first event that hasn't been sent in roundtriptime or at all client.EntityEventLastSent.TryGetValue(eventList[i].ID, out double lastSent); - float minInterval = Math.Max(client.Connection.AverageRoundtripTime, (float)server.UpdateInterval.TotalSeconds * 2); + float avgRoundtripTime = 0.01f; //TODO: reimplement client.Connection.AverageRoundtripTime + float minInterval = Math.Max(avgRoundtripTime, (float)server.UpdateInterval.TotalSeconds * 2); - if (lastSent > NetTime.Now - Math.Min(minInterval, 0.5f)) + if (lastSent > Lidgren.Network.NetTime.Now - Math.Min(minInterval, 0.5f)) { continue; } @@ -444,7 +444,7 @@ namespace Barotrauma.Networking /// /// Read the events from the message, ignoring ones we've already received /// - public void Read(NetIncomingMessage msg, Client sender = null) + public void Read(IReadMessage msg, Client sender = null) { UInt16 firstEventID = msg.ReadUInt16(); int eventCount = msg.ReadByte(); @@ -472,7 +472,7 @@ namespace Barotrauma.Networking { DebugConsole.NewMessage("Received msg " + thisEventID, Color.Red); } - msg.Position += msgLength * 8; + msg.BitPosition += msgLength * 8; } else if (entity == null) { @@ -486,7 +486,7 @@ namespace Barotrauma.Networking Microsoft.Xna.Framework.Color.Orange); } sender.LastSentEntityEventID++; - msg.Position += msgLength * 8; + msg.BitPosition += msgLength * 8; } else { @@ -497,8 +497,10 @@ namespace Barotrauma.Networking UInt16 characterStateID = msg.ReadUInt16(); - NetBuffer buffer = new NetBuffer(); - buffer.Write(msg.ReadBytes(msgLength - 2)); + ReadWriteMessage buffer = new ReadWriteMessage(); + byte[] temp = msg.ReadBytes(msgLength - 2); + buffer.Write(temp, 0, msgLength - 2); + buffer.BitPosition = 0; BufferEvent(new BufferedEvent(sender, sender.Character, characterStateID, entity, buffer)); sender.LastSentEntityEventID++; @@ -507,7 +509,7 @@ namespace Barotrauma.Networking } } - protected override void WriteEvent(NetBuffer buffer, NetEntityEvent entityEvent, Client recipient = null) + protected override void WriteEvent(IWriteMessage buffer, NetEntityEvent entityEvent, Client recipient = null) { var serverEvent = entityEvent as ServerEntityEvent; if (serverEvent == null) return; @@ -515,7 +517,7 @@ namespace Barotrauma.Networking serverEvent.Write(buffer, recipient); } - protected void ReadEvent(NetBuffer buffer, INetSerializable entity, Client sender = null) + protected void ReadEvent(IReadMessage buffer, INetSerializable entity, Client sender = null) { var clientEntity = entity as IClientSerializable; if (clientEntity == null) return; diff --git a/Barotrauma/BarotraumaServer/Source/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaServer/Source/Networking/OrderChatMessage.cs index 8b4cc65a7..78d625989 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/OrderChatMessage.cs @@ -1,13 +1,12 @@ using System; using System.Collections.Generic; using System.Text; -using Lidgren.Network; namespace Barotrauma.Networking { partial class OrderChatMessage : ChatMessage { - public override void ServerWrite(NetOutgoingMessage msg, Client c) + public override void ServerWrite(IWriteMessage msg, Client c) { msg.Write((byte)ServerNetObject.CHAT_MESSAGE); msg.Write(NetStateID); diff --git a/Barotrauma/BarotraumaServer/Source/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs b/Barotrauma/BarotraumaServer/Source/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs new file mode 100644 index 000000000..2f78ec9e4 --- /dev/null +++ b/Barotrauma/BarotraumaServer/Source/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs @@ -0,0 +1,674 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Linq; +using Lidgren.Network; +using Facepunch.Steamworks; + +namespace Barotrauma.Networking +{ + class LidgrenServerPeer : ServerPeer + { + private ServerSettings serverSettings; + + private NetPeerConfiguration netPeerConfiguration; + private NetServer netServer; + + private Facepunch.Steamworks.Server steamServer; + + private class PendingClient + { + public string Name; + public int OwnerKey; + public NetConnection Connection; + public ConnectionInitialization InitializationStep; + public double UpdateTime; + public double TimeOut; + public int Retries; + public UInt64? SteamID; + public Int32? PasswordSalt; + public bool AuthSessionStarted; + + public PendingClient(NetConnection conn) + { + OwnerKey = 0; + Connection = conn; + InitializationStep = ConnectionInitialization.SteamTicketAndVersion; + Retries = 0; + SteamID = null; + PasswordSalt = null; + UpdateTime = Timing.TotalTime; + TimeOut = 20.0; + AuthSessionStarted = false; + } + } + + private List connectedClients; + private List pendingClients; + + private List incomingLidgrenMessages; + + public LidgrenServerPeer(int? ownKey, ServerSettings settings) + { + serverSettings = settings; + + netServer = null; + + connectedClients = new List(); + pendingClients = new List(); + + incomingLidgrenMessages = new List(); + + steamServer = null; + + ownerKey = ownKey; + } + + public override void Start() + { + if (netServer != null) { return; } + + netPeerConfiguration = new NetPeerConfiguration("barotrauma"); + netPeerConfiguration.AcceptIncomingConnections = true; + netPeerConfiguration.AutoExpandMTU = false; + netPeerConfiguration.MaximumConnections = serverSettings.MaxPlayers * 2; + netPeerConfiguration.EnableUPnP = serverSettings.EnableUPnP; + netPeerConfiguration.Port = serverSettings.Port; + + netPeerConfiguration.DisableMessageType(NetIncomingMessageType.DebugMessage | + NetIncomingMessageType.WarningMessage | NetIncomingMessageType.Receipt | + NetIncomingMessageType.ErrorMessage | NetIncomingMessageType.Error | + NetIncomingMessageType.UnconnectedData); + + netPeerConfiguration.EnableMessageType(NetIncomingMessageType.ConnectionApproval); + + netServer = new NetServer(netPeerConfiguration); + + netServer.Start(); + + if (serverSettings.EnableUPnP) + { + InitUPnP(); + + while (DiscoveringUPnP()) { } + + FinishUPnP(); + } + } + + public override void Close(string msg=null) + { + if (netServer == null) { return; } + + for (int i=pendingClients.Count-1;i>=0;i--) + { + RemovePendingClient(pendingClients[i], msg ?? DisconnectReason.ServerShutdown.ToString()); + } + + for (int i=connectedClients.Count-1;i>=0;i--) + { + Disconnect(connectedClients[i], msg ?? DisconnectReason.ServerShutdown.ToString()); + } + + netServer.Shutdown(msg ?? DisconnectReason.ServerShutdown.ToString()); + + pendingClients.Clear(); + connectedClients.Clear(); + + netServer = null; + + if (steamServer != null) + { + steamServer.Auth.OnAuthChange = null; + } + steamServer = null; + + OnShutdown?.Invoke(); + } + + public override void Update(float deltaTime) + { + if (netServer == null) { return; } + + if (OnOwnerDetermined != null && OwnerConnection != null) + { + OnOwnerDetermined?.Invoke(OwnerConnection); + OnOwnerDetermined = null; + } + + netServer.ReadMessages(incomingLidgrenMessages); + + //process incoming connections first + foreach (NetIncomingMessage inc in incomingLidgrenMessages.Where(m => m.MessageType == NetIncomingMessageType.ConnectionApproval)) + { + HandleConnection(inc); + } + + try + { + //after processing connections, go ahead with the rest of the messages + foreach (NetIncomingMessage inc in incomingLidgrenMessages.Where(m => m.MessageType != NetIncomingMessageType.ConnectionApproval)) + { + switch (inc.MessageType) + { + case NetIncomingMessageType.Data: + HandleDataMessage(inc); + break; + case NetIncomingMessageType.StatusChanged: + HandleStatusChanged(inc); + break; + } + } + } + + catch (Exception e) + { + string errorMsg = "Server failed to read an incoming message. {" + e + "}\n" + e.StackTrace; + GameAnalyticsManager.AddErrorEventOnce("LidgrenServerPeer.Update:ClientReadException" + e.TargetSite.ToString(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); +#if DEBUG + DebugConsole.ThrowError(errorMsg); +#else + if (GameSettings.VerboseLogging) { DebugConsole.ThrowError(errorMsg); } +#endif + } + + for (int i = 0; i < pendingClients.Count; i++) + { + PendingClient pendingClient = pendingClients[i]; + UpdatePendingClient(pendingClient, deltaTime); + if (i >= pendingClients.Count || pendingClients[i] != pendingClient) { i--; } + } + + incomingLidgrenMessages.Clear(); + } + + private void InitUPnP() + { + if (netServer == null) { return; } + + netServer.UPnP.ForwardPort(netPeerConfiguration.Port, "barotrauma"); + if (Steam.SteamManager.USE_STEAM) + { + netServer.UPnP.ForwardPort(serverSettings.QueryPort, "barotrauma"); + } + } + + private bool DiscoveringUPnP() + { + if (netServer == null) { return false; } + + return netServer.UPnP.Status == UPnPStatus.Discovering; + } + + private void FinishUPnP() + { + //do nothing + } + + private void HandleConnection(NetIncomingMessage inc) + { + if (netServer == null) { return; } + + if (connectedClients.Count >= serverSettings.MaxPlayers) + { + inc.SenderConnection.Deny(DisconnectReason.ServerFull.ToString()); + return; + } + + if (serverSettings.BanList.IsBanned(inc.SenderConnection.RemoteEndPoint.Address, 0)) + { + //IP banned: deny immediately + //TODO: use TextManager + inc.SenderConnection.Deny(DisconnectReason.Banned.ToString()+"/ IP banned"); + return; + } + + PendingClient pendingClient = pendingClients.Find(c => c.Connection == inc.SenderConnection); + + if (pendingClient == null) + { + pendingClient = new PendingClient(inc.SenderConnection); + pendingClients.Add(pendingClient); + } + + inc.SenderConnection.Approve(); + } + + private void HandleDataMessage(NetIncomingMessage inc) + { + if (netServer == null) { return; } + + PendingClient pendingClient = pendingClients.Find(c => c.Connection == inc.SenderConnection); + + byte incByte = inc.ReadByte(); + bool isCompressed = (incByte & (byte)PacketHeader.IsCompressed) != 0; + bool isConnectionInitializationStep = (incByte & (byte)PacketHeader.IsConnectionInitializationStep) != 0; + + if (isConnectionInitializationStep && pendingClient != null) + { + ReadConnectionInitializationStep(pendingClient, inc); + } + else if (!isConnectionInitializationStep) + { + LidgrenConnection conn = connectedClients.Find(c => c.NetConnection == inc.SenderConnection); + if (conn == null) + { + if (pendingClient != null) + { + RemovePendingClient(pendingClient, DisconnectReason.AuthenticationRequired.ToString()+"/ Received data message from unauthenticated client"); + } + else if (inc.SenderConnection.Status != NetConnectionStatus.Disconnected && + inc.SenderConnection.Status != NetConnectionStatus.Disconnecting) + { + inc.SenderConnection.Disconnect(DisconnectReason.AuthenticationRequired.ToString() + "/ Received data message from unauthenticated client"); + } + return; + } + if (pendingClient != null) { pendingClients.Remove(pendingClient); } + if (serverSettings.BanList.IsBanned(conn.IPEndPoint.Address, conn.SteamID)) + { + Disconnect(conn, DisconnectReason.Banned.ToString()+"/ Received data message from banned client"); + return; + } + UInt16 length = inc.ReadUInt16(); + + //DebugConsole.NewMessage(isCompressed + " " + isConnectionInitializationStep + " " + (int)incByte + " " + length); + + IReadMessage msg = new ReadOnlyMessage(inc.Data, isCompressed, inc.PositionInBytes, length, conn); + OnMessageReceived?.Invoke(conn, msg); + } + } + + private void HandleStatusChanged(NetIncomingMessage inc) + { + if (netServer == null) { return; } + + switch (inc.SenderConnection.Status) + { + case NetConnectionStatus.Disconnected: + string disconnectMsg; + LidgrenConnection conn = connectedClients.Find(c => c.NetConnection == inc.SenderConnection); + if (conn != null) + { + if (conn == OwnerConnection) + { + DebugConsole.NewMessage("Owner disconnected: closing the server..."); + GameServer.Log("Owner disconnected: closing the server...", ServerLog.MessageType.ServerMessage); + Close(DisconnectReason.ServerShutdown.ToString() + "/ Owner disconnected"); + } + else + { + disconnectMsg = $"ServerMessage.HasDisconnected~[client]={conn.Name}"; + Disconnect(conn, disconnectMsg); + } + } + else + { + PendingClient pendingClient = pendingClients.Find(c => c.Connection == inc.SenderConnection); + if (pendingClient != null) + { + disconnectMsg = $"ServerMessage.HasDisconnected~[client]={pendingClient.Name}"; + RemovePendingClient(pendingClient, disconnectMsg); + } + } + break; + } + } + + private void ReadConnectionInitializationStep(PendingClient pendingClient, NetIncomingMessage inc) + { + if (netServer == null) { return; } + + pendingClient.TimeOut = 20.0; + + ConnectionInitialization initializationStep = (ConnectionInitialization)inc.ReadByte(); + + //DebugConsole.NewMessage(initializationStep+" "+pendingClient.InitializationStep); + + if (pendingClient.InitializationStep != initializationStep) return; + + switch (initializationStep) + { + case ConnectionInitialization.SteamTicketAndVersion: + string name = Client.SanitizeName(inc.ReadString()); + int ownKey = inc.ReadInt32(); + UInt64 steamId = inc.ReadUInt64(); + UInt16 ticketLength = inc.ReadUInt16(); + byte[] ticket = inc.ReadBytes(ticketLength); + + if (!Client.IsValidName(name, serverSettings)) + { + if (OwnerConnection != null || + !IPAddress.IsLoopback(pendingClient.Connection.RemoteEndPoint.Address.MapToIPv4()) && + ownerKey == null || ownKey == 0 && ownKey != ownerKey) + { + RemovePendingClient(pendingClient, DisconnectReason.InvalidName.ToString() + "/ The name \"" + name + "\" is invalid"); + return; + } + } + + string version = inc.ReadString(); + bool isCompatibleVersion = NetworkMember.IsCompatible(version, GameMain.Version.ToString()) ?? false; + if (!isCompatibleVersion) + { + RemovePendingClient(pendingClient, + $"DisconnectMessage.InvalidVersion~[version]={GameMain.Version.ToString()}~[clientversion]={version}"); + + GameServer.Log(name + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (incompatible game version)", ServerLog.MessageType.Error); + DebugConsole.NewMessage(name + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (incompatible game version)", Microsoft.Xna.Framework.Color.Red); + return; + } + + Int32 contentPackageCount = inc.ReadVariableInt32(); + List contentPackages = new List(); + for (int i = 0; i < contentPackageCount; i++) + { + string packageName = inc.ReadString(); + string packageHash = inc.ReadString(); + contentPackages.Add(new ClientContentPackage(packageName, packageHash)); + } + + List missingPackages = new List(); + foreach (ContentPackage contentPackage in GameMain.SelectedPackages) + { + if (!contentPackage.HasMultiplayerIncompatibleContent) continue; + bool packageFound = false; + for (int i = 0; i < contentPackageCount; i++) + { + if (contentPackages[i].Name == contentPackage.Name && contentPackages[i].Hash == contentPackage.MD5hash.Hash) + { + packageFound = true; + break; + } + } + if (!packageFound) missingPackages.Add(contentPackage); + } + + if (missingPackages.Count == 1) + { + RemovePendingClient(pendingClient, + $"DisconnectMessage.MissingContentPackage~[missingcontentpackage]={GetPackageStr(missingPackages[0])}"); + GameServer.Log(name + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (missing content package " + GetPackageStr(missingPackages[0]) + ")", ServerLog.MessageType.Error); + return; + } + else if (missingPackages.Count > 1) + { + List packageStrs = new List(); + missingPackages.ForEach(cp => packageStrs.Add(GetPackageStr(cp))); + RemovePendingClient(pendingClient, + $"DisconnectMessage.MissingContentPackages~[missingcontentpackages]={string.Join(", ", packageStrs)}"); + GameServer.Log(name + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (missing content packages " + string.Join(", ", packageStrs) + ")", ServerLog.MessageType.Error); + return; + } + + if (pendingClient.SteamID == null) + { + bool requireSteamAuth = GameMain.Config.RequireSteamAuthentication; +#if DEBUG + requireSteamAuth = false; +#endif + //steam auth cannot be done (SteamManager not initialized or no ticket given), + //but it's not required either -> let the client join without auth + if ((!Steam.SteamManager.IsInitialized || ticket.Length == 0) && + !requireSteamAuth) + { + pendingClient.Name = name; + pendingClient.OwnerKey = ownKey; + pendingClient.InitializationStep = ConnectionInitialization.Success; + } + else + { + ServerAuth.StartAuthSessionResult authSessionStartState = Steam.SteamManager.StartAuthSession(ticket, steamId); + if (authSessionStartState != ServerAuth.StartAuthSessionResult.OK) + { + RemovePendingClient(pendingClient, DisconnectReason.SteamAuthenticationFailed.ToString() + "/ Steam auth session failed to start: " + authSessionStartState.ToString()); + return; + } + pendingClient.SteamID = steamId; + pendingClient.Name = name; + pendingClient.OwnerKey = ownKey; + pendingClient.AuthSessionStarted = true; + } + } + else //TODO: could remove since this seems impossible + { + if (pendingClient.SteamID != steamId) + { + RemovePendingClient(pendingClient, DisconnectReason.SteamAuthenticationFailed.ToString() + "/ SteamID mismatch"); + return; + } + } + break; + case ConnectionInitialization.Password: + int pwLength = inc.ReadByte(); + byte[] incPassword = new byte[pwLength]; + inc.ReadBytes(incPassword, 0, pwLength); + if (pendingClient.PasswordSalt == null) + { + DebugConsole.ThrowError("Received password message from client without salt"); + return; + } + if (serverSettings.IsPasswordCorrect(incPassword, pendingClient.PasswordSalt.Value)) + { + pendingClient.InitializationStep = ConnectionInitialization.Success; + } + else + { + pendingClient.Retries++; + + if (pendingClient.Retries >= 3) + { + string banMsg = "Failed to enter correct password too many times"; + if (pendingClient.SteamID != null) + { + serverSettings.BanList.BanPlayer(pendingClient.Name, pendingClient.SteamID.Value, banMsg, null); + } + serverSettings.BanList.BanPlayer(pendingClient.Name, pendingClient.Connection.RemoteEndPoint.Address, banMsg, null); + RemovePendingClient(pendingClient, DisconnectReason.Banned.ToString()+" /"+banMsg); + return; + } + } + pendingClient.UpdateTime = Timing.TotalTime; + break; + } + } + + protected struct ClientContentPackage + { + public string Name; + public string Hash; + + public ClientContentPackage(string name, string hash) + { + Name = name; Hash = hash; + } + } + + private string GetPackageStr(ContentPackage contentPackage) + { + return "\"" + contentPackage.Name + "\" (hash " + contentPackage.MD5hash.ShortHash + ")"; + } + + private void UpdatePendingClient(PendingClient pendingClient, float deltaTime) + { + if (netServer == null) { return; } + + if (serverSettings.BanList.IsBanned(pendingClient.Connection.RemoteEndPoint.Address, pendingClient.SteamID ?? 0)) + { + RemovePendingClient(pendingClient, DisconnectReason.Banned.ToString()); + return; + } + + //DebugConsole.NewMessage("pending client status: " + pendingClient.InitializationStep); + + if (connectedClients.Count >= serverSettings.MaxPlayers) + { + RemovePendingClient(pendingClient, DisconnectReason.ServerFull.ToString()); + } + + if (pendingClient.InitializationStep == ConnectionInitialization.Success) + { + LidgrenConnection newConnection = new LidgrenConnection(pendingClient.Name, pendingClient.Connection, pendingClient.SteamID ?? 0); + newConnection.Status = NetworkConnectionStatus.Connected; + connectedClients.Add(newConnection); + pendingClients.Remove(pendingClient); + + if (OwnerConnection == null && + IPAddress.IsLoopback(pendingClient.Connection.RemoteEndPoint.Address.MapToIPv4()) && + ownerKey != null && pendingClient.OwnerKey != 0 && pendingClient.OwnerKey == ownerKey) + { + ownerKey = null; + OwnerConnection = newConnection; + } + + OnInitializationComplete?.Invoke(newConnection); + } + + + pendingClient.TimeOut -= deltaTime; + if (pendingClient.TimeOut < 0.0) + { + RemovePendingClient(pendingClient, Lidgren.Network.NetConnection.NoResponseMessage); + } + + if (Timing.TotalTime < pendingClient.UpdateTime) { return; } + pendingClient.UpdateTime = Timing.TotalTime + 1.0; + + NetOutgoingMessage outMsg = netServer.CreateMessage(); + outMsg.Write((byte)PacketHeader.IsConnectionInitializationStep); + outMsg.Write((byte)pendingClient.InitializationStep); + switch (pendingClient.InitializationStep) + { + case ConnectionInitialization.Password: + outMsg.Write(pendingClient.PasswordSalt == null); outMsg.WritePadBits(); + if (pendingClient.PasswordSalt == null) + { + pendingClient.PasswordSalt = CryptoRandom.Instance.Next(); + outMsg.Write(pendingClient.PasswordSalt.Value); + } + else + { + outMsg.Write(pendingClient.Retries); + } + break; + } + + NetSendResult result = netServer.SendMessage(outMsg, pendingClient.Connection, NetDeliveryMethod.ReliableUnordered); + //DebugConsole.NewMessage("sent update to pending client: "+result); + } + + private void RemovePendingClient(PendingClient pendingClient, string reason) + { + if (netServer == null) { return; } + + if (pendingClients.Contains(pendingClient)) + { + pendingClients.Remove(pendingClient); + + if (pendingClient.AuthSessionStarted) + { + Steam.SteamManager.StopAuthSession(pendingClient.SteamID.Value); + pendingClient.SteamID = null; + pendingClient.AuthSessionStarted = false; + } + + pendingClient.Connection.Disconnect(reason); + } + } + + public override void InitializeSteamServerCallbacks(Server steamSrvr) + { + steamServer = steamSrvr; + + steamServer.Auth.OnAuthChange = OnAuthChange; + } + + private void OnAuthChange(ulong steamID, ulong ownerID, ServerAuth.Status status) + { + if (netServer == null) { return; } + + PendingClient pendingClient = pendingClients.Find(c => c.SteamID == steamID); + DebugConsole.NewMessage(steamID + " validation: " + status+", "+(pendingClient!=null)); + + if (pendingClient == null) + { + if (status != ServerAuth.Status.OK) + { + LidgrenConnection connection = connectedClients.Find(c => c.SteamID == steamID); + if (connection != null) + { + Disconnect(connection, DisconnectReason.SteamAuthenticationFailed.ToString() + "/ Steam authentication status changed: " + status.ToString()); + } + } + return; + } + + if (serverSettings.BanList.IsBanned(pendingClient.Connection.RemoteEndPoint.Address, steamID)) + { + RemovePendingClient(pendingClient, DisconnectReason.Banned.ToString() + "/ SteamID banned"); + return; + } + + if (status == ServerAuth.Status.OK) + { + pendingClient.InitializationStep = serverSettings.HasPassword ? ConnectionInitialization.Password : ConnectionInitialization.Success; + pendingClient.UpdateTime = Timing.TotalTime; + } + else + { + RemovePendingClient(pendingClient, DisconnectReason.SteamAuthenticationFailed.ToString() + "/ Steam authentication failed: " + status.ToString()); + return; + } + } + + public override void Send(IWriteMessage msg, NetworkConnection conn, DeliveryMethod deliveryMethod) + { + if (netServer == null) { return; } + + if (!(conn is LidgrenConnection lidgrenConn)) return; + if (!connectedClients.Contains(lidgrenConn)) + { + DebugConsole.ThrowError("Tried to send message to unauthenticated connection: " + lidgrenConn.IPString); + return; + } + + NetDeliveryMethod lidgrenDeliveryMethod = NetDeliveryMethod.Unreliable; + switch (deliveryMethod) + { + case DeliveryMethod.Unreliable: + lidgrenDeliveryMethod = NetDeliveryMethod.Unreliable; + break; + case DeliveryMethod.Reliable: + lidgrenDeliveryMethod = NetDeliveryMethod.ReliableUnordered; + break; + case DeliveryMethod.ReliableOrdered: + lidgrenDeliveryMethod = NetDeliveryMethod.ReliableOrdered; + break; + } + + NetOutgoingMessage lidgrenMsg = netServer.CreateMessage(); + byte[] msgData = new byte[msg.LengthBytes]; + msg.PrepareForSending(ref msgData, out bool isCompressed, out int length); + lidgrenMsg.Write((byte)(isCompressed ? PacketHeader.IsCompressed : PacketHeader.None)); + lidgrenMsg.Write((UInt16)length); + lidgrenMsg.Write(msgData, 0, length); + + netServer.SendMessage(lidgrenMsg, lidgrenConn.NetConnection, lidgrenDeliveryMethod); + } + + public override void Disconnect(NetworkConnection conn,string msg=null) + { + if (netServer == null) { return; } + + if (!(conn is LidgrenConnection lidgrenConn)) { return; } + if (connectedClients.Contains(lidgrenConn)) + { + lidgrenConn.Status = NetworkConnectionStatus.Disconnected; + connectedClients.Remove(lidgrenConn); + OnDisconnect?.Invoke(conn, msg); + Steam.SteamManager.StopAuthSession(conn.SteamID); + } + lidgrenConn.NetConnection.Disconnect(msg ?? "Disconnected"); + } + } +} diff --git a/Barotrauma/BarotraumaServer/Source/Networking/Primitives/Peers/Server/ServerPeer.cs b/Barotrauma/BarotraumaServer/Source/Networking/Primitives/Peers/Server/ServerPeer.cs new file mode 100644 index 000000000..62ac3fc0b --- /dev/null +++ b/Barotrauma/BarotraumaServer/Source/Networking/Primitives/Peers/Server/ServerPeer.cs @@ -0,0 +1,34 @@ +using Facepunch.Steamworks; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Barotrauma.Networking +{ + abstract class ServerPeer + { + public delegate void MessageCallback(NetworkConnection connection, IReadMessage message); + public delegate void DisconnectCallback(NetworkConnection connection, string reason); + public delegate void InitializationCompleteCallback(NetworkConnection connection); + public delegate void ShutdownCallback(); + public delegate void OwnerDeterminedCallback(NetworkConnection connection); + + public MessageCallback OnMessageReceived; + public DisconnectCallback OnDisconnect; + public InitializationCompleteCallback OnInitializationComplete; + public ShutdownCallback OnShutdown; + public OwnerDeterminedCallback OnOwnerDetermined; + + protected int? ownerKey; + + public NetworkConnection OwnerConnection { get; protected set; } + + public abstract void InitializeSteamServerCallbacks(Facepunch.Steamworks.Server steamSrvr); + + public abstract void Start(); + public abstract void Close(string msg = null); + public abstract void Update(float deltaTime); + public abstract void Send(IWriteMessage msg, NetworkConnection conn, DeliveryMethod deliveryMethod); + public abstract void Disconnect(NetworkConnection conn, string msg = null); + } +} diff --git a/Barotrauma/BarotraumaServer/Source/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs b/Barotrauma/BarotraumaServer/Source/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs new file mode 100644 index 000000000..afa84a137 --- /dev/null +++ b/Barotrauma/BarotraumaServer/Source/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs @@ -0,0 +1,652 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Linq; +using System.Threading; +using Lidgren.Network; +using Facepunch.Steamworks; + +namespace Barotrauma.Networking +{ + class SteamP2PServerPeer : ServerPeer + { + private ServerSettings serverSettings; + + private NetPeerConfiguration netPeerConfiguration; + private NetServer netServer; + + private NetConnection netConnection; + + public UInt64 OwnerSteamID + { + get; + private set; + } + + private class PendingClient + { + public string Name; + public ConnectionInitialization InitializationStep; + public double UpdateTime; + public double TimeOut; + public int Retries; + public UInt64 SteamID; + public Int32? PasswordSalt; + public bool AuthSessionStarted; + + public PendingClient(UInt64 steamId) + { + InitializationStep = ConnectionInitialization.SteamTicketAndVersion; + Retries = 0; + SteamID = steamId; + PasswordSalt = null; + UpdateTime = Timing.TotalTime; + TimeOut = 20.0; + AuthSessionStarted = false; + } + + public void Heartbeat() + { + TimeOut = 5.0; + } + } + + private List connectedClients; + private List pendingClients; + + private List incomingLidgrenMessages; + + public SteamP2PServerPeer(UInt64 steamId, ServerSettings settings) + { + serverSettings = settings; + + netServer = null; + + connectedClients = new List(); + pendingClients = new List(); + + incomingLidgrenMessages = new List(); + + ownerKey = null; + + OwnerSteamID = steamId; + } + + public override void Start() + { + if (netServer != null) { return; } + + netPeerConfiguration = new NetPeerConfiguration("barotrauma") + { + AcceptIncomingConnections = true, + AutoExpandMTU = false, + MaximumConnections = 1, //only allow owner to connect + EnableUPnP = false, + Port = Steam.SteamManager.STEAMP2P_OWNER_PORT + }; + + netPeerConfiguration.DisableMessageType(NetIncomingMessageType.DebugMessage | + NetIncomingMessageType.WarningMessage | NetIncomingMessageType.Receipt | + NetIncomingMessageType.ErrorMessage | NetIncomingMessageType.Error | + NetIncomingMessageType.UnconnectedData); + + netPeerConfiguration.EnableMessageType(NetIncomingMessageType.ConnectionApproval); + + netServer = new NetServer(netPeerConfiguration); + + netServer.Start(); + } + + public override void Close(string msg = null) + { + if (netServer == null) { return; } + + if (OwnerConnection != null) OwnerConnection.Status = NetworkConnectionStatus.Disconnected; + + for (int i = pendingClients.Count - 1; i >= 0; i--) + { + RemovePendingClient(pendingClients[i], msg ?? DisconnectReason.ServerShutdown.ToString()); + } + + for (int i = connectedClients.Count - 1; i >= 0; i--) + { + Disconnect(connectedClients[i], msg ?? DisconnectReason.ServerShutdown.ToString()); + } + + netServer.Shutdown(msg ?? DisconnectReason.ServerShutdown.ToString()); + + pendingClients.Clear(); + connectedClients.Clear(); + + netServer = null; + + OnShutdown?.Invoke(); + } + + public override void Update(float deltaTime) + { + if (netServer == null) { return; } + + if (OnOwnerDetermined != null && OwnerConnection != null) + { + OnOwnerDetermined?.Invoke(OwnerConnection); + OnOwnerDetermined = null; + } + + netServer.ReadMessages(incomingLidgrenMessages); + + //backwards for loop so we can remove elements while iterating + for (int i = connectedClients.Count - 1; i >= 0; i--) + { + connectedClients[i].Decay(deltaTime); + if (connectedClients[i].Timeout < 0.0) + { + Disconnect(connectedClients[i], "Timed out"); + } + } + + //process incoming connections first + foreach (NetIncomingMessage inc in incomingLidgrenMessages.Where(m => m.MessageType == NetIncomingMessageType.ConnectionApproval)) + { + HandleConnection(inc); + } + + + try + { + //after processing connections, go ahead with the rest of the messages + foreach (NetIncomingMessage inc in incomingLidgrenMessages.Where(m => m.MessageType != NetIncomingMessageType.ConnectionApproval)) + { + switch (inc.MessageType) + { + case NetIncomingMessageType.Data: + HandleDataMessage(inc); + break; + case NetIncomingMessageType.StatusChanged: + HandleStatusChanged(inc); + break; + } + } + } + + catch (Exception e) + { + string errorMsg = "Server failed to read an incoming message. {" + e + "}\n" + e.StackTrace; + GameAnalyticsManager.AddErrorEventOnce("SteamP2PServerPeer.Update:ClientReadException" + e.TargetSite.ToString(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); +#if DEBUG + DebugConsole.ThrowError(errorMsg); +#else + if (GameSettings.VerboseLogging) { DebugConsole.ThrowError(errorMsg); } +#endif + } + + for (int i = 0; i < pendingClients.Count; i++) + { + PendingClient pendingClient = pendingClients[i]; + UpdatePendingClient(pendingClient); + if (i >= pendingClients.Count || pendingClients[i] != pendingClient) { i--; } + } + + incomingLidgrenMessages.Clear(); + } + + private void HandleConnection(NetIncomingMessage inc) + { + if (netServer == null) { return; } + + if (netConnection != null && inc.SenderConnection != netConnection) + { + inc.SenderConnection.Deny(DisconnectReason.SessionTaken.ToString()+"/ Owner is already connected"); + return; + } + + if (IPAddress.IsLoopback(inc.SenderConnection.RemoteEndPoint.Address.MapToIPv4())) + { + inc.SenderConnection.Approve(); + netConnection = inc.SenderConnection; + + return; + } + + inc.SenderConnection.Deny(DisconnectReason.Kicked.ToString()+"/ Incoming connection is not loopback"); + } + + private void HandleDataMessage(NetIncomingMessage inc) + { + if (netServer == null) { return; } + + if (inc.SenderConnection != netConnection) { return; } + + UInt64 senderSteamId = inc.ReadUInt64(); + + byte incByte = inc.ReadByte(); + bool isCompressed = (incByte & (byte)PacketHeader.IsCompressed) != 0; + bool isConnectionInitializationStep = (incByte & (byte)PacketHeader.IsConnectionInitializationStep) != 0; + bool isDisconnectMessage = (incByte & (byte)PacketHeader.IsDisconnectMessage) != 0; + bool isServerMessage = (incByte & (byte)PacketHeader.IsServerMessage) != 0; + bool isHeartbeatMessage = (incByte & (byte)PacketHeader.IsHeartbeatMessage) != 0; + + if (isServerMessage) + { + DebugConsole.ThrowError("got server message from" + senderSteamId.ToString()); + return; + } + + if (senderSteamId != OwnerSteamID) //sender is remote, handle disconnects and heartbeats + { + PendingClient pendingClient = pendingClients.Find(c => c.SteamID == senderSteamId); + SteamP2PConnection connectedClient = connectedClients.Find(c => c.SteamID == senderSteamId); + + pendingClient?.Heartbeat(); + connectedClient?.Heartbeat(); + + if (serverSettings.BanList.IsBanned(senderSteamId)) + { + if (pendingClient != null) + { + RemovePendingClient(pendingClient, DisconnectReason.Banned.ToString()+"/ Banned"); + } + else if (connectedClient != null) + { + Disconnect(connectedClient, DisconnectReason.Banned.ToString() + "/ Banned"); + } + return; + } + else if (isDisconnectMessage) + { + if (pendingClient != null) + { + string disconnectMsg = $"ServerMessage.HasDisconnected~[client]={pendingClient.Name}"; + RemovePendingClient(pendingClient, disconnectMsg); + } + else if (connectedClient != null) + { + string disconnectMsg = $"ServerMessage.HasDisconnected~[client]={connectedClient.Name}"; + Disconnect(connectedClient, disconnectMsg, false); + } + return; + } + else if (isHeartbeatMessage) + { + //message exists solely as a heartbeat, ignore its contents + return; + } + else if (isConnectionInitializationStep) + { + if (pendingClient != null) + { + ReadConnectionInitializationStep(pendingClient, new ReadOnlyMessage(inc.Data, false, inc.PositionInBytes, inc.LengthBytes - inc.PositionInBytes, null)); + } + else + { + ConnectionInitialization initializationStep = (ConnectionInitialization)inc.ReadByte(); + if (initializationStep == ConnectionInitialization.ConnectionStarted) + { + pendingClients.Add(new PendingClient(senderSteamId)); + } + } + } + else if (connectedClient != null) + { + UInt16 length = inc.ReadUInt16(); + + IReadMessage msg = new ReadOnlyMessage(inc.Data, isCompressed, inc.PositionInBytes, length, connectedClient); + OnMessageReceived?.Invoke(connectedClient, msg); + } + } + else //sender is owner + { + if (OwnerConnection != null) { (OwnerConnection as SteamP2PConnection).Heartbeat(); } + + if (isDisconnectMessage) + { + DebugConsole.ThrowError("Received disconnect message from owner"); + return; + } + if (isServerMessage) + { + DebugConsole.ThrowError("Received server message from owner"); + return; + } + if (isConnectionInitializationStep) + { + if (OwnerConnection == null) + { + string ownerName = inc.ReadString(); + OwnerConnection = new SteamP2PConnection(ownerName, OwnerSteamID); + OwnerConnection.Status = NetworkConnectionStatus.Connected; + + OnInitializationComplete?.Invoke(OwnerConnection); + } + return; + } + if (isHeartbeatMessage) + { + return; + } + else + { + UInt16 length = inc.ReadUInt16(); + + IReadMessage msg = new ReadOnlyMessage(inc.Data, isCompressed, inc.PositionInBytes, length, OwnerConnection); + OnMessageReceived?.Invoke(OwnerConnection, msg); + } + } + } + + private void HandleStatusChanged(NetIncomingMessage inc) + { + if (netServer == null) { return; } + + DebugConsole.NewMessage(inc.SenderConnection.Status.ToString()); + + switch (inc.SenderConnection.Status) + { + case NetConnectionStatus.Connected: + NetOutgoingMessage outMsg = netServer.CreateMessage(); + outMsg.Write(OwnerSteamID); + outMsg.Write((byte)(PacketHeader.IsConnectionInitializationStep | PacketHeader.IsServerMessage)); + netServer.SendMessage(outMsg, netConnection, NetDeliveryMethod.ReliableUnordered); + break; + case NetConnectionStatus.Disconnected: + DebugConsole.NewMessage("Owner disconnected: closing the server..."); + GameServer.Log("Owner disconnected: closing the server...", ServerLog.MessageType.ServerMessage); + Close(DisconnectReason.ServerShutdown.ToString() + "/ Owner disconnected"); + break; + } + } + + private void ReadConnectionInitializationStep(PendingClient pendingClient, IReadMessage inc) + { + if (netServer == null) { return; } + + pendingClient.TimeOut = 20.0; + + ConnectionInitialization initializationStep = (ConnectionInitialization)inc.ReadByte(); + + //DebugConsole.NewMessage(initializationStep+" "+pendingClient.InitializationStep); + + if (pendingClient.InitializationStep != initializationStep) return; + + switch (initializationStep) + { + case ConnectionInitialization.SteamTicketAndVersion: + string name = Client.SanitizeName(inc.ReadString()); + UInt64 steamId = inc.ReadUInt64(); + UInt16 ticketLength = inc.ReadUInt16(); + inc.BitPosition += ticketLength * 8; //skip ticket, owner handles steam authentication + + if (!Client.IsValidName(name, serverSettings)) + { + RemovePendingClient(pendingClient, DisconnectReason.InvalidName.ToString() + "/ The name \"" + name + "\" is invalid"); + return; + } + + string version = inc.ReadString(); + bool isCompatibleVersion = NetworkMember.IsCompatible(version, GameMain.Version.ToString()) ?? false; + if (!isCompatibleVersion) + { + RemovePendingClient(pendingClient, + $"DisconnectMessage.InvalidVersion~[version]={GameMain.Version.ToString()}~[clientversion]={version}"); + + GameServer.Log(name + " (" + pendingClient.SteamID.ToString() + ") couldn't join the server (incompatible game version)", ServerLog.MessageType.Error); + DebugConsole.NewMessage(name + " (" + pendingClient.SteamID.ToString() + ") couldn't join the server (incompatible game version)", Microsoft.Xna.Framework.Color.Red); + return; + } + + int contentPackageCount = (int)inc.ReadVariableUInt32(); + List contentPackages = new List(); + for (int i = 0; i < contentPackageCount; i++) + { + string packageName = inc.ReadString(); + string packageHash = inc.ReadString(); + contentPackages.Add(new ClientContentPackage(packageName, packageHash)); + } + + List missingPackages = new List(); + foreach (ContentPackage contentPackage in GameMain.SelectedPackages) + { + if (!contentPackage.HasMultiplayerIncompatibleContent) continue; + bool packageFound = false; + for (int i = 0; i < (int)contentPackageCount; i++) + { + if (contentPackages[i].Name == contentPackage.Name && contentPackages[i].Hash == contentPackage.MD5hash.Hash) + { + packageFound = true; + break; + } + } + if (!packageFound) missingPackages.Add(contentPackage); + } + + if (missingPackages.Count == 1) + { + RemovePendingClient(pendingClient, + $"DisconnectMessage.MissingContentPackage~[missingcontentpackage]={GetPackageStr(missingPackages[0])}"); + GameServer.Log(name + " (" + pendingClient.SteamID.ToString() + ") couldn't join the server (missing content package " + GetPackageStr(missingPackages[0]) + ")", ServerLog.MessageType.Error); + return; + } + else if (missingPackages.Count > 1) + { + List packageStrs = new List(); + missingPackages.ForEach(cp => packageStrs.Add(GetPackageStr(cp))); + RemovePendingClient(pendingClient, + $"DisconnectMessage.MissingContentPackages~[missingcontentpackages]={string.Join(", ", packageStrs)}"); + GameServer.Log(name + " (" + pendingClient.SteamID.ToString() + ") couldn't join the server (missing content packages " + string.Join(", ", packageStrs) + ")", ServerLog.MessageType.Error); + return; + } + + if (!pendingClient.AuthSessionStarted) + { + pendingClient.InitializationStep = serverSettings.HasPassword ? ConnectionInitialization.Password: ConnectionInitialization.Success; + + pendingClient.Name = name; + pendingClient.AuthSessionStarted = true; + } + break; + case ConnectionInitialization.Password: + int pwLength = inc.ReadByte(); + byte[] incPassword = inc.ReadBytes(pwLength); + if (pendingClient.PasswordSalt == null) + { + DebugConsole.ThrowError("Received password message from client without salt"); + return; + } + if (serverSettings.IsPasswordCorrect(incPassword, pendingClient.PasswordSalt.Value)) + { + pendingClient.InitializationStep = ConnectionInitialization.Success; + } + else + { + pendingClient.Retries++; + + if (pendingClient.Retries >= 3) + { + string banMsg = "Failed to enter correct password too many times"; + serverSettings.BanList.BanPlayer(pendingClient.Name, pendingClient.SteamID, banMsg, null); + + RemovePendingClient(pendingClient, DisconnectReason.Banned.ToString()+"/ "+banMsg); + return; + } + } + pendingClient.UpdateTime = Timing.TotalTime; + break; + } + } + + protected struct ClientContentPackage + { + public string Name; + public string Hash; + + public ClientContentPackage(string name, string hash) + { + Name = name; Hash = hash; + } + } + + private string GetPackageStr(ContentPackage contentPackage) + { + return "\"" + contentPackage.Name + "\" (hash " + contentPackage.MD5hash.ShortHash + ")"; + } + + private void UpdatePendingClient(PendingClient pendingClient) + { + if (netServer == null) { return; } + + if (serverSettings.BanList.IsBanned(pendingClient.SteamID)) + { + RemovePendingClient(pendingClient, DisconnectReason.Banned.ToString()+"/ Initialization interrupted by ban"); + return; + } + + //DebugConsole.NewMessage("pending client status: " + pendingClient.InitializationStep); + + if (connectedClients.Count >= serverSettings.MaxPlayers-1) + { + RemovePendingClient(pendingClient, DisconnectReason.ServerFull.ToString()); + } + + if (pendingClient.InitializationStep == ConnectionInitialization.Success) + { + SteamP2PConnection newConnection = new SteamP2PConnection(pendingClient.Name, pendingClient.SteamID); + newConnection.Status = NetworkConnectionStatus.Connected; + connectedClients.Add(newConnection); + pendingClients.Remove(pendingClient); + OnInitializationComplete?.Invoke(newConnection); + } + + pendingClient.TimeOut -= Timing.Step; + if (pendingClient.TimeOut < 0.0) + { + RemovePendingClient(pendingClient, Lidgren.Network.NetConnection.NoResponseMessage); + } + + if (Timing.TotalTime < pendingClient.UpdateTime) { return; } + pendingClient.UpdateTime = Timing.TotalTime + 1.0; + + NetOutgoingMessage outMsg = netServer.CreateMessage(); + outMsg.Write(pendingClient.SteamID); + outMsg.Write((byte)(PacketHeader.IsConnectionInitializationStep | + PacketHeader.IsServerMessage)); + outMsg.Write((byte)pendingClient.InitializationStep); + switch (pendingClient.InitializationStep) + { + case ConnectionInitialization.Password: + outMsg.Write(pendingClient.PasswordSalt == null); outMsg.WritePadBits(); + if (pendingClient.PasswordSalt == null) + { + pendingClient.PasswordSalt = CryptoRandom.Instance.Next(); + outMsg.Write(pendingClient.PasswordSalt.Value); + } + else + { + outMsg.Write(pendingClient.Retries); + } + break; + } + + if (netConnection != null) + { + NetSendResult result = netServer.SendMessage(outMsg, netConnection, NetDeliveryMethod.ReliableUnordered); + } + } + + private void RemovePendingClient(PendingClient pendingClient, string reason) + { + if (netServer == null) { return; } + + if (pendingClients.Contains(pendingClient)) + { + SendDisconnectMessage(pendingClient.SteamID, reason); + + pendingClients.Remove(pendingClient); + + if (pendingClient.AuthSessionStarted) + { + Steam.SteamManager.StopAuthSession(pendingClient.SteamID); + pendingClient.SteamID = 0; + pendingClient.AuthSessionStarted = false; + } + } + } + + public override void InitializeSteamServerCallbacks(Server steamSrvr) + { + throw new InvalidOperationException("Called InitializeSteamServerCallbacks on SteamP2PServerPeer!"); + } + + public override void Send(IWriteMessage msg, NetworkConnection conn, DeliveryMethod deliveryMethod) + { + if (netServer == null) { return; } + + if (!(conn is SteamP2PConnection steamp2pConn)) return; + if (!connectedClients.Contains(steamp2pConn) && conn != OwnerConnection) + { + DebugConsole.ThrowError("Tried to send message to unauthenticated connection: " + steamp2pConn.SteamID.ToString()); + return; + } + + NetDeliveryMethod lidgrenDeliveryMethod = NetDeliveryMethod.Unreliable; + switch (deliveryMethod) + { + case DeliveryMethod.Unreliable: + lidgrenDeliveryMethod = NetDeliveryMethod.Unreliable; + break; + case DeliveryMethod.Reliable: + lidgrenDeliveryMethod = NetDeliveryMethod.ReliableUnordered; + break; + case DeliveryMethod.ReliableOrdered: + lidgrenDeliveryMethod = NetDeliveryMethod.ReliableOrdered; + break; + } + + NetOutgoingMessage lidgrenMsg = netServer.CreateMessage(); + byte[] msgData = new byte[1500]; + msg.PrepareForSending(ref msgData, out bool isCompressed, out int length); + lidgrenMsg.Write(conn.SteamID); + lidgrenMsg.Write((byte)((isCompressed ? PacketHeader.IsCompressed : PacketHeader.None) | PacketHeader.IsServerMessage)); + lidgrenMsg.Write((UInt16)length); + lidgrenMsg.Write(msgData, 0, length); + + netServer.SendMessage(lidgrenMsg, netConnection, lidgrenDeliveryMethod); + } + + private void SendDisconnectMessage(UInt64 steamId, string msg) + { + if (netServer == null) { return; } + if (string.IsNullOrWhiteSpace(msg)) { return; } + + NetOutgoingMessage lidgrenMsg = netServer.CreateMessage(); + lidgrenMsg.Write(steamId); + lidgrenMsg.Write((byte)(PacketHeader.IsDisconnectMessage | PacketHeader.IsServerMessage)); + lidgrenMsg.Write(msg); + + netServer.SendMessage(lidgrenMsg, netConnection, NetDeliveryMethod.ReliableUnordered); + } + + private void Disconnect(NetworkConnection conn, string msg, bool sendDisconnectMessage) + { + if (netServer == null) { return; } + + if (!(conn is SteamP2PConnection steamp2pConn)) { return; } + if (connectedClients.Contains(steamp2pConn)) + { + if (sendDisconnectMessage) SendDisconnectMessage(steamp2pConn.SteamID, msg); + steamp2pConn.Status = NetworkConnectionStatus.Disconnected; + connectedClients.Remove(steamp2pConn); + OnDisconnect?.Invoke(conn, msg); + Steam.SteamManager.StopAuthSession(conn.SteamID); + } + else if (steamp2pConn == OwnerConnection) + { + netConnection.Disconnect(msg); + } + } + + public override void Disconnect(NetworkConnection conn, string msg = null) + { + Disconnect(conn, msg, true); + } + } +} diff --git a/Barotrauma/BarotraumaServer/Source/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/Source/Networking/RespawnManager.cs index 019db3603..c280a0463 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/RespawnManager.cs @@ -27,8 +27,8 @@ namespace Barotrauma.Networking .ToList(); } - int currPlayerCount = GameMain.Server.ConnectedClients.Count(c => - c.InGame && + int currPlayerCount = GameMain.Server.ConnectedClients.Count(c => + c.InGame && (!c.SpectateOnly || (!GameMain.Server.ServerSettings.AllowSpectating && GameMain.Server.OwnerConnection != c.Connection))); var existingBots = Character.CharacterList @@ -68,7 +68,7 @@ namespace Barotrauma.Networking { RespawnCountdownStarted = respawnPending; RespawnTime = DateTime.Now + new TimeSpan(0,0,0,0, (int)(GameMain.Server.ServerSettings.RespawnInterval * 1000.0f)); - GameMain.Server.CreateEntityEvent(this); + GameMain.Server.CreateEntityEvent(this); } if (!RespawnCountdownStarted) { return; } @@ -180,7 +180,7 @@ namespace Barotrauma.Networking partial void UpdateTransportingProjSpecific(float deltaTime) { - + if (!ReturnCountdownStarted) { //if there are no living chracters inside, transporting can be stopped immediately @@ -231,7 +231,7 @@ namespace Barotrauma.Networking var botsToSpawn = GetBotsToRespawn(); characterInfos.AddRange(botsToSpawn); - + GameMain.Server.AssignJobs(clients); foreach (Client c in clients) { @@ -257,7 +257,7 @@ namespace Barotrauma.Networking var character = Character.Create(characterInfos[i], shuttleSpawnPoints[i].WorldPosition, characterInfos[i].Name, !bot, bot); character.TeamID = Character.TeamType.Team1; - + if (bot) { GameServer.Log(string.Format("Respawning bot {0} as {1}", character.Info.Name, characterInfos[i].Job.Name), ServerLog.MessageType.Spawning); @@ -265,18 +265,18 @@ namespace Barotrauma.Networking else { //tell the respawning client they're no longer a traitor - if (GameMain.Server.TraitorManager != null && clients[i].Character != null) + if (GameMain.Server.TraitorManager?.Traitors != null && clients[i].Character != null) { - if (GameMain.Server.TraitorManager.TraitorList.Any(t => t.Character == clients[i].Character)) + if (GameMain.Server.TraitorManager.Traitors.Any(t => t.Character == clients[i].Character)) { - GameMain.Server.SendDirectChatMessage(TextManager.Get("traitorrespawnmessage"), clients[i], ChatMessageType.MessageBox); + GameMain.Server.SendDirectChatMessage(TextManager.FormatServerMessage("TraitorRespawnMessage"), clients[i], ChatMessageType.ServerMessageBox); } } clients[i].Character = character; - character.OwnerClientIP = clients[i].Connection.RemoteEndPoint.Address.ToString(); + character.OwnerClientEndPoint = clients[i].Connection.EndPointString; character.OwnerClientName = clients[i].Name; - GameServer.Log(string.Format("Respawning {0} ({1}) as {2}", clients[i].Name, clients[i].Connection?.RemoteEndPoint?.Address, characterInfos[i].Job.Name), ServerLog.MessageType.Spawning); + GameServer.Log(string.Format("Respawning {0} ({1}) as {2}", clients[i].Name, clients[i].Connection?.EndPointString, characterInfos[i].Job.Name), ServerLog.MessageType.Spawning); } if (divingSuitPrefab != null && oxyPrefab != null && RespawnShuttle != null) @@ -325,9 +325,9 @@ namespace Barotrauma.Networking } } - public void ServerWrite(NetBuffer msg, Client c, object[] extraData = null) + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { - msg.WriteRangedInteger(0, Enum.GetNames(typeof(State)).Length, (int)CurrentState); + msg.WriteRangedIntegerDeprecated(0, Enum.GetNames(typeof(State)).Length, (int)CurrentState); switch (CurrentState) { diff --git a/Barotrauma/BarotraumaServer/Source/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/Source/Networking/ServerSettings.cs index da1b96533..2a6fd1307 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/ServerSettings.cs @@ -1,5 +1,4 @@ -using Lidgren.Network; -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Globalization; @@ -20,7 +19,7 @@ namespace Barotrauma.Networking LoadClientPermissions(); } - private void WriteNetProperties(NetBuffer outMsg) + private void WriteNetProperties(IWriteMessage outMsg) { outMsg.Write((UInt16)netProperties.Keys.Count); foreach (UInt32 key in netProperties.Keys) @@ -30,7 +29,7 @@ namespace Barotrauma.Networking } } - public void ServerAdminWrite(NetBuffer outMsg, Client c) + public void ServerAdminWrite(IWriteMessage outMsg, Client c) { //outMsg.Write(isPublic); //outMsg.Write(EnableUPnP); @@ -43,11 +42,15 @@ namespace Barotrauma.Networking Whitelist.ServerAdminWrite(outMsg, c); } - public void ServerWrite(NetBuffer outMsg,Client c) + public void ServerWrite(IWriteMessage outMsg,Client c) { outMsg.Write(ServerName); outMsg.Write(ServerMessageText); - outMsg.WriteRangedInteger(1, 60, TickRate); + outMsg.Write((byte)MaxPlayers); + outMsg.Write(HasPassword); + outMsg.Write(isPublic); + outMsg.WritePadBits(); + outMsg.WriteRangedIntegerDeprecated(1, 60, TickRate); WriteExtraCargo(outMsg); @@ -67,7 +70,7 @@ namespace Barotrauma.Networking } } - public void ServerRead(NetIncomingMessage incMsg,Client c) + public void ServerRead(IReadMessage incMsg,Client c) { if (!c.HasPermission(Networking.ClientPermissions.ManageSettings)) return; @@ -112,7 +115,7 @@ namespace Barotrauma.Networking else { UInt32 size = incMsg.ReadVariableUInt32(); - incMsg.Position += 8 * size; + incMsg.BitPosition += (int)(8 * size); } } @@ -145,7 +148,7 @@ namespace Barotrauma.Networking if (botSpawnMode > 1) botSpawnMode = 0; BotSpawnMode = (BotSpawnMode)botSpawnMode; - float levelDifficulty = incMsg.ReadFloat(); + float levelDifficulty = incMsg.ReadSingle(); if (levelDifficulty >= 0.0f) SelectedLevelDifficulty = levelDifficulty; UseRespawnShuttle = incMsg.ReadBoolean(); @@ -177,24 +180,16 @@ namespace Barotrauma.Networking doc.Root.SetAttributeValue("name", ServerName); doc.Root.SetAttributeValue("public", isPublic); - doc.Root.SetAttributeValue("port", GameMain.Server.NetPeerConfiguration.Port); + doc.Root.SetAttributeValue("port", Port); if (Steam.SteamManager.USE_STEAM) doc.Root.SetAttributeValue("queryport", QueryPort); doc.Root.SetAttributeValue("maxplayers", maxPlayers); - doc.Root.SetAttributeValue("enableupnp", GameMain.Server.NetPeerConfiguration.EnableUPnP); + doc.Root.SetAttributeValue("enableupnp", EnableUPnP); doc.Root.SetAttributeValue("autorestart", autoRestart); - - doc.Root.SetAttributeValue("SubSelection", SubSelectionMode.ToString()); - doc.Root.SetAttributeValue("ModeSelection", ModeSelectionMode.ToString()); + doc.Root.SetAttributeValue("LevelDifficulty", ((int)selectedLevelDifficulty).ToString()); - doc.Root.SetAttributeValue("TraitorsEnabled", TraitorsEnabled.ToString()); - - /*doc.Root.SetAttributeValue("BotCount", BotCount); - doc.Root.SetAttributeValue("MaxBotCount", MaxBotCount);*/ - doc.Root.SetAttributeValue("BotSpawnMode", BotSpawnMode.ToString()); - + doc.Root.SetAttributeValue("AllowedRandomMissionTypes", string.Join(",", AllowedRandomMissionTypes)); - doc.Root.SetAttributeValue("AllowedClientNameChars", string.Join(",", AllowedClientNameChars.Select(c => c.First + "-" + c.Second))); doc.Root.SetAttributeValue("ServerMessage", ServerMessageText); @@ -239,18 +234,22 @@ namespace Barotrauma.Networking selectedLevelDifficulty = doc.Root.GetAttributeFloat("LevelDifficulty", 20.0f); GameMain.NetLobbyScreen.SetLevelDifficulty(selectedLevelDifficulty); - - var traitorsEnabled = TraitorsEnabled; - Enum.TryParse(doc.Root.GetAttributeString("TraitorsEnabled", "No"), out traitorsEnabled); - TraitorsEnabled = traitorsEnabled; + GameMain.NetLobbyScreen.SetTraitorsEnabled(traitorsEnabled); - - var botSpawnMode = BotSpawnMode.Normal; - Enum.TryParse(doc.Root.GetAttributeString("BotSpawnMode", "Normal"), out botSpawnMode); - BotSpawnMode = botSpawnMode; - - //"65-90", "97-122", "48-59" = upper and lower case english alphabet and numbers - string[] allowedClientNameCharsStr = doc.Root.GetAttributeStringArray("AllowedClientNameChars", new string[] { "65-90", "97-122", "48-59" }); + + string[] allowedClientNameCharsStr = doc.Root.GetAttributeStringArray("AllowedClientNameChars", + new string[] { + "32-33", + "38-46", + "48-57", + "65-90", + "91", + "93", + "95-122", + "192-255", + "384-591", + "1024-1279" + }); foreach (string allowedClientNameCharRange in allowedClientNameCharsStr) { string[] splitRange = allowedClientNameCharRange.Split('-'); @@ -329,7 +328,7 @@ namespace Barotrauma.Networking foreach (XElement clientElement in doc.Root.Elements()) { string clientName = clientElement.GetAttributeString("name", ""); - string clientIP = clientElement.GetAttributeString("ip", ""); + string clientEndPoint = clientElement.GetAttributeString("endpoint", null) ?? clientElement.GetAttributeString("ip", ""); string steamIdStr = clientElement.GetAttributeString("steamid", ""); if (string.IsNullOrWhiteSpace(clientName)) @@ -337,7 +336,7 @@ namespace Barotrauma.Networking DebugConsole.ThrowError("Error in " + ClientPermissionsFile + " - all clients must have a name and an IP address."); continue; } - if (string.IsNullOrWhiteSpace(clientIP) && string.IsNullOrWhiteSpace(steamIdStr)) + if (string.IsNullOrWhiteSpace(clientEndPoint) && string.IsNullOrWhiteSpace(steamIdStr)) { DebugConsole.ThrowError("Error in " + ClientPermissionsFile + " - all clients must have an IP address or a Steam ID."); continue; @@ -410,7 +409,7 @@ namespace Barotrauma.Networking } else { - ClientPermissions.Add(new SavedClientPermission(clientName, clientIP, permissions, permittedCommands)); + ClientPermissions.Add(new SavedClientPermission(clientName, clientEndPoint, permissions, permittedCommands)); } } } @@ -480,7 +479,7 @@ namespace Barotrauma.Networking } else { - clientElement.Add(new XAttribute("ip", clientPermission.IP)); + clientElement.Add(new XAttribute("endpoint", clientPermission.EndPoint)); } if (matchingPreset == null) diff --git a/Barotrauma/BarotraumaServer/Source/Networking/SteamManager.cs b/Barotrauma/BarotraumaServer/Source/Networking/SteamManager.cs index d13e5e89c..61c864935 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/SteamManager.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/SteamManager.cs @@ -29,7 +29,8 @@ namespace Barotrauma.Steam RefreshServerDetails(server); - instance.server.Auth.OnAuthChange = server.OnAuthChange; + server.ServerPeer.InitializeSteamServerCallbacks(instance.server); + Instance.server.LogOnAnonymous(); return true; @@ -66,23 +67,24 @@ namespace Barotrauma.Steam Instance.server.SetKey("traitors", server.ServerSettings.TraitorsEnabled.ToString()); Instance.server.SetKey("gamestarted", server.GameStarted.ToString()); Instance.server.SetKey("gamemode", server.ServerSettings.GameModeIdentifier); - + instance.server.DedicatedServer = true; return true; } - public static bool StartAuthSession(byte[] authTicketData, ulong clientSteamID) + public static ServerAuth.StartAuthSessionResult StartAuthSession(byte[] authTicketData, ulong clientSteamID) { - if (instance == null || !instance.isInitialized || instance.server == null) return false; - + if (instance == null || !instance.isInitialized || instance.server == null) return ServerAuth.StartAuthSessionResult.ServerNotConnectedToSteam; + DebugConsole.Log("SteamManager authenticating Steam client " + clientSteamID); - if (!instance.server.Auth.StartSession(authTicketData, clientSteamID)) + ServerAuth.StartAuthSessionResult startResult = instance.server.Auth.StartSession(authTicketData, clientSteamID); + if (startResult != ServerAuth.StartAuthSessionResult.OK) { - DebugConsole.Log("Authentication failed"); - return false; + DebugConsole.Log("Authentication failed: failed to start auth session (" + startResult.ToString() + ")"); } - return true; + + return startResult; } public static void StopAuthSession(ulong clientSteamID) diff --git a/Barotrauma/BarotraumaServer/Source/Networking/Voip/VoipServer.cs b/Barotrauma/BarotraumaServer/Source/Networking/Voip/VoipServer.cs index 02a88e0cc..b923dc1d5 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/Voip/VoipServer.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/Voip/VoipServer.cs @@ -1,5 +1,4 @@ using Barotrauma.Items.Components; -using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -9,11 +8,11 @@ namespace Barotrauma.Networking { class VoipServer { - private NetServer netServer; + private ServerPeer netServer; private List queues; private Dictionary lastSendTime; - public VoipServer(NetServer server) + public VoipServer(ServerPeer server) { this.netServer = server; queues = new List(); @@ -54,15 +53,13 @@ namespace Barotrauma.Networking if (!CanReceive(sender, recipient)) { continue; } - NetOutgoingMessage msg = netServer.CreateMessage(); + IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ServerPacketHeader.VOICE); msg.Write((byte)queue.QueueID); queue.Write(msg); - - GameMain.Server.CompressOutgoingMessage(msg); - - netServer.SendMessage(msg, recipient.Connection, NetDeliveryMethod.Unreliable); + + netServer.Send(msg, recipient.Connection, DeliveryMethod.Unreliable); } } } diff --git a/Barotrauma/BarotraumaServer/Source/Networking/Voting.cs b/Barotrauma/BarotraumaServer/Source/Networking/Voting.cs index cc4402453..5a5962955 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/Voting.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/Voting.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using Lidgren.Network; using System; using System.Collections.Generic; using System.Linq; @@ -19,7 +18,7 @@ namespace Barotrauma set { allowModeVoting = value; } } - public void ServerRead(NetIncomingMessage inc, Client sender) + public void ServerRead(IReadMessage inc, Client sender) { if (GameMain.Server == null || sender == null) return; @@ -86,7 +85,7 @@ namespace Barotrauma GameMain.Server.UpdateVoteStatus(); } - public void ServerWrite(NetBuffer msg) + public void ServerWrite(IWriteMessage msg) { if (GameMain.Server == null) return; diff --git a/Barotrauma/BarotraumaServer/Source/Networking/WhiteList.cs b/Barotrauma/BarotraumaServer/Source/Networking/WhiteList.cs index c5e27a867..9ab32db6d 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/WhiteList.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/WhiteList.cs @@ -1,5 +1,4 @@ -using Lidgren.Network; -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.IO; @@ -127,7 +126,7 @@ namespace Barotrauma.Networking whitelistedPlayers.Add(new WhiteListedPlayer(name, ip)); } - public void ServerAdminWrite(NetBuffer outMsg, Client c) + public void ServerAdminWrite(IWriteMessage outMsg, Client c) { if (!c.HasPermission(ClientPermissions.ManageSettings)) { @@ -139,7 +138,7 @@ namespace Barotrauma.Networking outMsg.Write(Enabled); outMsg.WritePadBits(); - outMsg.WriteVariableInt32(whitelistedPlayers.Count); + outMsg.WriteVariableUInt32((UInt32)whitelistedPlayers.Count); for (int i = 0; i < whitelistedPlayers.Count; i++) { WhiteListedPlayer whitelistedPlayer = whitelistedPlayers[i]; @@ -154,13 +153,13 @@ namespace Barotrauma.Networking } } - public bool ServerAdminRead(NetBuffer incMsg, Client c) + public bool ServerAdminRead(IReadMessage incMsg, Client c) { if (!c.HasPermission(ClientPermissions.ManageSettings)) { bool enabled = incMsg.ReadBoolean(); incMsg.ReadPadBits(); UInt16 removeCount = incMsg.ReadUInt16(); - incMsg.Position += removeCount * 4 * 8; + incMsg.BitPosition += removeCount * 4 * 8; UInt16 addCount = incMsg.ReadUInt16(); for (int i = 0; i < addCount; i++) { diff --git a/Barotrauma/BarotraumaServer/Source/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaServer/Source/Physics/PhysicsBody.cs index aceb6ad85..6874cab35 100644 --- a/Barotrauma/BarotraumaServer/Source/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaServer/Source/Physics/PhysicsBody.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using Lidgren.Network; using Microsoft.Xna.Framework; using System; @@ -7,7 +6,7 @@ namespace Barotrauma { partial class PhysicsBody { - public void ServerWrite(NetBuffer msg, Client c, object[] extraData = null) + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { float MaxVel = NetConfig.MaxPhysicsBodyVelocity; float MaxAngularVel = NetConfig.MaxPhysicsBodyAngularVelocity; diff --git a/Barotrauma/BarotraumaServer/Source/Program.cs b/Barotrauma/BarotraumaServer/Source/Program.cs index a30172199..77e872c80 100644 --- a/Barotrauma/BarotraumaServer/Source/Program.cs +++ b/Barotrauma/BarotraumaServer/Source/Program.cs @@ -24,17 +24,29 @@ namespace Barotrauma static void Main(string[] args) { GameMain game = null; - Thread inputThread = null; #if !DEBUG try { #endif game = new GameMain(args); - inputThread = new Thread(new ThreadStart(DebugConsole.UpdateCommandLine)); - inputThread.Start(); + DebugConsole.InputThread = null; +#if !DEBUG + if (!args.Contains("-ownerkey") && !args.Contains("-steamid")) + { +#endif + DebugConsole.InputThread = new Thread(new ThreadStart(DebugConsole.UpdateCommandLine)); + DebugConsole.InputThread.IsBackground = true; + DebugConsole.InputThread.Start(); +#if !DEBUG + } + else + { + Console.WriteLine("Server launched through client, command line IO disabled"); + } +#endif game.Run(); - inputThread.Abort(); inputThread.Join(); + DebugConsole.InputThread?.Abort(); DebugConsole.InputThread?.Join(); if (GameSettings.SendUserStatistics) GameAnalytics.OnQuit(); SteamManager.ShutDown(); #if !DEBUG @@ -42,7 +54,8 @@ namespace Barotrauma catch (Exception e) { CrashDump(game, "servercrashreport.log", e); - inputThread.Abort(); inputThread.Join(); + GameMain.Server?.NotifyCrash(); + DebugConsole.InputThread?.Abort(); DebugConsole.InputThread?.Join(); } #endif } @@ -115,7 +128,7 @@ namespace Barotrauma if (GameSettings.SendUserStatistics) { - GameAnalytics.AddErrorEvent(EGAErrorSeverity.Error, crashReport); + GameAnalytics.AddErrorEvent(EGAErrorSeverity.Critical, crashReport); GameAnalytics.OnQuit(); Console.Write("A crash report (\"crashreport.log\") was saved in the root folder of the game and sent to the developers."); } diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/Goal.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/Goal.cs new file mode 100644 index 000000000..b74289e24 --- /dev/null +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/Goal.cs @@ -0,0 +1,64 @@ +using Barotrauma.Networking; +using System.Collections.Generic; +using System.Linq; +using Microsoft.SqlServer.Server; + +namespace Barotrauma +{ + partial class Traitor + { + public abstract class Goal + { + public Traitor Traitor { get; private set; } + public TraitorMission Mission { get; internal set; } + + public virtual string StatusTextId { get; set; } = "TraitorGoalStatusTextFormat"; + + public virtual string InfoTextId { get; set; } = null; + + public virtual string CompletedTextId { get; set; } = null; + + public virtual string StatusValueTextId => IsCompleted ? "complete" : "inprogress"; + + public virtual IEnumerable StatusTextKeys => new [] { "[infotext]", "[status]" }; + public virtual IEnumerable StatusTextValues => new [] { InfoText, TextManager.FormatServerMessage(StatusValueTextId) }; + + public virtual IEnumerable InfoTextKeys => new string[] { }; + public virtual IEnumerable InfoTextValues => new string[] { }; + + public virtual IEnumerable CompletedTextKeys => new string[] { }; + public virtual IEnumerable CompletedTextValues => new string[] { }; + + protected virtual string FormatText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) => TextManager.FormatServerMessageWithGenderPronouns(traitor?.Character?.Info?.Gender ?? Gender.None, textId, keys, values); + + protected internal virtual string GetStatusText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) => FormatText(traitor, textId, keys, values); + protected internal virtual string GetInfoText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) => FormatText(traitor, textId, keys, values); + protected internal virtual string GetCompletedText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) => FormatText(traitor, textId, keys, values); + + public virtual string StatusText => GetStatusText(Traitor, StatusTextId, StatusTextKeys, StatusTextValues); + public virtual string InfoText => GetInfoText(Traitor, InfoTextId, InfoTextKeys, InfoTextValues); + + public virtual string CompletedText => CompletedTextId != null ? GetCompletedText(Traitor, CompletedTextId, CompletedTextKeys, CompletedTextValues) : StatusText; + + public abstract bool IsCompleted { get; } + public virtual bool IsStarted => Traitor != null; + public virtual bool CanBeCompleted => !(Traitor?.Character?.IsDead ?? true); + + public virtual bool IsEnemy(Character character) => false; + + public virtual bool Start(Traitor traitor) + { + Traitor = traitor; + return true; + } + + public virtual void Update(float deltaTime) + { + } + + protected Goal() + { + } + } + } +} diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalDestroyItemsWithTag.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalDestroyItemsWithTag.cs new file mode 100644 index 000000000..b0ffa4dbd --- /dev/null +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalDestroyItemsWithTag.cs @@ -0,0 +1,98 @@ +using Barotrauma.Networking; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + partial class Traitor + { + public sealed class GoalDestroyItemsWithTag : Goal + { + private readonly string tag; + private readonly bool matchIdentifier; + private readonly bool matchTag; + private readonly bool matchInventory; + + public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[percentage]", "[tag]" }); + public override IEnumerable InfoTextValues => base.InfoTextValues.Concat(new string[] { string.Format("{0:0}", DestroyPercent * 100.0f), tagPrefabName ?? "" }); + + private readonly float destroyPercent; + private float DestroyPercent => destroyPercent; + + private bool isCompleted = false; + public override bool IsCompleted => isCompleted; + + private int totalCount = 0; + private int targetCount = 0; + private string tagPrefabName = null; + + private int CountMatchingItems() + { + int result = 0; + foreach (var item in Item.ItemList) + { + if (!matchInventory && item.FindParentInventory(inventory => inventory.Owner is Character && inventory.Owner != Traitor.Character) != null) + { + continue; + } + + if (item.Submarine == null) + { + if (!(item.ParentInventory?.Owner is Character)) { continue; } + } + else + { + if (item.Submarine.TeamID != Traitor.Character.TeamID) { continue; } + } + + if (item.Condition <= 0.0f) + { + continue; + } + var identifierMatches = matchIdentifier && item.prefab.Identifier == tag; + if (identifierMatches && tagPrefabName == null) + { + var textId = item.Prefab.GetItemNameTextId(); + tagPrefabName = textId != null ? TextManager.FormatServerMessage(textId) : item.Prefab.Name; + } + if (identifierMatches || (matchTag && item.HasTag(tag))) + { + ++result; + } + } + return result; + } + + public override void Update(float deltaTime) + { + base.Update(deltaTime); + isCompleted = CountMatchingItems() <= targetCount; + } + + public override bool Start(Traitor traitor) + { + if (!base.Start(traitor)) + { + return false; + } + totalCount = CountMatchingItems(); + if (totalCount <= 0) + { + return false; + } + targetCount = (int)((1.0f - destroyPercent) * totalCount - 0.5f); + return true; + } + + public GoalDestroyItemsWithTag(string tag, float destroyPercent, bool matchTag, bool matchIdentifier, bool matchInventory) : base() + { + InfoTextId = "TraitorGoalDestroyItems"; + this.tag = tag; + this.destroyPercent = destroyPercent; + this.matchTag = matchTag; + this.matchIdentifier = matchIdentifier; + this.matchInventory = matchInventory; + } + } + } +} diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalFindItem.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalFindItem.cs new file mode 100644 index 000000000..11b42f2d1 --- /dev/null +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalFindItem.cs @@ -0,0 +1,164 @@ +using Barotrauma.Items.Components; +using Barotrauma.Networking; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + partial class Traitor + { + public class GoalFindItem : HumanoidGoal + { + private readonly string identifier; + private readonly bool preferNew; + private readonly bool allowNew; + private readonly bool allowExisting; + private readonly HashSet allowedContainerIdentifiers = new HashSet(); + + private ItemPrefab targetPrefab; + private Item targetContainer; + private Item target; + private HashSet existingItems = new HashSet(); + private string targetNameText; + private string targetContainerNameText; + private string targetHullNameText; + + public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[identifier]", "[target]", "[targethullname]" }); + public override IEnumerable InfoTextValues => base.InfoTextValues.Concat(new string[] { targetNameText ?? "", targetContainerNameText ?? "", targetHullNameText ?? "" }); + + public override bool IsCompleted => target != null && target.ParentInventory == Traitor.Character.Inventory; + public override bool CanBeCompleted { + get + { + if (!base.CanBeCompleted) + { + return false; + } + if (target == null) + { + return true; + } + if (target.Removed) + { + return false; + } + if (target.Submarine == null) + { + if (!(target.ParentInventory?.Owner is Character)) + { + return false; + } + } + else + { + if (target.Submarine.TeamID != Traitor.Character.TeamID) + { + return false; + } + } + return true; + } + } + + public override bool IsEnemy(Character character) => base.IsEnemy(character) || (target != null && target.FindParentInventory(inventory => inventory == character.Inventory) != null); + + protected ItemPrefab FindItemPrefab(string identifier) + { + return (ItemPrefab)MapEntityPrefab.List.Find(prefab => prefab is ItemPrefab && prefab.Identifier == identifier); + } + + protected Item FindRandomContainer(bool includeNew, bool includeExisting) + { + int itemsCount = Item.ItemList.Count; + int startIndex = TraitorMission.Random(itemsCount); + Item fallback = null; + for (int i = 0; i < itemsCount; ++i) + { + var item = Item.ItemList[(i + startIndex) % itemsCount]; + if (item.Submarine == null || item.Submarine.TeamID != Traitor.Character.TeamID) + { + continue; + } + + if (item.GetComponent() != null && allowedContainerIdentifiers.Contains(item.prefab.Identifier)) + { + if ((includeNew && !item.OwnInventory.IsFull()) || (includeExisting && item.OwnInventory.FindItemByIdentifier(targetPrefab.Identifier) != null)) + { + return item; + } + } + } + return null; + } + + public override bool Start(Traitor traitor) + { + if (!base.Start(traitor)) + { + return false; + } + targetPrefab = FindItemPrefab(identifier); + if (targetPrefab == null) + { + return false; + } + var targetPrefabTextId = targetPrefab.GetItemNameTextId(); + targetNameText = targetPrefabTextId != null ? TextManager.FormatServerMessage(targetPrefabTextId) : targetPrefab.Name; + targetContainer = null; + if (preferNew) + { + targetContainer = FindRandomContainer(true, false); + } + if (targetContainer == null) + { + targetContainer = FindRandomContainer(allowNew, allowExisting); + } + if (targetContainer == null) + { + return false; + } + var containerPrefabTextId = targetContainer.Prefab.GetItemNameTextId(); + targetContainerNameText = containerPrefabTextId != null ? TextManager.FormatServerMessage(containerPrefabTextId) : targetContainer.Prefab.Name; + var targetHullTextId = targetContainer.CurrentHull != null ? targetContainer.CurrentHull.prefab.GetHullNameTextId() : null; + targetHullNameText = targetHullTextId != null ? TextManager.FormatServerMessage(targetHullTextId) : targetContainer?.CurrentHull?.DisplayName ?? ""; + if (allowNew && !targetContainer.OwnInventory.IsFull()) + { + existingItems.Clear(); + foreach (var item in targetContainer.OwnInventory.Items) + { + existingItems.Add(item); + } + Entity.Spawner.AddToSpawnQueue(targetPrefab, targetContainer.OwnInventory); + target = null; + } + else if (allowExisting) + { + target = targetContainer.OwnInventory.FindItemByIdentifier(targetPrefab.Identifier); + } + return true; + } + + public override void Update(float deltaTime) + { + base.Update(deltaTime); + if (target == null) + { + target = targetContainer.OwnInventory.Items.FirstOrDefault(item => item != null && item.Prefab.Identifier == identifier && !existingItems.Contains(item)); + if (target != null) + { + existingItems.Clear(); + } + } + } + + public GoalFindItem(string identifier, bool preferNew, bool allowNew, bool allowExisting, params string[] allowedContainerIdentifiers) + { + this.identifier = identifier; + this.preferNew = preferNew; + this.allowNew = allowNew; + this.allowExisting = allowExisting; + this.allowedContainerIdentifiers.UnionWith(allowedContainerIdentifiers); + } + } + } +} diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalFloodPercentOfSub.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalFloodPercentOfSub.cs new file mode 100644 index 000000000..0cbe16075 --- /dev/null +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalFloodPercentOfSub.cs @@ -0,0 +1,45 @@ +using Barotrauma.Networking; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + partial class Traitor + { + public sealed class GoalFloodPercentOfSub : Goal + { + private readonly float minimumFloodingAmount; + + public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[percentage]" }); + public override IEnumerable InfoTextValues => base.InfoTextValues.Concat(new string[] { string.Format("{0:0}", minimumFloodingAmount * 100.0f) }); + + private bool isCompleted = false; + public override bool IsCompleted => isCompleted; + + public override void Update(float deltaTime) + { + base.Update(deltaTime); + var validHullsCount = 0; + var floodingAmount = 0.0f; + foreach (Hull hull in Hull.hullList) + { + if (hull.Submarine == null || hull.Submarine.IsOutpost || hull.Submarine.TeamID != Traitor.Character.TeamID) { continue; } + ++validHullsCount; + floodingAmount += hull.WaterVolume / hull.Volume; + } + if (validHullsCount > 0) + { + floodingAmount /= validHullsCount; + } + isCompleted = floodingAmount >= minimumFloodingAmount; + } + + public GoalFloodPercentOfSub(float minimumFloodingAmount) : base() + { + InfoTextId = "TraitorGoalFloodPercentOfSub"; + this.minimumFloodingAmount = minimumFloodingAmount; + } + } + + } +} diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalKillTarget.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalKillTarget.cs new file mode 100644 index 000000000..38511ac3d --- /dev/null +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalKillTarget.cs @@ -0,0 +1,45 @@ +using Barotrauma.Networking; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + partial class Traitor + { + public sealed class GoalKillTarget : Goal + { + public TraitorMission.CharacterFilter Filter { get; private set; } + public Character Target { get; private set; } + + public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[targetname]" }); + public override IEnumerable InfoTextValues => base.InfoTextValues.Concat(new string[] { Target?.Name ?? "(unknown)" }); + + private bool isCompleted = false; + public override bool IsCompleted => isCompleted; + + public override bool IsEnemy(Character character) => base.IsEnemy(character) || (!isCompleted && character == Target); + + public override void Update(float deltaTime) + { + base.Update(deltaTime); + isCompleted = Target?.IsDead ?? false; + } + + public override bool Start(Traitor traitor) + { + if (!base.Start(traitor)) + { + return false; + } + Target = traitor.Mission.FindKillTarget(traitor.Character, Filter); + return Target != null && !Target.IsDead; + } + + public GoalKillTarget(TraitorMission.CharacterFilter filter) : base() + { + InfoTextId = "TraitorGoalKillTargetInfo"; + Filter = filter; + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalRandom.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalRandom.cs new file mode 100644 index 000000000..e05be93c0 --- /dev/null +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalRandom.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + partial class Traitor + { + public class GoalRandom : Goal + { + private readonly List allGoals; + + private readonly List selectedGoals = new List(); + + public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[targetname]" }); + public override IEnumerable InfoTextValues => base.InfoTextValues.Concat(new string[] { Target?.Name ?? "(unknown)" }); + + private bool isCompleted = false; + public override bool IsCompleted => isCompleted; + + public override bool IsEnemy(Character character) => base.IsEnemy(character) || (!isCompleted && character == Target); + + public override void Update(float deltaTime) + { + base.Update(deltaTime); + isCompleted = Target?.IsDead ?? false; + } + + public override bool Start(Traitor traitor) + { + if (!base.Start(traitor)) + { + return false; + } + Target = traitor.Mission.FindKillTarget(traitor.Character, Filter); + return Target != null && !Target.IsDead; + } + + public GoalRandom(params Goal[] goals, int count) + { + this.goals = goals; + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalReachDistanceFromSub.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalReachDistanceFromSub.cs new file mode 100644 index 000000000..e259e2182 --- /dev/null +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalReachDistanceFromSub.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FarseerPhysics; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + partial class Traitor + { + public sealed class GoalReachDistanceFromSub : Goal + { + private readonly float requiredDistance; + private readonly float requiredDistanceSqr; + + public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[distance]" }); + public override IEnumerable InfoTextValues => base.InfoTextValues.Concat(new string[] { $"{requiredDistance:0.00}" }); + + public override bool IsCompleted + { + get + { + if (Traitor == null || Traitor.Character == null || Traitor.Character.Submarine == null) + { + return false; + } + var characterPosition = Traitor.Character.WorldPosition; + var submarinePosition = Traitor.Character.Submarine.WorldPosition; + var distance = Vector2.DistanceSquared(characterPosition, submarinePosition); + return distance >= requiredDistanceSqr; + } + } + + public GoalReachDistanceFromSub(float requiredDistance) : base() + { + InfoTextId = "TraitorGoalReachDistanceFromSub"; + this.requiredDistance = requiredDistance; + requiredDistanceSqr = requiredDistance * requiredDistance; + } + } + } +} diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalReplaceInventory.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalReplaceInventory.cs new file mode 100644 index 000000000..f737e1c04 --- /dev/null +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalReplaceInventory.cs @@ -0,0 +1,70 @@ +using Barotrauma.Networking; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + partial class Traitor + { + public class GoalReplaceInventory : HumanoidGoal + { + private readonly HashSet sabotageContainerIds = new HashSet(); + private readonly HashSet validReplacementIds = new HashSet(); + + private readonly float replaceAmount; + + private bool isCompleted = false; + public override bool IsCompleted => isCompleted; + + public override IEnumerable StatusTextKeys => base.StatusTextKeys.Concat(new string[] { "[percentage]" }); + public override IEnumerable StatusTextValues => base.StatusTextValues.Concat(new string[] { string.Format("{0:0}", replaceAmount * 100.0f) }); + + public override void Update(float deltaTime) + { + base.Update(deltaTime); + int totalAmount = 0, replacedAmount = 0; + foreach (var item in Item.ItemList) + { + if (item.Submarine == null || item.Submarine.TeamID != Traitor.Character.TeamID) + { + continue; + } + if (item.FindParentInventory(inventory => inventory.Owner is Character) != null) + { + continue; + } + if (sabotageContainerIds.Contains(item.prefab.Identifier)) + { + ++totalAmount; + if (item.OwnInventory.Items.Length <= 0 || item.OwnInventory.Items.All(containedItem => containedItem != null && !validReplacementIds.Contains(containedItem.Prefab.Identifier))) + { + continue; + } + ++replacedAmount; + } + } + isCompleted = replacedAmount >= (int)(replaceAmount * totalAmount + 0.5f); + } + + public override bool Start(Traitor traitor) + { + if (!base.Start(traitor)) + { + return false; + } + if (sabotageContainerIds.Count <= 0 || validReplacementIds.Count <= 0) + { + return false; + } + return true; + } + + public GoalReplaceInventory(string[] containerIds, string[] replacementIds, float replaceAmount) + { + sabotageContainerIds.UnionWith(containerIds); + validReplacementIds.UnionWith(replacementIds); + this.replaceAmount = replaceAmount; + } + } + } +} diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalSabotageItems.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalSabotageItems.cs new file mode 100644 index 000000000..d2cb82fb6 --- /dev/null +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalSabotageItems.cs @@ -0,0 +1,62 @@ +using Barotrauma.Networking; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + partial class Traitor + { + public sealed class GoalSabotageItems : HumanoidGoal + { + private readonly string tag; + private readonly float conditionThreshold; + + public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[tag]", "[target]", "[threshold]" }); + public override IEnumerable InfoTextValues => base.InfoTextValues.Concat(new string[] { tag ?? "", targetItemPrefabName ?? "", string.Format("{0:0}", conditionThreshold) }); + + private bool isCompleted = false; + public override bool IsCompleted => isCompleted; + + private readonly List targetItems = new List(); + private string targetItemPrefabName = null; + + public override bool Start(Traitor traitor) + { + if (!base.Start(traitor)) + { + return false; + } + foreach (var item in Item.ItemList) + { + if (item.Submarine == null || item.Submarine.TeamID != Traitor.Character.TeamID) + { + continue; + } + if (item.Condition > conditionThreshold && (item.Prefab?.Identifier == tag || item.HasTag(tag))) + { + targetItems.Add(item); + } + } + if (targetItems.Count > 0) + { + var textId = targetItems[0].Prefab.GetItemNameTextId(); + targetItemPrefabName = TextManager.FormatServerMessage(textId) ?? targetItems[0].Prefab.Name; + } + return targetItems.Count > 0; + } + + public override void Update(float deltaTime) + { + base.Update(deltaTime); + isCompleted = targetItems.All(item => item.Condition <= conditionThreshold); + } + + public GoalSabotageItems(string tag, float conditionThreshold) : base() + { + this.tag = tag; + this.conditionThreshold = conditionThreshold; + InfoTextId = "TraitorGoalSabotageInfo"; + } + } + } +} diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/HumanoidGoal.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/HumanoidGoal.cs new file mode 100644 index 000000000..ee9329989 --- /dev/null +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/HumanoidGoal.cs @@ -0,0 +1,17 @@ +namespace Barotrauma +{ + partial class Traitor + { + public abstract class HumanoidGoal : Goal + { + public override bool Start(Traitor traitor) + { + if (!base.Start(traitor)) + { + return false; + } + return Traitor?.Character?.IsHumanoid ?? false; + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/Modifiers/GoalHasDuration.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/Modifiers/GoalHasDuration.cs new file mode 100644 index 000000000..926403cee --- /dev/null +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/Modifiers/GoalHasDuration.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + partial class Traitor + { + public sealed class GoalHasDuration : Modifier + { + private readonly float requiredDuration; + private readonly bool countTotalDuration; + private readonly string durationInfoTextId; + + public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[duration]" }); + + public override IEnumerable InfoTextValues => base.InfoTextValues.Concat(new string[] { $"{TimeSpan.FromSeconds(requiredDuration):g}" }); + + protected internal override string GetInfoText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) + { + var infoText = base.GetInfoText(traitor, textId, keys, values); + return !string.IsNullOrEmpty(durationInfoTextId) ? TextManager.FormatServerMessage(durationInfoTextId, new[] { "[infotext]", "[duration]" }, new[] { infoText, $"{TimeSpan.FromSeconds(requiredDuration):g}" }) : infoText; + } + + private bool isCompleted = false; + public override bool IsCompleted => isCompleted; + + private float remainingDuration = float.NaN; + + public override void Update(float deltaTime) + { + base.Update(deltaTime); + if (Goal.IsCompleted) + { + if (!float.IsNaN(remainingDuration)) + { + remainingDuration -= deltaTime; + } + else + { + remainingDuration = requiredDuration; + } + isCompleted |= remainingDuration <= 0.0f; + } + else if (!countTotalDuration) + { + remainingDuration = float.NaN; + } + } + + public GoalHasDuration(Goal goal, float requiredDuration, bool countTotalDuration, string durationInfoTextId) : base(goal) + { + this.requiredDuration = requiredDuration; + this.countTotalDuration = countTotalDuration; + this.durationInfoTextId = durationInfoTextId; + } + } + } +} + diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/Modifiers/GoalHasTimeLimit.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/Modifiers/GoalHasTimeLimit.cs new file mode 100644 index 000000000..2c1472f92 --- /dev/null +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/Modifiers/GoalHasTimeLimit.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + partial class Traitor + { + public sealed class GoalHasTimeLimit : Modifier + { + private readonly float timeLimit; + private readonly string timeLimitInfoTextId; + + public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[timelimit]" }); + public override IEnumerable InfoTextValues => base.InfoTextValues.Concat(new string[] { $"{TimeSpan.FromSeconds(timeLimit):g}" }); + + protected internal override string GetInfoText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) + { + var infoText = base.GetInfoText(traitor, textId, keys, values); + return !string.IsNullOrEmpty(timeLimitInfoTextId) ? TextManager.FormatServerMessage(timeLimitInfoTextId, new[] { "[infotext]", "[timelimit]" }, new[] { infoText, $"{TimeSpan.FromSeconds(timeLimit):g}" }) : infoText; + } + + public override bool CanBeCompleted => base.CanBeCompleted && (!IsStarted || timeRemaining > 0.0f); + + private float timeRemaining; + + public override void Update(float deltaTime) + { + base.Update(deltaTime); + timeRemaining = System.Math.Max(0.0f, timeRemaining - deltaTime); + } + + public override bool Start(Traitor traitor) + { + if (!base.Start(traitor)) + { + return false; + } + timeRemaining = timeLimit; + return true; + } + + public GoalHasTimeLimit(Goal goal, float timeLimit, string timeLimitInfoTextId) : base(goal) + { + this.timeLimit = timeLimit; + this.timeLimitInfoTextId = timeLimitInfoTextId; + } + } + } +} diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/Modifiers/GoalIsOptional.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/Modifiers/GoalIsOptional.cs new file mode 100644 index 000000000..028766bbd --- /dev/null +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/Modifiers/GoalIsOptional.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + partial class Traitor + { + public sealed class GoalIsOptional : Modifier + { + private readonly string optionalInfoTextId; + + public override string StatusValueTextId => (base.IsStarted && !base.CanBeCompleted) ? "failed" : base.StatusValueTextId; + + public override IEnumerable StatusTextValues + { + get { + var values = base.StatusTextValues.ToArray(); + values[1] = TextManager.GetServerMessage(StatusValueTextId); + return values; + } + } + + public override bool IsCompleted => base.IsCompleted || (base.IsStarted && !base.CanBeCompleted); + public override bool CanBeCompleted => true; + + protected internal override string GetInfoText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) + { + var infoText = base.GetInfoText(traitor, textId, keys, values); + return !string.IsNullOrEmpty(optionalInfoTextId) ? TextManager.FormatServerMessage(optionalInfoTextId, new[] { "[infotext]" }, new[] { infoText }) : infoText; + } + + public GoalIsOptional(Goal goal, string optionalInfoTextId) : base(goal) + { + this.optionalInfoTextId = optionalInfoTextId; + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/Modifiers/Modifier.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/Modifiers/Modifier.cs new file mode 100644 index 000000000..825a8c785 --- /dev/null +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/Modifiers/Modifier.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + partial class Traitor + { + public abstract class Modifier : Goal + { + protected Goal Goal { get; } + + public override string StatusValueTextId => Goal.StatusValueTextId; + + public override string StatusTextId + { + get => Goal.StatusTextId; + set => Goal.StatusTextId = value; + } + + public override string InfoTextId + { + get => Goal.InfoTextId; + set => Goal.InfoTextId = value; + } + + public override string CompletedTextId + { + get => Goal.CompletedTextId; + set => Goal.CompletedTextId = value; + } + + public override IEnumerable StatusTextKeys => Goal.StatusTextKeys; + public override IEnumerable StatusTextValues => new [] { InfoText, TextManager.FormatServerMessage(StatusValueTextId) }; + + public override IEnumerable InfoTextKeys => Goal.InfoTextKeys; + public override IEnumerable InfoTextValues => Goal.InfoTextValues; + + public override IEnumerable CompletedTextKeys => Goal.CompletedTextKeys; + public override IEnumerable CompletedTextValues => Goal.CompletedTextValues; + + protected internal override string GetStatusText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) => Goal.GetStatusText(traitor, textId, keys, values); + protected internal override string GetInfoText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) => Goal.GetInfoText(traitor, textId, keys, values); + protected internal override string GetCompletedText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) => Goal.GetCompletedText(traitor, textId, keys, values); + + public override string StatusText => GetStatusText(Traitor, StatusTextId, StatusTextKeys, StatusTextValues); + public override string InfoText => GetInfoText(Traitor, InfoTextId, InfoTextKeys, InfoTextValues); + public override string CompletedText => CompletedTextId != null ? GetCompletedText(Traitor, CompletedTextId, CompletedTextKeys, CompletedTextValues) : StatusText; + + public override bool IsCompleted => Goal.IsCompleted; + public override bool IsStarted => base.IsStarted && Goal.IsStarted; + public override bool CanBeCompleted => base.CanBeCompleted && Goal.CanBeCompleted; + + public override bool IsEnemy(Character character) => base.IsEnemy(character) || Goal.IsEnemy(character); + + public override void Update(float deltaTime) + { + base.Update(deltaTime); + Goal.Update(deltaTime); + } + + public override bool Start(Traitor traitor) + { + if (!base.Start(traitor)) + { + return false; + } + if (!Goal.Start(traitor)) + { + return false; + } + return true; + } + + protected Modifier(Goal goal) : base() + { + Goal = goal; + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Objective.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Objective.cs new file mode 100644 index 000000000..7833f4f17 --- /dev/null +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Objective.cs @@ -0,0 +1,198 @@ +using Barotrauma.Networking; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + partial class Traitor + { + public class Objective + { + public Traitor Traitor { get; private set; } + + private int shuffleGoalsCount; + + private readonly List allGoals = new List(); + private readonly List activeGoals = new List(); + private readonly List pendingGoals = new List(); + private readonly List completedGoals = new List(); + + public bool IsCompleted => pendingGoals.Count <= 0; + public bool IsPartiallyCompleted => completedGoals.Count > 0; + public bool IsStarted { get; private set; } = false; + public bool CanBeCompleted => !IsStarted || pendingGoals.All(goal => goal.CanBeCompleted); + + public bool IsEnemy(Character character) => pendingGoals.Any(goal => goal.IsEnemy(character)); + + public string InfoText { get; private set; } + + public virtual string GoalInfoFormatId { get; set; } = "TraitorObjectiveGoalInfoFormat"; + + public string GoalInfos => + string.Join("/", + string.Join("/", activeGoals.Select((goal, index) => + { + var statusText = goal.StatusText; + var startIndex = statusText.LastIndexOf('/') + 1; + return $"{statusText.Substring(0, startIndex)}[{index}.st]={statusText.Substring(startIndex)}/[{index}.sl]={TextManager.FormatServerMessage(GoalInfoFormatId, new string[] { "[statustext]" }, new string[] { $"[{index}.st]" })}"; + }).ToArray()), + string.Join("", activeGoals.Select((goal, index) => $"[{index}.sl]").ToArray())); + + public string AllGoalInfos => + string.Join("/", + string.Join("/", allGoals.Select((goal, index) => + { + var statusText = goal.StatusText; + var startIndex = statusText.LastIndexOf('/') + 1; + return $"{statusText.Substring(0, startIndex)}[{index}.st]={statusText.Substring(startIndex)}/[{index}.sl]={TextManager.FormatServerMessage(GoalInfoFormatId, new string[] { "[statustext]" }, new string[] { $"[{index}.st]" })}"; + }).ToArray()), + string.Join("", allGoals.Select((goal, index) => $"[{index}.sl]").ToArray())); + + public virtual string StartMessageTextId { get; set; } = "TraitorObjectiveStartMessage"; + public virtual IEnumerable StartMessageKeys => new string[] { "[traitorgoalinfos]" }; + public virtual IEnumerable StartMessageValues => new string[] { GoalInfos }; + + public virtual string StartMessageText => TextManager.FormatServerMessageWithGenderPronouns(Traitor?.Character?.Info?.Gender ?? Gender.None, StartMessageTextId, StartMessageKeys, StartMessageValues); + + public virtual string StartMessageServerTextId { get; set; } = "TraitorObjectiveStartMessageServer"; + public virtual IEnumerable StartMessageServerKeys => StartMessageKeys.Concat(new string[] { "[traitorname]" }); + public virtual IEnumerable StartMessageServerValues => StartMessageValues.Concat(new string[] { Traitor?.Character?.Name ?? "(unknown)" }); + + public virtual string StartMessageServerText => TextManager.FormatServerMessageWithGenderPronouns(Traitor?.Character?.Info?.Gender ?? Gender.None, StartMessageServerTextId, StartMessageServerKeys, StartMessageServerValues); + + public virtual string EndMessageSuccessTextId { get; set; } = "TraitorObjectiveEndMessageSuccess"; + public virtual string EndMessageSuccessDeadTextId { get; set; } = "TraitorObjectiveEndMessageSuccessDead"; + public virtual string EndMessageSuccessDetainedTextId { get; set; } = "TraitorObjectiveEndMessageSuccessDetained"; + public virtual string EndMessageFailureTextId { get; set; } = "TraitorObjectiveEndMessageFailure"; + public virtual string EndMessageFailureDeadTextId { get; set; } = "TraitorObjectiveEndMessageFailureDead"; + public virtual string EndMessageFailureDetainedTextId { get; set; } = "TraitorObjectiveEndMessageFailureDetained"; + + public virtual IEnumerable EndMessageKeys => new string[] { "[traitorname]", "[traitorgoalinfos]" }; + public virtual IEnumerable EndMessageValues => new string[] { Traitor?.Character?.Name ?? "(unknown)", GoalInfos }; + public virtual string EndMessageText + { + get + { + var traitorIsDead = Traitor.Character.IsDead; + var traitorIsDetained = Traitor.Character.LockHands; + var messageId = IsCompleted + ? (traitorIsDead ? EndMessageSuccessDeadTextId : traitorIsDetained ? EndMessageSuccessDetainedTextId : EndMessageSuccessTextId) + : (traitorIsDead ? EndMessageFailureDeadTextId : traitorIsDetained ? EndMessageFailureDetainedTextId : EndMessageFailureTextId); + return TextManager.FormatServerMessageWithGenderPronouns(Traitor?.Character?.Info?.Gender ?? Gender.None, messageId, EndMessageKeys.ToArray(), EndMessageValues.ToArray()); + } + } + + public bool Start(Traitor traitor) + { + Traitor = traitor; + + activeGoals.Clear(); + pendingGoals.Clear(); + completedGoals.Clear(); + + var allGoalsCount = allGoals.Count; + var indices = allGoals.Select((goal, index) => index).ToArray(); + if (shuffleGoalsCount > 0) + { + for (var i = allGoalsCount; i > 1;) + { + int j = TraitorMission.Random(i--); + var temp = indices[j]; + indices[j] = indices[i]; + indices[i] = temp; + } + } + + for (var i = 0; i < allGoalsCount; ++i) + { + var goal = allGoals[indices[i]]; + if (goal.Start(traitor)) + { + activeGoals.Add(goal); + pendingGoals.Add(goal); + if (shuffleGoalsCount > 0 && pendingGoals.Count >= shuffleGoalsCount) + { + break; + } + } + else + { + completedGoals.Add(goal); + } + } + if (pendingGoals.Count <= 0) + { + return false; + } + IsStarted = true; + + traitor.SendChatMessageBox(StartMessageText); + traitor.UpdateCurrentObjective(GoalInfos); + + return true; + } + + public void StartMessage() + { + Traitor.SendChatMessage(StartMessageText); + } + + public void End(bool displayMessage) + { + if (displayMessage) + { + Traitor.SendChatMessageBox(EndMessageText); + } + } + + public void EndMessage() + { + Traitor.SendChatMessage(EndMessageText); + } + + public void Update(float deltaTime) + { + if (!IsStarted) + { + return; + } + for (int i = 0; i < pendingGoals.Count;) + { + var goal = pendingGoals[i]; + goal.Update(deltaTime); + if (!goal.IsCompleted) + { + ++i; + } + else + { + completedGoals.Add(goal); + pendingGoals.RemoveAt(i); + if (GameMain.Server != null) + { + Traitor.SendChatMessage(goal.CompletedText); + if (pendingGoals.Count > 0) + { + Traitor.SendChatMessageBox(goal.CompletedText); + } + Traitor.UpdateCurrentObjective(GoalInfos); + } + } + } + } + + public Objective(string infoText, int shuffleGoalsCount, params Goal[] goals) + { + InfoText = infoText; + this.shuffleGoalsCount = shuffleGoalsCount; + allGoals.AddRange(goals); + } + + public bool HasGoalsOfType() where T : Goal + { + return allGoals?.Any(g => g is T) ?? false; + } + } + } +} diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Traitor.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Traitor.cs new file mode 100644 index 000000000..79d739b48 --- /dev/null +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Traitor.cs @@ -0,0 +1,66 @@ +using Barotrauma.Networking; +using Lidgren.Network; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + partial class Traitor + { + public readonly Character Character; + + public string Role { get; private set; } + public TraitorMission Mission { get; private set; } + public Objective CurrentObjective => Mission.GetCurrentObjective(this); + + public Traitor(TraitorMission mission, string role, Character character) + { + Mission = mission; + Role = role; + Character = character; + Character.IsTraitor = true; + GameMain.NetworkMember.CreateEntityEvent(Character, new object[] { NetEntityEvent.Type.Status }); + } + + public delegate void MessageSender(string message); + public void Greet(GameServer server, string codeWords, string codeResponse, MessageSender messageSender) + { + string greetingMessage = TextManager.FormatServerMessage(Mission.StartText, new string[] { + "[codewords]", "[coderesponse]" + }, new string[] { + codeWords, codeResponse + }); + + messageSender(greetingMessage); + // boxSender(greetingMessage); + // SendChatMessage(greetingMessage); + // SendChatMessageBox(greetingMessage); + + Client traitorClient = server.ConnectedClients.Find(c => c.Character == Character); + Client ownerClient = server.ConnectedClients.Find(c => c.Connection == server.OwnerConnection); + if (traitorClient != ownerClient && ownerClient != null && ownerClient.Character == null) + { + GameMain.Server.SendTraitorMessage(ownerClient, CurrentObjective.StartMessageServerText, TraitorMessageType.ServerMessageBox); + } + } + + public void SendChatMessage(string serverText) + { + Client traitorClient = GameMain.Server.ConnectedClients.Find(c => c.Character == Character); + GameMain.Server.SendTraitorMessage(traitorClient, serverText, TraitorMessageType.Server); + } + + public void SendChatMessageBox(string serverText) + { + Client traitorClient = GameMain.Server.ConnectedClients.Find(c => c.Character == Character); + GameMain.Server.SendTraitorMessage(traitorClient, serverText, TraitorMessageType.ServerMessageBox); + } + + public void UpdateCurrentObjective(string objectiveText) + { + Client traitorClient = GameMain.Server.ConnectedClients.Find(c => c.Character == Character); + Character.TraitorCurrentObjective = objectiveText; + GameMain.Server.SendTraitorMessage(traitorClient, Character.TraitorCurrentObjective, TraitorMessageType.Objective); + } + } +} diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/TraitorManager.cs b/Barotrauma/BarotraumaServer/Source/Traitors/TraitorManager.cs new file mode 100644 index 000000000..a22702d4e --- /dev/null +++ b/Barotrauma/BarotraumaServer/Source/Traitors/TraitorManager.cs @@ -0,0 +1,208 @@ +// #define DISABLE_MISSIONS + +using System; +using Barotrauma.Networking; +using Lidgren.Network; +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + partial class TraitorManager + { + public readonly Dictionary Missions = new Dictionary(); + + public string GetCodeWords(Character.TeamType team) => Missions.TryGetValue(team, out var mission) ? mission.CodeWords : ""; + public string GetCodeResponse(Character.TeamType team) => Missions.TryGetValue(team, out var mission) ? mission.CodeResponse : ""; + + public IEnumerable Traitors => Missions.Values.SelectMany(mission => mission.Traitors.Values); + + private float startCountdown = 0.0f; + private GameServer server; + + private readonly Dictionary traitorCountsBySteamId = new Dictionary(); + private readonly Dictionary traitorCountsByEndPoint = new Dictionary(); + + public int GetTraitorCount(Tuple steamIdAndEndPoint) + { + if (steamIdAndEndPoint.Item1 > 0 && traitorCountsBySteamId.TryGetValue(steamIdAndEndPoint.Item1, out var steamIdResult)) + { + return steamIdResult; + } + return traitorCountsByEndPoint.TryGetValue(steamIdAndEndPoint.Item2, out var endPointResult) ? endPointResult : 0; + } + + public void SetTraitorCount(Tuple steamIdAndEndPoint, int count) + { + if (steamIdAndEndPoint.Item1 > 0) + { + traitorCountsBySteamId[steamIdAndEndPoint.Item1] = count; + } + traitorCountsByEndPoint[steamIdAndEndPoint.Item2] = count; + } + + public bool IsTraitor(Character character) + { + if (Traitors == null) + { + return false; + } + return Traitors.Any(traitor => traitor.Character == character); + } + + public TraitorManager() + { + } + + public void Start(GameServer server) + { +#if DISABLE_MISSIONS + return; +#endif + if (server == null) return; + + Traitor.TraitorMission.InitializeRandom(); + this.server = server; + //TODO: configure countdowns in xml + startCountdown = MathHelper.Lerp(90.0f, 180.0f, (float)Traitor.TraitorMission.RandomDouble()); + traitorCountsBySteamId.Clear(); + traitorCountsByEndPoint.Clear(); + } + + public void Update(float deltaTime) + { +#if DISABLE_MISSIONS + return; +#endif + if (Missions.Any()) + { + bool missionCompleted = false; + bool gameShouldEnd = false; + Character.TeamType winningTeam = Character.TeamType.None; + foreach (var mission in Missions) + { + mission.Value.Update(deltaTime, () => + { + switch (mission.Key) + { + case Character.TeamType.Team1: + winningTeam = (winningTeam == Character.TeamType.None) ? Character.TeamType.Team2 : Character.TeamType.None; + break; + case Character.TeamType.Team2: + winningTeam = (winningTeam == Character.TeamType.None) ? Character.TeamType.Team1 : Character.TeamType.None; + break; + default: + break; + } + gameShouldEnd = true; + }); + if (!gameShouldEnd && mission.Value.IsCompleted) + { + missionCompleted = true; + foreach (var traitor in mission.Value.Traitors.Values) + { + traitor.UpdateCurrentObjective(""); + } + } + } + if (gameShouldEnd) + { + GameMain.GameSession.WinningTeam = winningTeam; + GameMain.Server.EndGame(); + return; + } + if (missionCompleted) + { + Missions.Clear(); + //TODO: configure countdowns in xml + startCountdown = MathHelper.Lerp(90.0f, 180.0f, (float)Traitor.TraitorMission.RandomDouble()); + } + } + else if (startCountdown > 0.0f && server.GameStarted) + { + startCountdown -= deltaTime; + if (startCountdown <= 0.0f) + { + int playerCharactersCount = server.ConnectedClients.Sum(client => client.Character != null && !client.Character.IsDead ? 1 : 0); + if (playerCharactersCount < server.ServerSettings.TraitorsMinPlayerCount) + { + startCountdown = 60.0f; + return; + } + if (GameMain.GameSession.Mission is CombatMission) + { + var teamIds = new[] { Character.TeamType.Team1, Character.TeamType.Team2 }; + foreach (var teamId in teamIds) + { + var mission = TraitorMissionPrefab.RandomPrefab()?.Instantiate(); + if (mission != null) + { + Missions.Add(teamId, mission); + } + } + var canBeStartedCount = Missions.Sum(mission => mission.Value.CanBeStarted(server, this, mission.Key, "traitor") ? 1 : 0); + if (canBeStartedCount >= Missions.Count) + { + var startSuccessCount = Missions.Sum(mission => mission.Value.Start(server, this, mission.Key, "traitor") ? 1 : 0); + if (startSuccessCount >= Missions.Count) + { + return; + } + } + } + else + { + var mission = TraitorMissionPrefab.RandomPrefab()?.Instantiate(); + if (mission != null) { + if (mission.CanBeStarted(server, this, Character.TeamType.None, "traitor")) + { + if (mission.Start(server, this, Character.TeamType.None, "traitor")) + { + Missions.Add(Character.TeamType.None, mission); + return; + } + } + } + } + Missions.Clear(); + startCountdown = 60.0f; + } + } + } + + public string GetEndMessage() + { +#if DISABLE_MISSIONS + return ""; +#endif + if (GameMain.Server == null || !Missions.Any()) return ""; + + return string.Join("\n\n", Missions.Select(mission => mission.Value.GlobalEndMessage)); + } + + public static T WeightedRandom(ICollection collection, Func random, Func readSelectedWeight, Action writeSelectedWeight, int entryWeight, int selectionWeight) where T : class + { + var count = collection.Count; + if (count <= 0) + { + return null; + } + var maxCount = entryWeight + collection.Max(readSelectedWeight); + var totalWeight = collection.Sum(entry => maxCount - readSelectedWeight(entry)); + var selected = random(totalWeight); + foreach (var entry in collection) + { + var weight = readSelectedWeight(entry); + selected -= maxCount; + selected += weight; + if (selected <= 0) + { + writeSelectedWeight(entry, weight + selectionWeight); + return entry; + } + } + return null; + } + } +} diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/TraitorMission.cs b/Barotrauma/BarotraumaServer/Source/Traitors/TraitorMission.cs new file mode 100644 index 000000000..d29884ce1 --- /dev/null +++ b/Barotrauma/BarotraumaServer/Source/Traitors/TraitorMission.cs @@ -0,0 +1,320 @@ +//#define SERVER_IS_TRAITOR +//#define ALLOW_SOLO_TRAITOR + +using System; +using Barotrauma.Networking; +using Lidgren.Network; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Barotrauma.Extensions; + +namespace Barotrauma +{ + partial class Traitor + { + public class TraitorMission + { + private static System.Random random = null; + + public static void InitializeRandom() => random = new System.Random((int)DateTime.UtcNow.Ticks); + + // All traitor related functionality should use the following interface for generating random values + public static int Random(int n) => random.Next(n); + + // All traitor related functionality should use the following interface for generating random values + public static double RandomDouble() => random.NextDouble(); + + private static string wordsTxt = Path.Combine("Content", "CodeWords.txt"); + + private readonly List allObjectives = new List(); + private readonly List pendingObjectives = new List(); + private readonly List completedObjectives = new List(); + + public virtual bool IsCompleted => pendingObjectives.Count <= 0; + + public readonly Dictionary Traitors = new Dictionary(); + + public string StartText { get; private set; } + public string CodeWords { get; private set; } + public string CodeResponse { get; private set; } + public string EndMessage { + get + { + if (!Traitors.TryGetValue("traitor", out Traitor traitor)) + { + return ""; + } + + if (pendingObjectives.Count <= 0) + { + if (completedObjectives.Count <= 0) return ""; + return completedObjectives[completedObjectives.Count - 1].EndMessageText; + } + else + { + return pendingObjectives[0].EndMessageText; + } + } + } + + public string GlobalEndMessageSuccessTextId { get; private set; } + public string GlobalEndMessageSuccessDeadTextId { get; private set; } + public string GlobalEndMessageSuccessDetainedTextId { get; private set; } + public string GlobalEndMessageFailureTextId { get; private set; } + public string GlobalEndMessageFailureDeadTextId { get; private set; } + public string GlobalEndMessageFailureDetainedTextId { get; private set; } + + private readonly string objectiveGoalInfoFormat = "[index]. [goalinfos]\n"; + + public virtual IEnumerable GlobalEndMessageKeys => new string[] { "[traitorname]", "[traitorgoalinfos]" }; + public virtual IEnumerable GlobalEndMessageValues { + get { + var isSuccess = completedObjectives.Count >= allObjectives.Count; + return new string[] { + (Traitors.TryGetValue("traitor", out var traitor) ? traitor.Character?.Name : null) ?? "(unknown)", + (isSuccess ? completedObjectives.LastOrDefault() : pendingObjectives.FirstOrDefault())?.GoalInfos ?? "" + }; + } + } + + public string GlobalEndMessage + { + get + { + if (!Traitors.TryGetValue("traitor", out Traitor traitor)) + { + return ""; + } + + if (allObjectives.Count > 0) + { + var isSuccess = completedObjectives.Count >= allObjectives.Count; + var traitorIsDead = traitor.Character.IsDead; + var traitorIsDetained = traitor.Character.LockHands; + var messageId = isSuccess + ? (traitorIsDead ? GlobalEndMessageSuccessDeadTextId : traitorIsDetained ? GlobalEndMessageSuccessDetainedTextId : GlobalEndMessageSuccessTextId) + : (traitorIsDead ? GlobalEndMessageFailureDeadTextId : traitorIsDetained ? GlobalEndMessageFailureDetainedTextId : GlobalEndMessageFailureTextId); + return TextManager.FormatServerMessageWithGenderPronouns(traitor.Character?.Info?.Gender ?? Gender.None, messageId, GlobalEndMessageKeys.ToArray(), GlobalEndMessageValues.ToArray()); + } + return ""; + } + } + + public Objective GetCurrentObjective(Traitor traitor) + { + return pendingObjectives.Count > 0 ? pendingObjectives[0] : null; + } + + protected List> FindTraitorCandidates(GameServer server, Character.TeamType team, params string[] traitorRoles) + { + var traitorCandidates = new List>(); +#if SERVER_IS_TRAITOR + if (server.Character != null) + { + traitorCandidates.Add(server.Character); + } + else +#endif + { + traitorCandidates.AddRange(server.ConnectedClients.FindAll(c => c.Character != null && !c.Character.IsDead && (team == Character.TeamType.None || c.Character.TeamID == team)).ConvertAll(client => Tuple.Create(client, client.Character))); + } + return traitorCandidates; + } + + protected List FindCharacters() + { + List characters = new List(); + foreach (var character in Character.CharacterList) + { + characters.Add(character); + } + return characters; + } + + public virtual bool CanBeStarted(GameServer server, TraitorManager traitorManager, Character.TeamType team, params string[] traitorRoles) + { + var traitorCandidates = FindTraitorCandidates(server, team, traitorRoles); + if (traitorCandidates.Count <= 0) + { + return false; + } + var characters = FindCharacters(); +#if !ALLOW_SOLO_TRAITOR + if (characters.Count < 2) + { + return false; + } +#endif + return true; + } + + public virtual bool Start(GameServer server, TraitorManager traitorManager, Character.TeamType team, params string[] traitorRoles) + { + List characters = FindCharacters(); + List> traitorCandidates = FindTraitorCandidates(server, team, traitorRoles); + if (traitorCandidates.Count <= 0) + { + return false; + } +#if !ALLOW_SOLO_TRAITOR + if (characters.Count < 2) + { + return false; + } +#endif + CodeWords = ToolBox.GetRandomLine(wordsTxt) + ", " + ToolBox.GetRandomLine(wordsTxt); + CodeResponse = ToolBox.GetRandomLine(wordsTxt) + ", " + ToolBox.GetRandomLine(wordsTxt); + Traitors.Clear(); + foreach (var role in traitorRoles) + { + var candidate = TraitorManager.WeightedRandom(traitorCandidates, Random, t => + { + var previousClient = server.FindPreviousClientData(t.Item1); + return Math.Max( + previousClient != null ? traitorManager.GetTraitorCount(previousClient) : 0, + traitorManager.GetTraitorCount(Tuple.Create(t.Item1.SteamID, t.Item1.Connection?.EndPointString ?? ""))); + }, (t, c) => + { + traitorManager.SetTraitorCount(Tuple.Create(t.Item1.SteamID, t.Item1.Connection?.EndPointString ?? ""), c); + }, 2, 3); + traitorCandidates.Remove(candidate); + + var traitor = new Traitor(this, role, candidate.Item2); + Traitors.Add(role, traitor); + } + + var messages = new Dictionary>(); + foreach (var traitor in Traitors.Values) + { + messages[traitor] = new List(); + if (traitor.CurrentObjective == null) { continue; } + traitor.Greet(server, CodeWords, CodeResponse, message => messages[traitor].Add(message)); + } + + messages.ForEach(traitor => traitor.Value.ForEach(message => traitor.Key.SendChatMessage(message))); + Update(0.0f, GameMain.Server.EndGame); + messages.ForEach(traitor => traitor.Value.ForEach(message => traitor.Key.SendChatMessageBox(message))); +#if SERVER + foreach (var traitor in Traitors.Values) + { + GameServer.Log(string.Format("{0} is the traitor and the current goals are:\n{1}", traitor.Character.Name, traitor.CurrentObjective?.GoalInfos != null ? TextManager.GetServerMessage(traitor.CurrentObjective?.GoalInfos) : "(empty)"), ServerLog.MessageType.ServerMessage); + } +#endif + return true; + } + + public delegate void TraitorWinHandler(); + + public virtual void Update(float deltaTime, TraitorWinHandler winHandler) + { + if (pendingObjectives.Count <= 0 || Traitors.Count <= 0) + { + return; + } + foreach (var traitor in Traitors.Values) + { + if (traitor.Character.IsDead) + { + traitor.UpdateCurrentObjective(""); + } + } + int previousCompletedCount = completedObjectives.Count; + int startedCount = 0; + while (pendingObjectives.Count > 0) + { + var objective = pendingObjectives[0]; + if (!objective.IsStarted) + { + if (!objective.Start(Traitors["traitor"])) + { + pendingObjectives.RemoveAt(0); + completedObjectives.Add(objective); + if (pendingObjectives.Count > 0) + { + objective.EndMessage(); + } + continue; + } + ++startedCount; + } + objective.Update(deltaTime); + if (objective.IsCompleted) + { + pendingObjectives.RemoveAt(0); + completedObjectives.Add(objective); + if (pendingObjectives.Count > 0) + { + objective.EndMessage(); + } + continue; + } + if (!objective.CanBeCompleted) + { + objective.EndMessage(); + objective.End(true); + pendingObjectives.Clear(); + } + break; + } + int completedMax = completedObjectives.Count - 1; + for (int i = previousCompletedCount; i <= completedMax; ++i) + { + var objective = completedObjectives[i]; + objective.End(i < completedMax || pendingObjectives.Count > 0); + } + if (pendingObjectives.Count > 0) + { + if (startedCount > 0) + { + pendingObjectives[0].StartMessage(); + } + } + else if (completedObjectives.Count >= allObjectives.Count) + { + foreach (var traitor in Traitors) + { + SteamAchievementManager.OnTraitorWin(traitor.Value.Character); + } + winHandler(); + } + } + + public delegate bool CharacterFilter(Character character); + public Character FindKillTarget(Character traitor, CharacterFilter filter) + { + if (traitor == null) { return null; } + + List validCharacters = Character.CharacterList.FindAll(c => + c.TeamID == traitor.TeamID && + c != traitor && + !c.IsDead && + (filter == null || filter(c))); + + if (validCharacters.Count > 0) + { + return validCharacters[Random(validCharacters.Count)]; + } + +#if ALLOW_SOLO_TRAITOR + return traitor; +#else + return null; +#endif + } + + public TraitorMission(string startText, string globalEndMessageSuccessTextId, string globalEndMessageSuccessDeadTextId, string globalEndMessageSuccessDetainedTextId, string globalEndMessageFailureTextId, string globalEndMessageFailureDeadTextId, string globalEndMessageFailureDetainedTextId, params Objective[] objectives) + { + StartText = startText; + GlobalEndMessageSuccessTextId = globalEndMessageSuccessTextId; + GlobalEndMessageSuccessDeadTextId = globalEndMessageSuccessDeadTextId; + GlobalEndMessageSuccessDetainedTextId = globalEndMessageSuccessDetainedTextId; + GlobalEndMessageFailureTextId = globalEndMessageFailureTextId; + GlobalEndMessageFailureDeadTextId = globalEndMessageFailureDeadTextId; + GlobalEndMessageFailureDetainedTextId = globalEndMessageFailureDetainedTextId; + allObjectives.AddRange(objectives); + pendingObjectives.AddRange(objectives); + } + } + } +} diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/TraitorMissionPrefab.cs b/Barotrauma/BarotraumaServer/Source/Traitors/TraitorMissionPrefab.cs new file mode 100644 index 000000000..686086ec5 --- /dev/null +++ b/Barotrauma/BarotraumaServer/Source/Traitors/TraitorMissionPrefab.cs @@ -0,0 +1,475 @@ +using System; +using System.Collections.Generic; +using System.Xml.Linq; +using System.Linq; +using Barotrauma.Extensions; +using Barotrauma.Networking; + +namespace Barotrauma { + + class TraitorMissionPrefab + { + public class TraitorMissionEntry + { + public readonly TraitorMissionPrefab Prefab; + public int SelectedWeight; + + public TraitorMissionEntry(XElement element) + { + Prefab = new TraitorMissionPrefab(element); + SelectedWeight = 0; + } + } + public static readonly List List = new List(); + + public static void Init() + { + var files = GameMain.Instance.GetFilesOfType(ContentType.TraitorMissions); + foreach (string file in files) + { + XDocument doc = XMLExtensions.TryLoadXml(file); + if (doc?.Root == null) continue; + + foreach (XElement element in doc.Root.Elements()) + { + List.Add(new TraitorMissionEntry(element)); + } + } + } + + public static TraitorMissionPrefab RandomPrefab() + { + return TraitorManager.WeightedRandom(List, Traitor.TraitorMission.Random, entry => entry.SelectedWeight, (entry, weight) => entry.SelectedWeight = weight, 2, 3)?.Prefab; + } + + private class AttributeChecker : IDisposable + { + private readonly XElement element; + private readonly HashSet required = new HashSet(); + private readonly HashSet optional = new HashSet(); + + public void Optional(params string[] names) + { + optional.UnionWith(names); + } + + public void Required(params string[] names) + { + required.UnionWith(names); + } + + public void Dispose() + { + foreach (var requiredName in required) + { + if (element.Attributes().All(attribute => attribute.Name != requiredName)) + { + GameServer.Log($"Required attribute \"{requiredName}\" is missing in \"{element.Name}\"", ServerLog.MessageType.Error); + } + } + foreach (var attribute in element.Attributes()) + { + var attributeName = attribute.Name.ToString(); + if (!required.Contains(attributeName) && !optional.Contains(attributeName)) + { + GameServer.Log($"Unsupported attribute \"{attributeName}\" in \"{element.Name}\"", ServerLog.MessageType.Error); + } + } + } + + public AttributeChecker(XElement element) + { + this.element = element; + } + } + + public class Goal + { + public readonly string Type; + public readonly XElement Config; + + public Goal(string type, XElement config) + { + Type = type; + Config = config; + } + + private delegate bool TargetFilter(string value, Character character); + private static Dictionary targetFilters = new Dictionary() + { + { "job", (value, character) => value.Equals(character.Info.Job.Prefab.Identifier, StringComparison.OrdinalIgnoreCase) }, + }; + + public Traitor.Goal Instantiate() + { + Traitor.Goal goal = null; + using (var checker = new AttributeChecker(Config)) + { + checker.Required("type"); + var goalType = Config.GetAttributeString("type", ""); + switch (goalType.ToLowerInvariant()) + { + case "killtarget": + { + checker.Optional(targetFilters.Keys.ToArray()); + List filters = new List(); + foreach (var attribute in Config.Attributes()) + { + if (targetFilters.TryGetValue(attribute.Name.ToString().ToLower(System.Globalization.CultureInfo.InvariantCulture), out var filter)) + { + filters.Add((character) => filter(attribute.Value, character)); + } + } + goal = new Traitor.GoalKillTarget((character) => filters.All(f => f(character))); + break; + } + case "destroyitems": + { + checker.Required("tag"); + checker.Optional("percentage", "matchIdentifier", "matchTag", "matchInventory"); + var tag = Config.GetAttributeString("tag", null); + if (tag != null) + { + goal = new Traitor.GoalDestroyItemsWithTag( + tag, + Config.GetAttributeFloat("percentage", 100.0f) / 100.0f, + Config.GetAttributeBool("matchIdentifier", true), + Config.GetAttributeBool("matchTag", true), + Config.GetAttributeBool("matchInventory", false)); + } + break; + } + case "sabotage": + { + checker.Required("tag"); + checker.Optional("threshold"); + var tag = Config.GetAttributeString("tag", null); + if (tag != null) + { + goal = new Traitor.GoalSabotageItems(tag, Config.GetAttributeFloat("threshold", 20.0f)); + } + break; + } + case "floodsub": + checker.Optional("percentage"); + goal = new Traitor.GoalFloodPercentOfSub(Config.GetAttributeFloat("percentage", 100.0f) / 100.0f); + break; + case "finditem": + checker.Required("identifier"); + checker.Optional("preferNew", "allowNew", "allowExisting", "allowedContainers"); + goal = new Traitor.GoalFindItem(Config.GetAttributeString("identifier", null), Config.GetAttributeBool("preferNew", true), Config.GetAttributeBool("allowNew", true), Config.GetAttributeBool("allowExisting", true), Config.GetAttributeStringArray("allowedContainers", new string[] {"steelcabinet", "mediumsteelcabinet", "suppliescabinet"})); + break; + case "replaceinventory": + checker.Required("containers", "replacements"); + checker.Optional("percentage"); + goal = new Traitor.GoalReplaceInventory(Config.GetAttributeStringArray("containers", new string[] { }), Config.GetAttributeStringArray("replacements", new string[] { }), Config.GetAttributeFloat("percentage", 100.0f) / 100.0f); + break; + case "reachdistancefromsub": + checker.Optional("distance"); + goal = new Traitor.GoalReachDistanceFromSub(Config.GetAttributeFloat("distance", 10000.0f)); + break; + default: + GameServer.Log($"Unrecognized goal type \"{goalType}\".", ServerLog.MessageType.Error); + break; + } + } + if (goal == null) + { + return null; + } + foreach (var element in Config.Elements()) + { + switch (element.Name.ToString().ToLowerInvariant()) + { + case "modifier": + { + using (var checker = new AttributeChecker(element)) + { + checker.Required("type"); + var modifierType = element.GetAttributeString("type", ""); + switch (modifierType) + { + case "duration": + { + checker.Optional("cumulative", "duration", "infotext"); + var isCumulative = element.GetAttributeBool("cumulative", false); + goal = new Traitor.GoalHasDuration(goal, element.GetAttributeFloat("duration", 5.0f), isCumulative, element.GetAttributeString("infotext", isCumulative ? "TraitorGoalWithCumulativeDurationInfoText" : "TraitorGoalWithDurationInfoText")); + break; + } + case "timelimit": + checker.Optional("timelimit", "infotext"); + goal = new Traitor.GoalHasTimeLimit(goal, element.GetAttributeFloat("timelimit", 180.0f), element.GetAttributeString("infotext", "TraitorGoalWithTimeLimitInfoText")); + break; + case "optional": + checker.Optional("infotext"); + goal = new Traitor.GoalIsOptional(goal, element.GetAttributeString("infotext", "TraitorGoalIsOptionalInfoText")); + break; + default: + GameServer.Log($"Unrecognized modifier type \"{modifierType}\".", ServerLog.MessageType.Error); + break; + } + } + break; + } + } + } + foreach (var element in Config.Elements()) + { + var elementName = element.Name.ToString().ToLowerInvariant(); + switch (elementName) + { + case "modifier": + // loaded above + break; + case "infotext": + { + using (var checker = new AttributeChecker(element)) + { + checker.Required("id"); + var id = element.GetAttributeString("id", null); + if (id != null) + { + goal.InfoTextId = id; + } + } + break; + } + case "completedtext": + { + using (var checker = new AttributeChecker(element)) + { + checker.Required("id"); + var id = element.GetAttributeString("id", null); + if (id != null) + { + goal.CompletedTextId = id; + } + } + break; + } + default: + GameServer.Log($"Unrecognized element \"{element.Name}\" in goal.", ServerLog.MessageType.Error); + break; + } + } + return goal; + } + } + + public class Objective + { + public string InfoText { get; internal set; } + public string StartMessageTextId { get; internal set; } + public string StartMessageServerTextId { get; internal set; } + public string EndMessageSuccessTextId { get; internal set; } + public string EndMessageSuccessDeadTextId { get; internal set; } + public string EndMessageSuccessDetainedTextId { get; internal set; } + public string EndMessageFailureTextId { get; internal set; } + public string EndMessageFailureDeadTextId { get; internal set; } + public string EndMessageFailureDetainedTextId { get; internal set; } + public int ShuffleGoalsCount { get; internal set; } + + public readonly List Goals = new List(); + + public Traitor.Objective Instantiate() + { + var result = new Traitor.Objective(InfoText, ShuffleGoalsCount, Goals.ConvertAll(goal => { + var instance = goal.Instantiate(); + if (instance == null) + { + GameServer.Log($"Failed to instantiate goal \"{goal.Type}\".", ServerLog.MessageType.Error); + } + return instance; + }).FindAll(goal => goal != null).ToArray()); + if (StartMessageTextId != null) + { + result.StartMessageTextId = StartMessageTextId; + } + if (StartMessageServerTextId != null) + { + result.StartMessageServerTextId = StartMessageServerTextId; + } + if (EndMessageSuccessTextId != null) + { + result.EndMessageSuccessTextId = EndMessageSuccessTextId; + } + if (EndMessageSuccessDeadTextId != null) + { + result.EndMessageSuccessDeadTextId = EndMessageSuccessDeadTextId; + } + if (EndMessageSuccessDetainedTextId != null) + { + result.EndMessageSuccessDetainedTextId = EndMessageSuccessDetainedTextId; + } + if (EndMessageFailureTextId != null) + { + result.EndMessageFailureTextId = EndMessageFailureTextId; + } + if (EndMessageFailureDeadTextId != null) + { + result.EndMessageFailureDeadTextId = EndMessageFailureDeadTextId; + } + if (EndMessageFailureDetainedTextId != null) + { + result.EndMessageFailureDetainedTextId = EndMessageFailureDetainedTextId; + } + return result; + } + } + /* + public class Role + { + public string Job; + } + + public readonly Dictionary Roles = new Dictionary(); + */ + public readonly string Identifier; + public readonly string StartText; + public readonly string EndMessageSuccessText; + public readonly string EndMessageSuccessDeadText; + public readonly string EndMessageSuccessDetainedText; + public readonly string EndMessageFailureText; + public readonly string EndMessageFailureDeadText; + public readonly string EndMessageFailureDetainedText; + + public readonly List Objectives = new List(); + + public Traitor.TraitorMission Instantiate() + { + return new Traitor.TraitorMission( + StartText ?? "TraitorMissionStartMessage", + EndMessageSuccessText ?? "TraitorObjectiveEndMessageSuccess", + EndMessageSuccessDeadText ?? "TraitorObjectiveEndMessageSuccessDead", + EndMessageSuccessDetainedText ?? "TraitorObjectiveEndMessageSuccessDetained", + EndMessageFailureText ?? "TraitorObjectiveEndMessageFailure", + EndMessageFailureDeadText ?? "TraitorObjectiveEndMessageFailureDead", + EndMessageFailureDetainedText ?? "TraitorObjectiveEndMessageFailureDetained", + Objectives.ConvertAll(objective => objective.Instantiate()).ToArray()); + } + + protected Goal LoadGoal(XElement goalRoot) + { + var goalType = goalRoot.GetAttributeString("type", ""); + return new Goal(goalType, goalRoot); + } + + protected Objective LoadObjective(XElement objectiveRoot) + { + var result = new Objective(); + result.ShuffleGoalsCount = objectiveRoot.GetAttributeInt("shuffleGoalsCount", -1); + foreach (var element in objectiveRoot.Elements()) + { + using (var checker = new AttributeChecker(element)) + { + switch (element.Name.ToString().ToLowerInvariant()) + { + case "infotext": + checker.Required("id"); + result.InfoText = element.GetAttributeString("id", null); + break; + case "startmessage": + checker.Required("id"); + result.StartMessageTextId = element.GetAttributeString("id", null); + break; + case "startmessageserver": + checker.Required("id"); + result.StartMessageServerTextId = element.GetAttributeString("id", null); + break; + case "endmessagesuccess": + checker.Required("id"); + result.EndMessageSuccessTextId = element.GetAttributeString("id", null); + break; + case "endmessagesuccessdead": + checker.Required("id"); + result.EndMessageSuccessDeadTextId = element.GetAttributeString("id", null); + break; + case "endmessagesuccessdetained": + checker.Required("id"); + result.EndMessageSuccessDetainedTextId = element.GetAttributeString("id", null); + break; + case "endmessagefailure": + checker.Required("id"); + result.EndMessageFailureTextId = element.GetAttributeString("id", null); + break; + case "endmessagefailuredead": + checker.Required("id"); + result.EndMessageFailureDeadTextId = element.GetAttributeString("id", null); + break; + case "endmessagefailuredetained": + checker.Required("id"); + result.EndMessageFailureDetainedTextId = element.GetAttributeString("id", null); + break; + case "goal": + { + var goal = LoadGoal(element); + if (goal != null) + { + result.Goals.Add(goal); + } + break; + } + default: + GameServer.Log($"Unrecognized element \"{element.Name}\"under Objective.", ServerLog.MessageType.Error); + break; + } + } + } + return result; + } + + public TraitorMissionPrefab(XElement missionRoot) + { + Identifier = missionRoot.GetAttributeString("identifier", null); + foreach (var element in missionRoot.Elements()) + { + using (var checker = new AttributeChecker(element)) + { + switch (element.Name.ToString().ToLowerInvariant()) + { + case "startinfotext": + checker.Required("id"); + StartText = element.GetAttributeString("id", null); + break; + case "endmessagesuccess": + checker.Required("id"); + EndMessageSuccessText = element.GetAttributeString("id", null); + break; + case "endmessagesuccessdead": + checker.Required("id"); + EndMessageSuccessDeadText = element.GetAttributeString("id", null); + break; + case "endmessagesuccessdetained": + checker.Required("id"); + EndMessageSuccessDetainedText = element.GetAttributeString("id", null); + break; + case "endmessagefailure": + checker.Required("id"); + EndMessageFailureText = element.GetAttributeString("id", null); + break; + case "endmessagefailuredead": + checker.Required("id"); + EndMessageFailureDeadText = element.GetAttributeString("id", null); + break; + case "endmessagefailuredetained": + checker.Required("id"); + EndMessageFailureDetainedText = element.GetAttributeString("id", null); + break; + case "objective": + { + var objective = LoadObjective(element); + if (objective != null) + { + Objectives.Add(objective); + } + break; + } + default: + GameServer.Log($"Unrecognized element \"{element.Name}\"under TraitorMission.", ServerLog.MessageType.Error); + break; + } + } + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml index 9cd0afff7..93a775acf 100644 --- a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml +++ b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml @@ -1,4 +1,4 @@ - + @@ -82,6 +82,7 @@ + @@ -91,6 +92,10 @@ + + + + @@ -104,6 +109,7 @@ + diff --git a/Barotrauma/BarotraumaShared/Data/karmasettings.xml b/Barotrauma/BarotraumaShared/Data/karmasettings.xml index 50ba0e140..bbbda55fc 100644 --- a/Barotrauma/BarotraumaShared/Data/karmasettings.xml +++ b/Barotrauma/BarotraumaShared/Data/karmasettings.xml @@ -14,7 +14,7 @@ damageenemykarmaincrease="0.1" damagefriendlykarmadecrease="0.2" extinguishfirekarmaincrease="1" - allowedwiredisconnectionsperminute="5" + allowedwiredisconnectionsperminute="3" wiredisconnectionkarmadecrease="6.0" steersubkarmaincrease="0.15" spamfilterkarmadecrease="15" @@ -35,7 +35,7 @@ damageenemykarmaincrease="0.1" damagefriendlykarmadecrease="0.3" extinguishfirekarmaincrease="1" - allowedwiredisconnectionsperminute="4" + allowedwiredisconnectionsperminute="1" wiredisconnectionkarmadecrease="10.0" steersubkarmaincrease="0.15" spamfilterkarmadecrease="25" @@ -56,7 +56,7 @@ damageenemykarmaincrease="0.1" damagefriendlykarmadecrease="0.2" extinguishfirekarmaincrease="1" - allowedwiredisconnectionsperminute="5" + allowedwiredisconnectionsperminute="3" wiredisconnectionkarmadecrease="6.0" steersubkarmaincrease="0.15" spamfilterkarmadecrease="15" diff --git a/Barotrauma/BarotraumaShared/Data/permissionpresets.xml b/Barotrauma/BarotraumaShared/Data/permissionpresets.xml index 648674540..d06d1d321 100644 --- a/Barotrauma/BarotraumaShared/Data/permissionpresets.xml +++ b/Barotrauma/BarotraumaShared/Data/permissionpresets.xml @@ -25,7 +25,13 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Humpback2.sub b/Barotrauma/BarotraumaShared/Mods/ExampleMod/Humpback2.sub new file mode 100644 index 000000000..38b34627a Binary files /dev/null and b/Barotrauma/BarotraumaShared/Mods/ExampleMod/Humpback2.sub differ diff --git a/Barotrauma/BarotraumaShared/Mods/ExampleMod/PreviewImage.png b/Barotrauma/BarotraumaShared/Mods/ExampleMod/PreviewImage.png new file mode 100644 index 000000000..c76c81542 Binary files /dev/null and b/Barotrauma/BarotraumaShared/Mods/ExampleMod/PreviewImage.png differ diff --git a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Animations/RedcrawlerRun.xml b/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Animations/RedcrawlerRun.xml new file mode 100644 index 000000000..6da1fe1c9 --- /dev/null +++ b/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Animations/RedcrawlerRun.xml @@ -0,0 +1,23 @@ + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Animations/RedcrawlerSwimFast.xml b/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Animations/RedcrawlerSwimFast.xml new file mode 100644 index 000000000..db65d0370 --- /dev/null +++ b/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Animations/RedcrawlerSwimFast.xml @@ -0,0 +1,20 @@ + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Animations/RedcrawlerSwimSlow.xml b/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Animations/RedcrawlerSwimSlow.xml new file mode 100644 index 000000000..ab9573b83 --- /dev/null +++ b/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Animations/RedcrawlerSwimSlow.xml @@ -0,0 +1,20 @@ + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Animations/RedcrawlerWalk.xml b/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Animations/RedcrawlerWalk.xml new file mode 100644 index 000000000..b46d3a4c0 --- /dev/null +++ b/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Animations/RedcrawlerWalk.xml @@ -0,0 +1,23 @@ + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Ragdolls/RedcrawlerDefaultRagdoll.xml b/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Ragdolls/RedcrawlerDefaultRagdoll.xml new file mode 100644 index 000000000..ef45bf03e --- /dev/null +++ b/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Ragdolls/RedcrawlerDefaultRagdoll.xml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Redcrawler.xml b/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Redcrawler.xml new file mode 100644 index 000000000..1135fd55a --- /dev/null +++ b/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Redcrawler.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/crawler.png b/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/crawler.png new file mode 100644 index 000000000..31f9101d9 Binary files /dev/null and b/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/crawler.png differ diff --git a/Barotrauma/BarotraumaShared/Mods/ExampleMod/filelist.xml b/Barotrauma/BarotraumaShared/Mods/ExampleMod/filelist.xml new file mode 100644 index 000000000..82367efc8 --- /dev/null +++ b/Barotrauma/BarotraumaShared/Mods/ExampleMod/filelist.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Mods/info.txt b/Barotrauma/BarotraumaShared/Mods/info.txt index 4a2d5754b..c48fff272 100644 --- a/Barotrauma/BarotraumaShared/Mods/info.txt +++ b/Barotrauma/BarotraumaShared/Mods/info.txt @@ -1,17 +1,14 @@ ------------------------------------------------------------------------ -Modding info/tips: +General ------------------------------------------------------------------------ - When doing an automatic update through the launcher, any old/unnecessary - files in the Content folder will be deleted. This also includes any new - files you may have added to the folder. + This folder is the place where mods installed from the Steam Workshop go. + You should also place your own mods in this folder, under a separate + subfolder (e.g. "Mods/MyMod"). - It is recommended to save all modifications to the ''Mods'' folder, or - in the case of custom subs, to the 'Submarines'' folder. - ----------------------------------- +------------------------------------------------------------------------ Content Packages: ----------------------------------- +------------------------------------------------------------------------ Content packages determine which configuration files the game will be using. This includes the configuration files for items, map structures, characters, @@ -20,19 +17,18 @@ Content Packages: In the multiplayer mode, players are required to use the same content package as the server or they won't be able to join. - If you are making a mod for the game, it is recommended to create a new content - package instead of just replacing existing files in the content folder. - This way you and anyone else using the mod can easily change between different - mods and the vanilla version, without having to manually replace files in the - Content folder or keep backups of different files. + All mods published in the Steam Workshop need a content package. - The content packages are configured in XML files which are saved in Data\ContentPackages. + If you just want to publish a custom submarine in the workshop, you + don't need to worry about content packages - you can just select the + submarine from the "Publish item" tab in the Workshop menu, and the + game automatically creates a folder and content package for your mod. Example: A very simple content package could be configured as follows: - + @@ -50,178 +46,4 @@ Example: a new event that spawns Cthulhu and removing the events that spawn monsters/items which aren't included in the mod. - It is also set to be used with the version 0.9.0.0 of the game. - ----------------------------------- -Creature modding: ----------------------------------- - - All the creatures/characters in the game are configured in XML files. - - The XML files should be formatted in the following way: - - - - - - - - - - - - - - - - - - - ------------------------------------------ - - Elements: - - Character: - - has to be the root element of the file - - Attributes: - name: the name of the creature - humanoid: true/false, if set to true the character is animated using - a bipedal animator (like humans) - needsair: true/false, does the character drown/suffocate without - oxygen (false by default) - drowningtime: how fast the character drowns (in seconds) - doesbleed: if set to false, the character takes no bleeding damage - health: self explanatory (100.0 by default) - - Ragdoll: - - has to be a child element of the character element - - Attributes: - headposition: how high from the ground the head of the character should - be when the character is standing (50.0 by default) - headangle: an angle which the head is rotated to when the character is walking - (0.0 by default, meaning that the head will face straight forward) - torsoposition: same as headposition but for torso (50.0 by default) - torsoangle: an angle which the torso is rotated to when the character is walking - (0.0 by default, meaning that it will face straight forward) - waveamplitude, wavelength: if the character is not a humanoid, it will - do a "wave-like" swimming movement with the selected amplitude and - wavelength. To put it simply, amplitude affects how large up-and-down - movement the character will do and wavelength affects how fast the - character does the movement (both 0.0 by default) - flip: should the entire character be "mirrored" over the y-axis when it - switches its movement direction from left to right or vice versa, - or should it just rotate along the z-axis (false by default) - walkspeed, swimspeed: how fast the character should move on land and - in water, (the actual speeds are also affected by the weight, shape - and steerforces of individual limbs) (both 1.0 by default) - swimspeed: how fast the character should move in water - - Limb: - - an individual part of the ragdoll - - has to be a child element of the ragdoll element - - Attributes: - id: an integer that is used to distinguish between limbs when connecting them - with joints. The first limb should have the id "0", the second "1" and so on. - radius, width, height: used for setting the dimensions of the physics body - of the limb. If only radius is set, the limb will be a circle with - the selected radius. If width and height are set, it will be a rectangle. - If radius and height are set, it will be a capsule. - density: the mass of the limb will be area_of_the_limb * density (default 10.0) - friction: the friction coefficient of the limb (0.3 by default) - flip: true/false, if set to true the limb will be "flipped" from one side - to another when the character turns around (as in, if a character is - facing left and has an arm extended left, the arm will be extended - to the right when the character faces to the right) (false by default) - ignorecollisions: true/false, should the limb collide with walls (true by default) - impacttolerance: if the limb receives an impact larger than this value, it takes - damage (20.0 by default) - type: determines how the limb should be animated and what kind of items can be - equipped on the limb. Can be set to None, LeftHand, RightHand, LeftArm, - RightArm, LeftLeg, RightLeg, LeftFoot, RightFoot, Head, Torso, Waist, Tail, - Legs, RightThigh or LeftThigh - pullpos: when animating the character, forces will be applied to this - point of the physics body of the limb. Defaults to "0.0, 0.0" which - is the center of the limb. - refjoint: index of the joint that is used as the "center point" along - the x-axis when doing a walking animation. For example, if the joint - between a characters thigh and waist is set as refjoint, the feet of the - character will be moved directly under said joint when the character is - standing still. - steerforce: how much force is applied to the limb when the character moves (0.0 by default) - armorsector: an armored sector between two angles (in degrees). For example, - -90,90 would make the front sector of the limb armored (0.0,0.0 by default) - armor: how effective the armor is: damage is divided by this value if an attack hits the - armored sector (1.0 by default) - - Sprite: - - a child element of a limb element - - Attributes: - texture: file path of the texture - sourcerect: which part of the texture should be used. If either width or - height are 0, they will be set to the width or height of the texture. - (0,0,0,0 by default) - origin: what point in the sprite is considered the "middle point". "0,0" - is the upper left corner of the sprite and "1,1" the lower right - corner. ("0.5, 0.5" by default) - depth: Affects the order which sprites are drawn in. Sprites with a - depth of 1.0 will be drawn under sprites that have the depth set - to 0.9 for example. Note that setting several limbs to the same - depth value may cause them to "flicker" on top of each other, so - it's recommended that every sprite has a slightly different depth. - - Attack: - - a child element of a limb element - - Attributes: - type: affects the logic for moving the attacking limb. At the moment the only - types are None, PinchCW and PinchCW - - PinchCW: the limb rotates clockwise when attacking (or counter-clockwise - if the character is facing left). Useful for attacks like biting - or slashing - PinchCCW: the same as PinchCW, but in the limb is rotated in the - opposite direction - Hit: the limb will "punch" the target - - damage: damage done to other characters (0.0 by default) - bleedingdamage: how much the attack affects the bleeding rate (0.0 by default) - structuredamage: damage done to structures (0.0 by default) - stun: how long the target is stunned (in seconds, 0.0 by default) - range: how close the limb doing the attack has to be to the target to do damage - (0.0 by default, but should be set to a higher value or otherwise it - will only do damage if the limb is exactly at the position of the target, - so practically never) - duration: how long the attack lasts - if set to zero, it will be a "one-hit" - attack, otherwise it will be active for a while and the damage values - will be damage per second - priority: can be used for adjusting how likely the character is to use specific - attacks. For example, if a character has two attacks, first one - having the priority 2.0 and the second 1.0, the character is twice as - likely to use the first one. - - Joint: - - a revolute joint connecting two limbs to each other - - a child element of the ragdoll element - - Attributes: - limb1, limb2: thes id of the limbs that should be connected - limb1anchor, limb2anchor: the points where the joint is attached to on - the limbs (0.0, 0.0 being the center) - lowerlimit, upperlimit: how much the joint can turn. If both are set to 0.0, - the joint can rotate freely. - ----------------------------------- -Editing items: ----------------------------------- - - (A more extensive tutorial coming up in the future) - - Items are also configured in XML files. An item consist of several ''components'' - which determine the functionality of the item. See the existing item files for - examples on the components. - - TO BE CONTINUED \ No newline at end of file + It is also set to be used with the version 0.9.1.0 of the game. \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedCode.projitems b/Barotrauma/BarotraumaShared/SharedCode.projitems index 78b2b83f0..50f93972a 100644 --- a/Barotrauma/BarotraumaShared/SharedCode.projitems +++ b/Barotrauma/BarotraumaShared/SharedCode.projitems @@ -235,6 +235,13 @@ + + + + + + + diff --git a/Barotrauma/BarotraumaShared/SharedCode.shproj.user b/Barotrauma/BarotraumaShared/SharedCode.shproj.user index a11cd7f91..12a505406 100644 --- a/Barotrauma/BarotraumaShared/SharedCode.shproj.user +++ b/Barotrauma/BarotraumaShared/SharedCode.shproj.user @@ -1,6 +1,6 @@  - true + false \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedContent.projitems b/Barotrauma/BarotraumaShared/SharedContent.projitems index ee46100fe..bdcdd6528 100644 --- a/Barotrauma/BarotraumaShared/SharedContent.projitems +++ b/Barotrauma/BarotraumaShared/SharedContent.projitems @@ -1,4 +1,4 @@ - + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) @@ -400,12 +400,18 @@ PreserveNewest + + PreserveNewest + PreserveNewest PreserveNewest + + PreserveNewest + PreserveNewest @@ -520,6 +526,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -614,6 +623,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -623,6 +635,12 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + PreserveNewest @@ -632,6 +650,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -689,6 +710,33 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + PreserveNewest @@ -2126,6 +2174,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -2354,6 +2405,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -2361,11 +2415,11 @@ PreserveNewest - Never + PreserveNewest - + + PreserveNewest - @@ -3539,4 +3593,4 @@ PreserveNewest - \ No newline at end of file + diff --git a/Barotrauma/BarotraumaShared/SharedContent.shproj.user b/Barotrauma/BarotraumaShared/SharedContent.shproj.user index 5bc13ae87..7e04c94d7 100644 --- a/Barotrauma/BarotraumaShared/SharedContent.shproj.user +++ b/Barotrauma/BarotraumaShared/SharedContent.shproj.user @@ -1,6 +1,6 @@  - true + false \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/AITarget.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/AITarget.cs index d432de18e..0d1742307 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/AITarget.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/AITarget.cs @@ -14,11 +14,6 @@ namespace Barotrauma get; private set; } - - /// - /// Use as a minimum or static sight range. - /// - public static float StaticSightRange = 3000; private float soundRange; private float sightRange; @@ -75,7 +70,7 @@ namespace Barotrauma public bool Enabled = true; public float MinSoundRange, MinSightRange; - public float MaxSoundRange = float.MaxValue, MaxSightRange = float.MaxValue; + public float MaxSoundRange = 100000, MaxSightRange = 100000; public TargetType Type { get; private set; } @@ -128,8 +123,8 @@ namespace Barotrauma { SightRange = element.GetAttributeFloat("sightrange", 0.0f); SoundRange = element.GetAttributeFloat("soundrange", 0.0f); - MinSightRange = element.GetAttributeFloat("minsightrange", SightRange); - MinSoundRange = element.GetAttributeFloat("minsoundrange", SoundRange); + MinSightRange = element.GetAttributeFloat("minsightrange", 0f); + MinSoundRange = element.GetAttributeFloat("minsoundrange", 0f); MaxSightRange = element.GetAttributeFloat("maxsightrange", SightRange); MaxSoundRange = element.GetAttributeFloat("maxsoundrange", SoundRange); FadeOutTime = element.GetAttributeFloat("fadeouttime", FadeOutTime); @@ -142,15 +137,9 @@ namespace Barotrauma } } - public AITarget(Entity e, float sightRange = -1, float soundRange = 0) + public AITarget(Entity e) { Entity = e; - if (sightRange < 0) - { - sightRange = StaticSightRange; - } - SightRange = sightRange; - SoundRange = soundRange; List.Add(this); } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/EnemyAIController.cs index 43428f673..10cd8374d 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/EnemyAIController.cs @@ -109,9 +109,9 @@ namespace Barotrauma private Dictionary targetMemories; //the eyesight of the NPC (0.0 = blind, 1.0 = sees every target within sightRange) - private float sight; + public float sight; //how far the NPC can hear targets from (0.0 = deaf, 1.0 = hears every target within soundRange) - private float hearing; + public float hearing; private float colliderSize; @@ -270,11 +270,14 @@ namespace Barotrauma return null; } - public override void SelectTarget(AITarget target) + public override void SelectTarget(AITarget target) => SelectTarget(target, 100); + + public void SelectTarget(AITarget target, float priority) { SelectedAiTarget = target; selectedTargetMemory = GetTargetMemory(target); - targetValue = 100.0f; + selectedTargetMemory.Priority = priority; + targetValue = priority; } public override void Update(float deltaTime) @@ -985,7 +988,7 @@ namespace Barotrauma var aiTarget = wallTarget.Structure.AiTarget; if (aiTarget != null && SelectedAiTarget != aiTarget) { - SelectTarget(aiTarget); + SelectTarget(aiTarget, GetTargetMemory(SelectedAiTarget).Priority); } } if (SelectedAiTarget.Entity is IDamageable damageTarget) diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/HumanAIController.cs index 479abd337..912084633 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/HumanAIController.cs @@ -370,7 +370,7 @@ namespace Barotrauma if (item.CurrentHull != hull) { continue; } if (AIObjectiveRepairItems.IsValidTarget(item, Character)) { - if (item.Repairables.All(r => item.Condition > r.ShowRepairUIThreshold)) { continue; } + if (item.Repairables.All(r => item.ConditionPercentage > r.ShowRepairUIThreshold)) { continue; } AddTargets(Character, item); if (newOrder == null) { @@ -640,7 +640,7 @@ namespace Barotrauma if (item.CurrentHull != hull) { continue; } if (AIObjectiveRepairItems.IsValidTarget(item, character)) { - if (item.Repairables.All(r => item.Condition > r.ShowRepairUIThreshold)) { continue; } + if (item.Repairables.All(r => item.ConditionPercentage > r.ShowRepairUIThreshold)) { continue; } AddTargets(character, item); } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/IndoorsSteeringManager.cs index 72c150ed6..5e05bc20b 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/IndoorsSteeringManager.cs @@ -142,6 +142,9 @@ namespace Barotrauma IsPathDirty = false; } + public Func startNodeFilter; + public Func endNodeFilter; + protected override Vector2 DoSteeringSeek(Vector2 target, float weight) { bool needsNewPath = currentPath != null && currentPath.Unreachable || Vector2.DistanceSquared(target, currentTarget) > 1; @@ -164,7 +167,7 @@ namespace Barotrauma } } - var newPath = pathFinder.FindPath(pos, target, character.Submarine, "(Character: " + character.Name + ")"); + var newPath = pathFinder.FindPath(pos, target, character.Submarine, "(Character: " + character.Name + ")", startNodeFilter, endNodeFilter); bool useNewPath = currentPath == null || needsNewPath; if (!useNewPath && currentPath != null && currentPath.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable) { diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveGoTo.cs index 0ec20a4c9..34e1a4048 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -14,7 +14,9 @@ namespace Barotrauma private float waitUntilPathUnreachable; private bool getDivingGearIfNeeded; - public Func customCondition; + public Func requiredCondition; + public Func startNodeFilter; + public Func endNodeFilter; public bool followControlledCharacter; public bool mimic; @@ -137,7 +139,12 @@ namespace Barotrauma currTargetSimPos -= diff; } } - character.AIController.SteeringManager.SteeringSeek(currTargetSimPos); + if (PathSteering != null) + { + PathSteering.startNodeFilter = startNodeFilter; + PathSteering.endNodeFilter = endNodeFilter; + } + SteeringManager.SteeringSeek(currTargetSimPos); if (SteeringManager != PathSteering) { SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 1, heading: VectorExtensions.Forward(character.AnimController.Collider.Rotation)); @@ -191,7 +198,7 @@ namespace Barotrauma } else if (closeEnough) { - if (customCondition == null || customCondition()) + if (requiredCondition == null || requiredCondition()) { if (Target is Item item) { @@ -218,7 +225,7 @@ namespace Barotrauma private void CalculateCloseEnough() { - float interactionDistance = Target is Item i ? i.InteractDistance * 0.9f : 0; + float interactionDistance = Target is Item i ? i.InteractDistance + Math.Max(i.Rect.Width, i.Rect.Height) / 2 : 0; CloseEnough = Math.Max(interactionDistance, CloseEnough); } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRepairItem.cs index c8d162c3b..94f81003f 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -149,7 +149,14 @@ namespace Barotrauma character?.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f); } } - repairable.CurrentFixer = abandon && repairable.CurrentFixer == character ? null : character; + if (abandon) + { + repairable.StopRepairing(character); + } + else + { + repairable.StartRepairing(character, Repairable.FixActions.Repair); + } break; } } @@ -161,7 +168,11 @@ namespace Barotrauma constructor: () => { previousCondition = -1; - var objective = new AIObjectiveGoTo(Item, character, objectiveManager); + var objective = new AIObjectiveGoTo(Item, character, objectiveManager) + { + // Don't stop in ladders, because we can't interact with other items while holding the ladders. + endNodeFilter = node => node.Waypoint.Ladders == null + }; if (repairTool != null) { objective.CloseEnough = repairTool.Range * 0.75f; diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRepairItems.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRepairItems.cs index cfbda6189..751b1271a 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRepairItems.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRepairItems.cs @@ -43,7 +43,7 @@ namespace Barotrauma if (Character.CharacterList.Any(c => c.CurrentHull == item.CurrentHull && !HumanAIController.IsFriendly(c))) { return false; } if (!Objectives.ContainsKey(item)) { - if (item.Repairables.All(r => item.Condition > r.ShowRepairUIThreshold)) { return false; } + if (item.Repairables.All(r => item.ConditionPercentage > r.ShowRepairUIThreshold)) { return false; } } if (RequireAdequateSkills) { diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/PathFinder.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/PathFinder.cs index 9f589cf8f..a23b44fbb 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/PathFinder.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/PathFinder.cs @@ -157,12 +157,13 @@ namespace Barotrauma } } - public SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub = null, string errorMsgStr = null) + public SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub = null, string errorMsgStr = null, Func startNodeFilter = null, Func endNodeFilter = null) { float closestDist = 0.0f; PathNode startNode = null; foreach (PathNode node in nodes) { + if (startNodeFilter != null && !startNodeFilter(node)) { continue; } Vector2 nodePos = node.Position; if (hostSub != null) { @@ -219,6 +220,7 @@ namespace Barotrauma PathNode endNode = null; foreach (PathNode node in nodes) { + if (endNodeFilter != null && !endNodeFilter(node)) { continue; } Vector2 nodePos = node.Position; if (hostSub != null) { diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Animation/Params/Animation/AnimationParams.cs b/Barotrauma/BarotraumaShared/Source/Characters/Animation/Params/Animation/AnimationParams.cs index 53edbdc7d..8f5506eae 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Animation/Params/Animation/AnimationParams.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Animation/Params/Animation/AnimationParams.cs @@ -96,8 +96,8 @@ namespace Barotrauma public virtual AnimationType AnimationType { get; protected set; } public static string GetDefaultFileName(string speciesName, AnimationType animType) => $"{speciesName.CapitaliseFirstInvariant()}{animType.ToString()}"; - public static string GetDefaultFile(string speciesName, AnimationType animType, ContentPackage contentPackage = null) => - $"{GetFolder(speciesName, contentPackage)}{GetDefaultFileName(speciesName, animType)}.xml"; + public static string GetDefaultFile(string speciesName, AnimationType animType, ContentPackage contentPackage = null) + => Path.Combine(GetFolder(speciesName, contentPackage), $"{GetDefaultFileName(speciesName, animType)}.xml"); public static string GetFolder(string speciesName, ContentPackage contentPackage = null) { diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Animation/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/Source/Characters/Animation/Params/Ragdoll/RagdollParams.cs index c7eb7cf4a..06ab8d665 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Animation/Params/Ragdoll/RagdollParams.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Animation/Params/Ragdoll/RagdollParams.cs @@ -66,7 +66,8 @@ namespace Barotrauma .Concat(Joints.Select(j => j as RagdollSubParams))); public static string GetDefaultFileName(string speciesName) => $"{speciesName.CapitaliseFirstInvariant()}DefaultRagdoll"; - public static string GetDefaultFile(string speciesName, ContentPackage contentPackage = null) => $"{GetFolder(speciesName, contentPackage)}{GetDefaultFileName(speciesName)}.xml"; + public static string GetDefaultFile(string speciesName, ContentPackage contentPackage = null) + => Path.Combine(GetFolder(speciesName, contentPackage), $"{GetDefaultFileName(speciesName)}.xml"); private static readonly object[] dummyParams = new object[] { diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/Source/Characters/Animation/Ragdoll.cs index a87f4c7b9..8488c6b14 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Animation/Ragdoll.cs @@ -1324,7 +1324,7 @@ namespace Barotrauma private void CheckBodyInRest(float deltaTime) { - if (Collider.LinearVelocity.LengthSquared() > 0.01f || character.SelectedBy != null || !character.IsDead) + if (InWater || Collider.LinearVelocity.LengthSquared() > 0.01f || character.SelectedBy != null || !character.IsDead) { bodyInRestTimer = 0.0f; foreach (Limb limb in Limbs) @@ -1383,10 +1383,12 @@ namespace Barotrauma private bool CheckValidity(PhysicsBody body) { string errorMsg = null; - string bodyName = body.UserData is Limb ? "Limb" : "Collider"; + string bodyName = body.UserData is Limb limb ? + "Limb (" + limb.type + ")" : + "Collider"; if (!MathUtils.IsValid(body.SimPosition) || Math.Abs(body.SimPosition.X) > 1e10f || Math.Abs(body.SimPosition.Y) > 1e10f) { - errorMsg = bodyName+ " position invalid (" + body.SimPosition + ", character: " + character.Name + "), resetting the ragdoll."; + errorMsg = bodyName + " position invalid (" + body.SimPosition + ", character: " + character.Name + "), resetting the ragdoll."; } else if (!MathUtils.IsValid(body.LinearVelocity) || Math.Abs(body.LinearVelocity.X) > 1000f || Math.Abs(body.LinearVelocity.Y) > 1000f) { @@ -1426,10 +1428,10 @@ namespace Barotrauma { Collider.SetTransform(Vector2.Zero, 0.0f); } - foreach (Limb limb in Limbs) + foreach (Limb otherLimb in Limbs) { - limb.body.SetTransform(Collider.SimPosition, 0.0f); - limb.body.ResetDynamics(); + otherLimb.body.SetTransform(Collider.SimPosition, 0.0f); + otherLimb.body.ResetDynamics(); } SetInitialLimbPositions(); return false; diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Character.cs b/Barotrauma/BarotraumaShared/Source/Characters/Character.cs index 1ee5c62e8..37a406016 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Character.cs @@ -104,6 +104,9 @@ namespace Barotrauma public readonly bool IsHumanoid; + public bool IsTraitor; + public string TraitorCurrentObjective = ""; + //the name of the species (e.q. human) public readonly string SpeciesName; @@ -1515,12 +1518,12 @@ namespace Barotrauma bool leftHand = Inventory.IsInLimbSlot(item, InvSlotType.LeftHand); bool selected = false; - if (rightHand && SelectedItems[0] == null) + if (rightHand && (SelectedItems[0] == null || SelectedItems[0] == item)) { selectedItems[0] = item; selected = true; } - if (leftHand && SelectedItems[1] == null) + if (leftHand && (SelectedItems[1] == null || SelectedItems[1] == item)) { selectedItems[1] = item; selected = true; @@ -1839,7 +1842,7 @@ namespace Barotrauma { DeselectCharacter(); } - else if (focusedCharacter != null && IsKeyHit(InputType.Grab) && FocusedCharacter.CanInventoryBeAccessed) + else if (focusedCharacter != null && IsKeyHit(InputType.Grab) && FocusedCharacter.CanBeDragged) { SelectCharacter(focusedCharacter); } @@ -1954,6 +1957,10 @@ namespace Barotrauma //disable AI characters that are far away from the sub and the controlled character float distSqr = Vector2.DistanceSquared(Submarine.MainSub.WorldPosition, c.WorldPosition); if (Controlled != null) + { + distSqr = Math.Min(distSqr, Vector2.DistanceSquared(Controlled.WorldPosition, c.WorldPosition)); + } + else { distSqr = Math.Min(distSqr, Vector2.DistanceSquared(GameMain.GameScreen.Cam.GetPosition(), c.WorldPosition)); } @@ -2198,16 +2205,15 @@ namespace Barotrauma private void UpdateSightRange() { if (aiTarget == null) { return; } - // TODO: the formula might need some tweaking - float range = (float)Math.Sqrt(Mass) * 1000.0f + AnimController.Collider.LinearVelocity.Length() * 500.0f; - aiTarget.SightRange = MathHelper.Clamp(range, 0, 15000.0f); + float range = (float)Math.Sqrt(Mass) * 250 + AnimController.Collider.LinearVelocity.Length() * 500; + aiTarget.SightRange = MathHelper.Clamp(range, 0, 10000); } private void UpdateSoundRange() { if (aiTarget == null) { return; } - float range = Mass / 5 * AnimController.TargetMovement.Length() * Noise; - aiTarget.SoundRange = MathHelper.Clamp(range, 0f, 5000f); + float range = ((float)Math.Sqrt(Mass) / 3) * (AnimController.TargetMovement.Length() * 2) * Noise; + aiTarget.SoundRange = MathHelper.Clamp(range, 0, 10000); } public void SetOrder(Order order, string orderOption, Character orderGiver, bool speak = true) @@ -2598,6 +2604,7 @@ namespace Barotrauma { if (selectedItems[i] != null) selectedItems[i].Drop(this); } + SelectedConstruction = null; AnimController.ResetPullJoints(); diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/Source/Characters/Health/Afflictions/Affliction.cs index d9cc9f827..fc826ad7f 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Health/Afflictions/Affliction.cs @@ -21,7 +21,7 @@ namespace Barotrauma /// /// Probability for the affliction to be applied. Used by attacks. /// - public float ApplyProbability; + public float ApplyProbability = 1.0f; /// /// Which character gave this affliction diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Health/Afflictions/AfflictionSpaceHerpes.cs b/Barotrauma/BarotraumaShared/Source/Characters/Health/Afflictions/AfflictionSpaceHerpes.cs index 8e1f3dd45..5ddc5d0a6 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Health/Afflictions/AfflictionSpaceHerpes.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Health/Afflictions/AfflictionSpaceHerpes.cs @@ -9,6 +9,10 @@ namespace Barotrauma { private float invertControlsCooldown = 60.0f; private float stunCoolDown = 60.0f; + private float invertControlsTimer; + + private float invertControlsToggleTimer; + public AfflictionSpaceHerpes(AfflictionPrefab prefab, float strength) : base(prefab, strength) { } @@ -25,10 +29,29 @@ namespace Barotrauma //invert controls every 126-234 seconds when strength is close to 0 //every 56-104 seconds when strength is close to 100 invertControlsCooldown = (180.0f - Strength) * Rand.Range(0.7f, 1.3f); - var invertControlsAffliction = AfflictionPrefab.List.Find(ap => ap.Identifier == "invertcontrols"); - float invertControlsDuration = MathHelper.Lerp(10.0f, 60.0f, Strength / 100.0f) * Rand.Range(0.7f, 1.3f); - characterHealth.ApplyAffliction(null, new Affliction(invertControlsAffliction, invertControlsDuration)); + invertControlsTimer = MathHelper.Lerp(10.0f, 60.0f, Strength / 100.0f) * Rand.Range(0.7f, 1.3f); } + else if (invertControlsTimer > 0.0f) + { + //randomly toggle inverted controls on/off every 5 seconds + invertControlsToggleTimer -= deltaTime; + if (invertControlsToggleTimer <= 0.0f) + { + invertControlsToggleTimer = 5.0f; + if (Rand.Range(0.0f, 1.0f) < 0.5f) + { + characterHealth.ReduceAffliction(null, "invertcontrols", 100); + } + else + { + var invertControlsAffliction = AfflictionPrefab.List.Find(ap => ap.Identifier == "invertcontrols"); + characterHealth.ApplyAffliction(null, new Affliction(invertControlsAffliction, 5.0f)); + } + } + + invertControlsTimer -= deltaTime; + } + if (Strength > 50.0f) { diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/Source/Characters/Health/CharacterHealth.cs index 97ce677aa..59d72e9e1 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Health/CharacterHealth.cs @@ -1,9 +1,9 @@ -using Lidgren.Network; -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using Barotrauma.Networking; namespace Barotrauma { @@ -732,14 +732,14 @@ namespace Barotrauma return allAfflictions; } - public void ServerWrite(NetBuffer msg) + public void ServerWrite(IWriteMessage msg) { List activeAfflictions = afflictions.FindAll(a => a.Strength > 0.0f && a.Strength >= a.Prefab.ActivationThreshold); msg.Write((byte)activeAfflictions.Count); foreach (Affliction affliction in activeAfflictions) { - msg.WriteRangedInteger(0, AfflictionPrefab.List.Count - 1, AfflictionPrefab.List.IndexOf(affliction.Prefab)); + msg.WriteRangedIntegerDeprecated(0, AfflictionPrefab.List.Count - 1, AfflictionPrefab.List.IndexOf(affliction.Prefab)); msg.WriteRangedSingle( MathHelper.Clamp(affliction.Strength, 0.0f, affliction.Prefab.MaxStrength), 0.0f, affliction.Prefab.MaxStrength, 8); @@ -758,8 +758,8 @@ namespace Barotrauma msg.Write((byte)limbAfflictions.Count); foreach (var limbAffliction in limbAfflictions) { - msg.WriteRangedInteger(0, limbHealths.Count - 1, limbHealths.IndexOf(limbAffliction.First)); - msg.WriteRangedInteger(0, AfflictionPrefab.List.Count - 1, AfflictionPrefab.List.IndexOf(limbAffliction.Second.Prefab)); + msg.WriteRangedIntegerDeprecated(0, limbHealths.Count - 1, limbHealths.IndexOf(limbAffliction.First)); + msg.WriteRangedIntegerDeprecated(0, AfflictionPrefab.List.Count - 1, AfflictionPrefab.List.IndexOf(limbAffliction.Second.Prefab)); msg.WriteRangedSingle( MathHelper.Clamp(limbAffliction.Second.Strength, 0.0f, limbAffliction.Second.Prefab.MaxStrength), 0.0f, limbAffliction.Second.Prefab.MaxStrength, 8); diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Jobs/Job.cs b/Barotrauma/BarotraumaShared/Source/Characters/Jobs/Job.cs index ad0d59f13..deea9e2ab 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Jobs/Job.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Jobs/Job.cs @@ -144,6 +144,15 @@ namespace Barotrauma #if SERVER if (GameMain.Server != null && Entity.Spawner != null) { + if (GameMain.Server.EntityEventManager.UniqueEvents.Any(ev => ev.Entity == item)) + { + string errorMsg = $"Error while spawning job items. Item {item.Name} created network events before the spawn event had been created."; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Job.InitializeJobItem:EventsBeforeSpawning", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameMain.Server.EntityEventManager.UniqueEvents.RemoveAll(ev => ev.Entity == item); + GameMain.Server.EntityEventManager.Events.RemoveAll(ev => ev.Entity == item); + } + Entity.Spawner.CreateNetworkEvent(item, false); } #endif diff --git a/Barotrauma/BarotraumaShared/Source/ContentPackage.cs b/Barotrauma/BarotraumaShared/Source/ContentPackage.cs index ebeb9dbda..414e8d982 100644 --- a/Barotrauma/BarotraumaShared/Source/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/Source/ContentPackage.cs @@ -36,7 +36,8 @@ namespace Barotrauma Afflictions, Buffs, Tutorials, - UIStyle + UIStyle, + TraitorMissions } public class ContentPackage @@ -79,6 +80,7 @@ namespace Barotrauma ContentType.LevelGenerationParameters, ContentType.RandomEvents, ContentType.Missions, + ContentType.TraitorMissions, ContentType.BackgroundCreaturePrefabs, ContentType.RuinConfig, ContentType.NPCConversations, @@ -96,7 +98,7 @@ namespace Barotrauma public string Path { get; - private set; + set; } public string SteamWorkshopUrl; @@ -419,7 +421,7 @@ namespace Barotrauma switch (contentFile.Type) { case ContentType.Submarine: - return path == "Submarines"; + return path == "Submarines" || path == "Mods"; default: return path == "Mods"; } @@ -460,8 +462,9 @@ namespace Barotrauma return Files.Where(f => f.Type == type).Select(f => f.Path); } - public static void LoadAll(string folder) + public static void LoadAll() { + string folder = ContentPackage.Folder; if (!Directory.Exists(folder)) { try @@ -475,14 +478,23 @@ namespace Barotrauma } } - string[] files = Directory.GetFiles(folder, "*.xml"); - List.Clear(); + string[] files = Directory.GetFiles(folder, "*.xml"); foreach (string filePath in files) { - ContentPackage package = new ContentPackage(filePath); - List.Add(package); + List.Add(new ContentPackage(filePath)); + } + + string[] modDirectories = Directory.GetDirectories("Mods"); + foreach (string modDirectory in modDirectories) + { + if (System.IO.Path.GetFileName(modDirectory.TrimEnd(System.IO.Path.DirectorySeparatorChar)) == "ExampleMod") { continue; } + string modFilePath = System.IO.Path.Combine(modDirectory, Steam.SteamManager.MetadataFileName); + if (File.Exists(modFilePath)) + { + List.Add(new ContentPackage(modFilePath)); + } } } @@ -505,7 +517,7 @@ namespace Barotrauma public class ContentFile { - public readonly string Path; + public string Path; public ContentType Type; public Workshop.Item WorkShopItem; diff --git a/Barotrauma/BarotraumaShared/Source/CoroutineManager.cs b/Barotrauma/BarotraumaShared/Source/CoroutineManager.cs index f0297a6a4..b9af5f57a 100644 --- a/Barotrauma/BarotraumaShared/Source/CoroutineManager.cs +++ b/Barotrauma/BarotraumaShared/Source/CoroutineManager.cs @@ -142,6 +142,7 @@ namespace Barotrauma } catch (Exception e) { + handle.Exception = e; DebugConsole.ThrowError("Coroutine \"" + handle.Name + "\" has thrown an exception", e); } } @@ -182,7 +183,7 @@ namespace Barotrauma { if (handle.Thread.ThreadState.HasFlag(ThreadState.Stopped)) { - if ((CoroutineStatus)handle.Coroutine.Current == CoroutineStatus.Failure) + if (handle.Exception!=null || (CoroutineStatus)handle.Coroutine.Current == CoroutineStatus.Failure) { DebugConsole.ThrowError("Coroutine \"" + handle.Name + "\" has failed"); } diff --git a/Barotrauma/BarotraumaShared/Source/DebugConsole.cs b/Barotrauma/BarotraumaShared/Source/DebugConsole.cs index 71be5c15c..aa41e2699 100644 --- a/Barotrauma/BarotraumaShared/Source/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/Source/DebugConsole.cs @@ -438,7 +438,7 @@ namespace Barotrauma }); })); - commands.Add(new Command("banip", "banip [ip]: Ban the IP address from the server.", null)); + commands.Add(new Command("banendpoint|banip", "banendpoint [endpoint]: Ban the IP address/SteamID from the server.", null)); commands.Add(new Command("teleportcharacter|teleport", "teleport [character name]: Teleport the specified character to the position of the cursor. If the name parameter is omitted, the controlled character will be teleported.", (string[] args) => { @@ -674,6 +674,11 @@ namespace Barotrauma },null)); #if DEBUG + commands.Add(new Command("crash", "crash: Crashes the game.", (string[] args) => + { + throw new Exception("crash command issued"); + })); + commands.Add(new Command("teleportsub", "teleportsub [start/end]: Teleport the submarine to the start or end of the level. WARNING: does not take outposts into account, so often leads to physics glitches. Only use for debugging.", (string[] args) => { if (Submarine.MainSub == null || Level.Loaded == null) return; @@ -960,6 +965,7 @@ namespace Barotrauma })); #if DEBUG + /*TODO: reimplement commands.Add(new Command("simulatedlatency", "simulatedlatency [minimumlatencyseconds] [randomlatencyseconds]: applies a simulated latency to network messages. Useful for simulating real network conditions when testing the multiplayer locally.", (string[] args) => { if (args.Count() < 2 || (GameMain.NetworkMember == null)) return; @@ -1029,7 +1035,7 @@ namespace Barotrauma } #endif NewMessage("Set packet duplication to " + (int)(duplicates * 100) + "%.", Color.White); - })); + }));*/ #endif //"dummy commands" that only exist so that the server can give clients permissions to use them @@ -1502,8 +1508,9 @@ namespace Barotrauma public static void NewMessage(string msg, Color color, bool isCommand = false) { if (string.IsNullOrEmpty((msg))) return; - + var newMsg = new ColoredText(msg, color, isCommand); + lock (queuedMessages) { queuedMessages.Enqueue(new ColoredText(msg, color, isCommand)); diff --git a/Barotrauma/BarotraumaShared/Source/Events/EventManager.cs b/Barotrauma/BarotraumaShared/Source/Events/EventManager.cs index 23317eb03..cb5365874 100644 --- a/Barotrauma/BarotraumaShared/Source/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/Source/Events/EventManager.cs @@ -36,6 +36,8 @@ namespace Barotrauma private List selectedEventSets; private EventManagerSettings settings; + + private readonly bool isClient; public float CurrentIntensity { @@ -51,13 +53,15 @@ namespace Barotrauma { events = new List(); selectedEventSets = new List(); + + isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; } public bool Enabled = true; public void StartRound(Level level) { - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) return; + if (isClient) { return; } var suitableSettings = EventManagerSettings.List.FindAll(s => level.Difficulty >= s.MinLevelDifficulty && @@ -193,13 +197,13 @@ namespace Barotrauma public void Update(float deltaTime) { - if (!Enabled) return; + if (!Enabled) { return; } //clients only calculate the intensity but don't create any events //(the intensity is used for controlling the background music) CalculateCurrentIntensity(deltaTime); - - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) return; + + if (isClient) { return; } roundDuration += deltaTime; diff --git a/Barotrauma/BarotraumaShared/Source/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/Source/Events/Missions/Mission.cs index 7df6131cd..a518a9bc0 100644 --- a/Barotrauma/BarotraumaShared/Source/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/Source/Events/Missions/Mission.cs @@ -32,7 +32,7 @@ namespace Barotrauma private set { failureMessage = value; } } - private string description; + protected string description; public virtual string Description { get { return description; } @@ -155,6 +155,13 @@ namespace Barotrauma return false; } + protected void ShowMessage(int index) + { + ShowMessageProjSpecific(index); + } + + partial void ShowMessageProjSpecific(int index); + /// /// End the mission and give a reward if it was completed successfully /// diff --git a/Barotrauma/BarotraumaShared/Source/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/Source/Events/Missions/MissionPrefab.cs index 49827886d..486e84442 100644 --- a/Barotrauma/BarotraumaShared/Source/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/Source/Events/Missions/MissionPrefab.cs @@ -5,7 +5,7 @@ using System.Xml.Linq; namespace Barotrauma { - enum MissionType + public enum MissionType { Random, None, diff --git a/Barotrauma/BarotraumaShared/Source/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaShared/Source/Events/Missions/MonsterMission.cs index e37aeed7d..66a92c012 100644 --- a/Barotrauma/BarotraumaShared/Source/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaShared/Source/Events/Missions/MonsterMission.cs @@ -28,6 +28,9 @@ namespace Barotrauma { monsterFile = prefab.ConfigElement.GetAttributeString("monsterfile", ""); monsterCount = prefab.ConfigElement.GetAttributeInt("monstercount", 1); + + description = description.Replace("[monster]", + TextManager.Get("character." + System.IO.Path.GetFileNameWithoutExtension(monsterFile))); } public override void Start(Level level) @@ -66,9 +69,9 @@ namespace Barotrauma if (activeMonsters.Any()) { return; } -#if CLIENT + ShowMessage(state); -#endif + state = 1; break; } diff --git a/Barotrauma/BarotraumaShared/Source/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/Source/Events/Missions/SalvageMission.cs index 7d5d16917..f0728eb71 100644 --- a/Barotrauma/BarotraumaShared/Source/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/Source/Events/Missions/SalvageMission.cs @@ -94,16 +94,15 @@ namespace Barotrauma if (item.ParentInventory != null) item.body.FarseerBody.IsKinematic = false; if (item.CurrentHull?.Submarine == null) return; -#if CLIENT ShowMessage(state); -#endif + state = 1; break; case 1: if (!Submarine.MainSub.AtEndPosition && !Submarine.MainSub.AtStartPosition) return; -#if CLIENT + ShowMessage(state); -#endif + state = 2; break; } diff --git a/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/CampaignMode.cs index 514256b4d..da06dc538 100644 --- a/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/CampaignMode.cs @@ -6,7 +6,7 @@ using System.Xml.Linq; namespace Barotrauma { - abstract class CampaignMode : GameMode + abstract partial class CampaignMode : GameMode { public readonly CargoManager CargoManager; @@ -111,9 +111,8 @@ namespace Barotrauma base.Update(deltaTime); if (!IsRunning) { return; } -#if CLIENT - if (GameMain.Client != null) { return; } -#endif + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + if (!watchmenSpawned) { if (Level.Loaded.StartOutpost != null) { startWatchman = SpawnWatchman(Level.Loaded.StartOutpost); } @@ -128,7 +127,7 @@ namespace Barotrauma foreach (Character character in Character.CharacterList) { #if SERVER - if (string.IsNullOrEmpty(character.OwnerClientIP)) { continue; } + if (string.IsNullOrEmpty(character.OwnerClientEndPoint)) { continue; } #else if (!CrewManager.GetCharacters().Contains(character)) { continue; } #endif diff --git a/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/CharacterCampaignData.cs b/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/CharacterCampaignData.cs index a3eebbc8f..ae0e46e56 100644 --- a/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/CharacterCampaignData.cs +++ b/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/CharacterCampaignData.cs @@ -13,7 +13,7 @@ namespace Barotrauma public readonly string Name; - public string ClientIP + public string ClientEndPoint { get; private set; @@ -42,7 +42,7 @@ namespace Barotrauma public CharacterCampaignData(XElement element) { Name = element.GetAttributeString("name", "Unnamed"); - ClientIP = element.GetAttributeString("ip", ""); + ClientEndPoint = element.GetAttributeString("endpoint", null) ?? element.GetAttributeString("ip", ""); string steamID = element.GetAttributeString("steamid", ""); if (!string.IsNullOrEmpty(steamID)) { @@ -69,7 +69,7 @@ namespace Barotrauma { XElement element = new XElement("CharacterCampaignData", new XAttribute("name", Name), - new XAttribute("ip", ClientIP), + new XAttribute("endpoint", ClientEndPoint), new XAttribute("steamid", SteamID)); CharacterInfo?.Save(element); diff --git a/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/GameMode.cs b/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/GameMode.cs index 0b3515ced..78936c9f8 100644 --- a/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/GameMode.cs +++ b/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/GameMode.cs @@ -64,7 +64,7 @@ namespace Barotrauma isRunning = true; } - public virtual void MsgBox() { } + public virtual void ShowStartMessage() { } public virtual void AddToGUIUpdateList() { diff --git a/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/MultiPlayerCampaign.cs index 72ce67d80..9c7f8c86d 100644 --- a/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/MultiPlayerCampaign.cs @@ -3,7 +3,6 @@ using Microsoft.Xna.Framework; using System; using System.Linq; using System.Xml.Linq; -using Lidgren.Network; using System.Collections.Generic; using System.IO; @@ -98,14 +97,11 @@ namespace Barotrauma GameMain.GameSession.EndRound(""); + //client character has spawned this round -> remove old data (and replace with an up-to-date one if the client still has an alive character) + characterData.RemoveAll(cd => cd.HasSpawned); + foreach (Client c in GameMain.Server.ConnectedClients) { - if (c.HasSpawned) - { - //client has spawned this round -> remove old data (and replace with new one if the client still has an alive character) - characterData.RemoveAll(cd => cd.MatchesClient(c)); - } - if (c.Character?.Info != null && !c.Character.IsDead) { characterData.Add(new CharacterCampaignData(c)); diff --git a/Barotrauma/BarotraumaShared/Source/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/Source/GameSession/GameSession.cs index 30bf32cab..2a478e78f 100644 --- a/Barotrauma/BarotraumaShared/Source/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/Source/GameSession/GameSession.cs @@ -260,7 +260,7 @@ namespace Barotrauma if (GameMode != null) { - GameMode.MsgBox(); + GameMode.ShowStartMessage(); if (GameMode is MultiPlayerCampaign mpCampaign && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { diff --git a/Barotrauma/BarotraumaShared/Source/GameSettings.cs b/Barotrauma/BarotraumaShared/Source/GameSettings.cs index 8783df978..b036c1ed7 100644 --- a/Barotrauma/BarotraumaShared/Source/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/Source/GameSettings.cs @@ -46,6 +46,8 @@ namespace Barotrauma public bool PauseOnFocusLost { get; set; } public bool MuteOnFocusLost { get; set; } + public bool DynamicRangeCompressionEnabled { get; set; } + public bool VoipAttenuationEnabled { get; set; } public bool UseDirectionalVoiceChat { get; set; } public enum VoiceMode @@ -176,9 +178,9 @@ namespace Barotrauma #if CLIENT if (GameMain.SoundManager != null) { - GameMain.SoundManager.SetCategoryGainMultiplier("default", soundVolume); - GameMain.SoundManager.SetCategoryGainMultiplier("ui", soundVolume); - GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", soundVolume); + GameMain.SoundManager.SetCategoryGainMultiplier("default", soundVolume, 0); + GameMain.SoundManager.SetCategoryGainMultiplier("ui", soundVolume, 0); + GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", soundVolume, 0); } #endif } @@ -191,7 +193,7 @@ namespace Barotrauma { musicVolume = MathHelper.Clamp(value, 0.0f, 1.0f); #if CLIENT - GameMain.SoundManager?.SetCategoryGainMultiplier("music", musicVolume); + GameMain.SoundManager?.SetCategoryGainMultiplier("music", musicVolume, 0); #endif } } @@ -203,7 +205,7 @@ namespace Barotrauma { voiceChatVolume = MathHelper.Clamp(value, 0.0f, 1.0f); #if CLIENT - GameMain.SoundManager?.SetCategoryGainMultiplier("voip", voiceChatVolume * 20.0f); + GameMain.SoundManager?.SetCategoryGainMultiplier("voip", voiceChatVolume * 20.0f, 0); #endif } } @@ -286,7 +288,7 @@ namespace Barotrauma public GameSettings() { - ContentPackage.LoadAll(ContentPackage.Folder); + ContentPackage.LoadAll(); CompletedTutorialNames = new List(); LoadDefaultConfig(); @@ -443,6 +445,11 @@ namespace Barotrauma LoadAudioSettings(doc); LoadControls(doc); LoadContentPackages(doc); + +#if DEBUG + WindowMode = WindowMode.Windowed; +#endif + UnsavedSettings = false; } @@ -797,6 +804,8 @@ namespace Barotrauma new XAttribute("voicechatvolume", voiceChatVolume), new XAttribute("microphonevolume", microphoneVolume), new XAttribute("muteonfocuslost", MuteOnFocusLost), + new XAttribute("dynamicrangecompressionenabled", DynamicRangeCompressionEnabled), + new XAttribute("voipattenuationenabled", VoipAttenuationEnabled), new XAttribute("usedirectionalvoicechat", UseDirectionalVoiceChat), new XAttribute("voicesetting", VoiceSetting), new XAttribute("voicecapturedevice", VoiceCaptureDevice ?? ""), @@ -910,7 +919,7 @@ namespace Barotrauma } AutoCheckUpdates = doc.Root.GetAttributeBool("autocheckupdates", AutoCheckUpdates); sendUserStatistics = doc.Root.GetAttributeBool("senduserstatistics", sendUserStatistics); - QuickStartSubmarineName = doc.Root.GetAttributeString("quickstartsubmarine", ""); + QuickStartSubmarineName = doc.Root.GetAttributeString("quickstartsub", QuickStartSubmarineName); useSteamMatchmaking = doc.Root.GetAttributeBool("usesteammatchmaking", useSteamMatchmaking); requireSteamAuthentication = doc.Root.GetAttributeBool("requiresteamauthentication", requireSteamAuthentication); EnableSplashScreen = doc.Root.GetAttributeBool("enablesplashscreen", EnableSplashScreen); @@ -997,8 +1006,11 @@ namespace Barotrauma { SoundVolume = audioSettings.GetAttributeFloat("soundvolume", SoundVolume); MusicVolume = audioSettings.GetAttributeFloat("musicvolume", MusicVolume); + DynamicRangeCompressionEnabled = audioSettings.GetAttributeBool("dynamicrangecompressionenabled", DynamicRangeCompressionEnabled); + VoipAttenuationEnabled = audioSettings.GetAttributeBool("voipattenuationenabled", VoipAttenuationEnabled); VoiceChatVolume = audioSettings.GetAttributeFloat("voicechatvolume", VoiceChatVolume); MuteOnFocusLost = audioSettings.GetAttributeBool("muteonfocuslost", MuteOnFocusLost); + UseDirectionalVoiceChat = audioSettings.GetAttributeBool("usedirectionalvoicechat", UseDirectionalVoiceChat); VoiceCaptureDevice = audioSettings.GetAttributeString("voicecapturedevice", VoiceCaptureDevice); NoiseGateThreshold = audioSettings.GetAttributeFloat("noisegatethreshold", NoiseGateThreshold); @@ -1108,6 +1120,8 @@ namespace Barotrauma ChatOpen = true; soundVolume = 0.5f; musicVolume = 0.3f; + DynamicRangeCompressionEnabled = true; + VoipAttenuationEnabled = true; voiceChatVolume = 0.5f; microphoneVolume = 1.0f; AutoCheckUpdates = true; diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/DockingPort.cs index fcfdc692e..77eb57a86 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/DockingPort.cs @@ -82,7 +82,7 @@ namespace Barotrauma.Items.Components } } } - + public DockingPort(Item item, XElement element) : base(item, element) { @@ -160,7 +160,7 @@ namespace Barotrauma.Items.Components if (DockingTarget != null) { Undock(); - } + } if (target.item.Submarine == item.Submarine) { @@ -168,7 +168,7 @@ namespace Barotrauma.Items.Components DockingTarget = null; return; } - + target.InitializeLinks(); if (!item.linkedTo.Contains(target.item)) item.linkedTo.Add(target.item); @@ -193,7 +193,7 @@ namespace Barotrauma.Items.Components Math.Sign(DockingTarget.item.WorldPosition.X - item.WorldPosition.X) : Math.Sign(DockingTarget.item.WorldPosition.Y - item.WorldPosition.Y); DockingTarget.DockingDir = -DockingDir; - + if (door != null && DockingTarget.door != null) { WayPoint myWayPoint = WayPoint.WayPointList.Find(wp => door.LinkedGap == wp.ConnectedGap); @@ -205,7 +205,7 @@ namespace Barotrauma.Items.Components targetWayPoint.linkedTo.Add(myWayPoint); } } - + CreateJoint(false); #if SERVER @@ -246,7 +246,7 @@ namespace Barotrauma.Items.Components else if (DockingTarget.item.Submarine.PhysicsBody.Mass < item.Submarine.PhysicsBody.Mass || item.Submarine.IsOutpost) { - DockingTarget.item.Submarine.SubBody.SetPosition(item.Submarine.SubBody.Position - ConvertUnits.ToDisplayUnits(jointDiff)); + DockingTarget.item.Submarine.SubBody.SetPosition(DockingTarget.item.Submarine.SubBody.Position - ConvertUnits.ToDisplayUnits(jointDiff)); } ConnectWireBetweenPorts(); @@ -401,7 +401,7 @@ namespace Barotrauma.Items.Components } private void CreateHulls() - { + { var hullRects = new Rectangle[] { item.WorldRect, DockingTarget.item.WorldRect }; var subs = new Submarine[] { item.Submarine, DockingTarget.item.Submarine }; @@ -416,18 +416,18 @@ namespace Barotrauma.Items.Components { DockingTarget.CreateDoorBody(); } - + if (IsHorizontal) { if (hullRects[0].Center.X > hullRects[1].Center.X) { hullRects = new Rectangle[] { DockingTarget.item.WorldRect, item.WorldRect }; - subs = new Submarine[] { DockingTarget.item.Submarine,item.Submarine }; + subs = new Submarine[] { DockingTarget.item.Submarine, item.Submarine }; } hullRects[0] = new Rectangle(hullRects[0].Center.X, hullRects[0].Y, ((int)DockedDistance / 2), hullRects[0].Height); hullRects[1] = new Rectangle(hullRects[1].Center.X - ((int)DockedDistance / 2), hullRects[1].Y, ((int)DockedDistance / 2), hullRects[1].Height); - + //expand hulls if needed, so there's no empty space between the sub's hulls and docking port hulls int leftSubRightSide = int.MinValue, rightSubLeftSide = int.MaxValue; foreach (Hull hull in Hull.hullList) @@ -478,7 +478,7 @@ namespace Barotrauma.Items.Components hullRects[1].Width += rightHullDiff; } } - + for (int i = 0; i < 2; i++) { hullRects[i].Location -= MathUtils.ToPoint((subs[i].WorldPosition - subs[i].HiddenSubPosition)); @@ -503,7 +503,7 @@ namespace Barotrauma.Items.Components hullRects = new Rectangle[] { DockingTarget.item.WorldRect, item.WorldRect }; subs = new Submarine[] { DockingTarget.item.Submarine, item.Submarine }; } - + hullRects[0] = new Rectangle(hullRects[0].X, hullRects[0].Y + (int)(-hullRects[0].Height + DockedDistance) / 2, hullRects[0].Width, ((int)DockedDistance / 2)); hullRects[1] = new Rectangle(hullRects[1].X, hullRects[1].Y - hullRects[1].Height / 2, hullRects[1].Width, ((int)DockedDistance / 2)); @@ -954,7 +954,7 @@ namespace Barotrauma.Items.Components #endif } - public void ServerWrite(Lidgren.Network.NetBuffer msg, Client c, object[] extraData = null) + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { msg.Write(docked); @@ -965,7 +965,7 @@ namespace Barotrauma.Items.Components } } - public void ClientRead(ServerNetObject type, Lidgren.Network.NetBuffer msg, float sendingTime) + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { bool isDocked = msg.ReadBoolean(); diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Door.cs index 7a34357e8..82c04b75a 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Door.cs @@ -313,8 +313,7 @@ namespace Barotrauma.Items.Components PredictedState = null; } } - - LinkedGap.Open = openState; + LinkedGap.Open = isBroken ? 1.0f : openState; } if (isClosing) @@ -371,7 +370,7 @@ namespace Barotrauma.Items.Components { LinkedGap.AutoOrient(); } - LinkedGap.Open = openState; + LinkedGap.Open = isBroken ? 1.0f : openState; LinkedGap.PassAmbientLight = Window != Rectangle.Empty; } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/Holdable.cs index c896f1e39..a893e97ea 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/Holdable.cs @@ -1,6 +1,5 @@ using Barotrauma.Networking; using FarseerPhysics; -using Lidgren.Network; using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Xml.Linq; @@ -286,14 +285,21 @@ namespace Barotrauma.Items.Components item.SetTransform(rightHand.SimPosition, 0.0f); } - bool alreadySelected = character.HasEquippedItem(item); - if (picker.TrySelectItem(item) || picker.HasEquippedItem(item)) + bool alreadyEquipped = character.HasEquippedItem(item); + bool canSelect = picker.TrySelectItem(item); + + if (canSelect || picker.HasEquippedItem(item)) { + if (!canSelect) + { + character.DeselectItem(item); + } + item.body.Enabled = true; IsActive = true; #if SERVER - if (!alreadySelected) GameServer.Log(character.LogName + " equipped " + item.Name, ServerLog.MessageType.ItemInteraction); + if (!alreadyEquipped) GameServer.Log(character.LogName + " equipped " + item.Name, ServerLog.MessageType.ItemInteraction); #endif } } @@ -557,7 +563,7 @@ namespace Barotrauma.Items.Components DeattachFromWall(); } } - + public override XElement Save(XElement parentElement) { if (!attachable) @@ -581,8 +587,8 @@ namespace Barotrauma.Items.Components return saveElement; } - - public override void ServerWrite(NetBuffer msg, Client c, object[] extraData = null) + + public override void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { base.ServerWrite(msg, c, extraData); if (!attachable || body == null) { return; } @@ -592,11 +598,11 @@ namespace Barotrauma.Items.Components msg.Write(body.SimPosition.Y); } - public override void ClientRead(ServerNetObject type, NetBuffer msg, float sendingTime) + public override void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { base.ClientRead(type, msg, sendingTime); bool shouldBeAttached = msg.ReadBoolean(); - Vector2 simPosition = new Vector2(msg.ReadFloat(), msg.ReadFloat()); + Vector2 simPosition = new Vector2(msg.ReadSingle(), msg.ReadSingle()); if (!attachable) { diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/LevelResource.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/LevelResource.cs index d557dd19d..547ebde12 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/LevelResource.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/LevelResource.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Linq; @@ -116,7 +115,7 @@ namespace Barotrauma.Items.Components } } - public void ServerWrite(NetBuffer msg, Client c, object[] extraData = null) + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { msg.Write(deattachTimer); } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/MeleeWeapon.cs index 1b7e9af30..631a520f2 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/MeleeWeapon.cs @@ -56,11 +56,9 @@ namespace Barotrauma.Items.Components public MeleeWeapon(Item item, XElement element) : base(item, element) { - //throwForce = ToolBox.GetAttributeFloat(element, "throwforce", 1.0f); - foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "attack") continue; + if (subElement.Name.ToString().ToLowerInvariant() != "attack") { continue; } attack = new Attack(subElement, item.Name + ", MeleeWeapon"); } item.IsShootable = true; @@ -70,23 +68,22 @@ namespace Barotrauma.Items.Components public override bool Use(float deltaTime, Character character = null) { - if (character == null || reloadTimer > 0.0f) return false; - if (Item.RequireAimToUse && !character.IsKeyDown(InputType.Aim) || hitting) return false; + if (character == null || reloadTimer > 0.0f) { return false; } + if (Item.RequireAimToUse && !character.IsKeyDown(InputType.Aim) || hitting) { return false; } //don't allow hitting if the character is already hitting with another weapon for (int i = 0; i < 2; i++ ) { - if (character.SelectedItems[i] == null || character.SelectedItems[i] == Item) continue; + if (character.SelectedItems[i] == null || character.SelectedItems[i] == Item) { continue; } var otherWeapon = character.SelectedItems[i].GetComponent(); - if (otherWeapon == null) continue; - - if (otherWeapon.hitting) return false; + if (otherWeapon == null) { continue; } + if (otherWeapon.hitting) { return false; } } SetUser(character); - if (hitPos < MathHelper.PiOver4) return false; + if (hitPos < MathHelper.PiOver4) { return false; } reloadTimer = reload; @@ -94,21 +91,20 @@ namespace Barotrauma.Items.Components item.body.FarseerBody.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall; item.body.FarseerBody.OnCollision += OnCollision; - foreach (Limb l in character.AnimController.Limbs) + if (!character.AnimController.InWater) { - //item.body.FarseerBody.IgnoreCollisionWith(l.body.FarseerBody); - - if (character.AnimController.InWater) continue; - if (l.type == LimbType.LeftFoot || l.type == LimbType.LeftThigh || l.type == LimbType.LeftLeg) continue; - - if (l.type == LimbType.Head || l.type == LimbType.Torso) + foreach (Limb l in character.AnimController.Limbs) { - l.body.ApplyLinearImpulse(new Vector2(character.AnimController.Dir * 7.0f, -4.0f)); + if (l.type == LimbType.LeftFoot || l.type == LimbType.LeftThigh || l.type == LimbType.LeftLeg) { continue; } + if (l.type == LimbType.Head || l.type == LimbType.Torso) + { + l.body.ApplyLinearImpulse(new Vector2(character.AnimController.Dir * 7.0f, -4.0f)); + } + else + { + l.body.ApplyLinearImpulse(new Vector2(character.AnimController.Dir * 5.0f, -2.0f)); + } } - else - { - l.body.ApplyLinearImpulse(new Vector2(character.AnimController.Dir * 5.0f, -2.0f)); - } } hitting = true; @@ -121,7 +117,6 @@ namespace Barotrauma.Items.Components public override void Drop(Character dropper) { base.Drop(dropper); - hitting = false; hitPos = 0.0f; } @@ -133,17 +128,17 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - if (!item.body.Enabled) return; - if (!picker.HasSelectedItem(item)) IsActive = false; + if (!item.body.Enabled) { return; } + if (!picker.HasSelectedItem(item)) { IsActive = false; } reloadTimer -= deltaTime; if (reloadTimer < 0) { reloadTimer = 0; } - if (!picker.IsKeyDown(InputType.Aim) && !hitting) hitPos = 0.0f; + if (!picker.IsKeyDown(InputType.Aim) && !hitting) { hitPos = 0.0f; } ApplyStatusEffects(ActionType.OnActive, deltaTime, picker); - if (item.body.Dir != picker.AnimController.Dir) Flip(); + if (item.body.Dir != picker.AnimController.Dir) { Flip(); } AnimController ac = picker.AnimController; @@ -236,16 +231,16 @@ namespace Barotrauma.Items.Components if (f2.Body.UserData is Limb) { targetLimb = (Limb)f2.Body.UserData; - if (targetLimb.IsSevered || targetLimb.character == null) return false; + if (targetLimb.IsSevered || targetLimb.character == null) { return false; } targetCharacter = targetLimb.character; - if (targetCharacter == picker) return false; + if (targetCharacter == picker){ return false; } if (AllowHitMultiple) { - if (hitTargets.Contains(targetCharacter)) return false; + if (hitTargets.Contains(targetCharacter)) { return false; } } else { - if (hitTargets.Any(t => t is Character)) return false; + if (hitTargets.Any(t => t is Character)) { return false; } } hitTargets.Add(targetCharacter); } @@ -256,11 +251,11 @@ namespace Barotrauma.Items.Components targetLimb = targetCharacter.AnimController.GetLimb(LimbType.Torso); //Otherwise armor can be bypassed in strange ways if (AllowHitMultiple) { - if (hitTargets.Contains(targetCharacter)) return false; + if (hitTargets.Contains(targetCharacter)) { return false; } } else { - if (hitTargets.Any(t => t is Character)) return false; + if (hitTargets.Any(t => t is Character)) { return false; } } hitTargets.Add(targetCharacter); } @@ -269,11 +264,11 @@ namespace Barotrauma.Items.Components targetStructure = (Structure)f2.Body.UserData; if (AllowHitMultiple) { - if (hitTargets.Contains(targetStructure)) return true; + if (hitTargets.Contains(targetStructure)) { return true; } } else { - if (hitTargets.Any(t => t is Structure)) return true; + if (hitTargets.Any(t => t is Structure)) { return true; } } hitTargets.Add(targetStructure); } @@ -303,8 +298,8 @@ namespace Barotrauma.Items.Components return false; } } - - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) return true; + + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return true; } #if SERVER if (GameMain.Server != null && targetCharacter != null) //TODO: Log structure hits diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/Pickable.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/Pickable.cs index 15844682f..5a770bceb 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/Pickable.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/Pickable.cs @@ -3,7 +3,6 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Xml.Linq; -using Lidgren.Network; namespace Barotrauma.Items.Components { @@ -233,12 +232,12 @@ namespace Barotrauma.Items.Components } } - public virtual void ServerWrite(NetBuffer msg, Client c, object[] extraData = null) + public virtual void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { msg.Write(activePicker == null ? (ushort)0 : activePicker.ID); } - public virtual void ClientRead(ServerNetObject type, NetBuffer msg, float sendingTime) + public virtual void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { ushort pickerID = msg.ReadUInt16(); if (pickerID == 0) diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/RepairTool.cs index 54f8c1372..50fd0fa90 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/RepairTool.cs @@ -54,6 +54,9 @@ namespace Barotrauma.Items.Components [Serialize(false, false)] public bool RepairMultiple { get; set; } + [Serialize(false, false)] + public bool RepairThroughHoles { get; set; } + [Serialize(0.0f, false)] public float FireProbability { get; set; } @@ -146,10 +149,24 @@ namespace Barotrauma.Items.Components } } - Vector2 targetPosition = item.WorldPosition; - targetPosition += new Vector2( - (float)Math.Cos(item.body.Rotation), - (float)Math.Sin(item.body.Rotation)) * Range * item.body.Dir; + Vector2 rayStart; + Vector2 sourcePos = character?.AnimController == null ? item.SimPosition : character.AnimController.AimSourceSimPos; + Vector2 barrelPos = item.SimPosition + ConvertUnits.ToSimUnits(TransformedBarrelPos); + //make sure there's no obstacles between the base of the item (or the shoulder of the character) and the end of the barrel + if (Submarine.PickBody(sourcePos, barrelPos, collisionCategory: Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking) == null) + { + //no obstacles -> we start the raycast at the end of the barrel + rayStart = ConvertUnits.ToSimUnits(item.WorldPosition + TransformedBarrelPos); + } + else + { + rayStart = ConvertUnits.ToSimUnits(item.WorldPosition); + } + + Vector2 rayEnd = rayStart + + ConvertUnits.ToSimUnits(new Vector2( + (float)Math.Cos(item.body.Rotation), + (float)Math.Sin(item.body.Rotation)) * Range * item.body.Dir); List ignoredBodies = new List(); foreach (Limb limb in character.AnimController.Limbs) @@ -161,11 +178,8 @@ namespace Barotrauma.Items.Components IsActive = true; activeTimer = 0.1f; - - Vector2 rayStart = ConvertUnits.ToSimUnits(item.WorldPosition); - Vector2 rayEnd = ConvertUnits.ToSimUnits(targetPosition); - - debugRayStartPos = item.WorldPosition; + + debugRayStartPos = ConvertUnits.ToDisplayUnits(rayStart); debugRayEndPos = ConvertUnits.ToDisplayUnits(rayEnd); if (character.Submarine == null) @@ -203,7 +217,7 @@ namespace Barotrauma.Items.Components float lastPickedFraction = 0.0f; if (RepairMultiple) { - var bodies = Submarine.PickBodies(rayStart, rayEnd, ignoredBodies, collisionCategories, ignoreSensors: false, allowInsideFixture: true); + var bodies = Submarine.PickBodies(rayStart, rayEnd, ignoredBodies, collisionCategories, ignoreSensors: RepairThroughHoles, allowInsideFixture: true); lastPickedFraction = Submarine.LastPickedFraction; Type lastHitType = null; hitCharacters.Clear(); @@ -243,7 +257,7 @@ namespace Barotrauma.Items.Components { FixBody(user, deltaTime, degreeOfSuccess, Submarine.PickBody(rayStart, rayEnd, - ignoredBodies, collisionCategories, ignoreSensors: false, + ignoredBodies, collisionCategories, ignoreSensors: RepairThroughHoles, customPredicate: (Fixture f) => { return f?.Body?.UserData != null; }, allowInsideFixture: true)); lastPickedFraction = Submarine.LastPickedFraction; @@ -324,6 +338,7 @@ namespace Barotrauma.Items.Components } else if (targetBody.UserData is Character targetCharacter) { + if (targetCharacter.Removed) { return false; } targetCharacter.LastDamageSource = item; ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, new List() { targetCharacter }); FixCharacterProjSpecific(user, deltaTime, targetCharacter); @@ -331,6 +346,7 @@ namespace Barotrauma.Items.Components } else if (targetBody.UserData is Limb targetLimb) { + if (targetLimb.character == null || targetLimb.character.Removed) { return false; } targetLimb.character.LastDamageSource = item; ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, new List() { targetLimb.character, targetLimb }); FixCharacterProjSpecific(user, deltaTime, targetLimb.character); @@ -506,9 +522,9 @@ namespace Barotrauma.Items.Components if (propertyName != "stuck") { continue; } if (door.SerializableProperties == null || !door.SerializableProperties.TryGetValue(propertyName, out SerializableProperty property)) { continue; } object value = property.GetValue(target); - if (value.GetType() == typeof(float)) + if (door.Stuck > 0) { - var progressBar = user.UpdateHUDProgressBar(door, door.Item.WorldPosition, (float)value / 100, Color.DarkGray * 0.5f, Color.White); + var progressBar = user.UpdateHUDProgressBar(door, door.Item.WorldPosition, door.Stuck / 100, Color.DarkGray * 0.5f, Color.White); if (progressBar != null) { progressBar.Size = new Vector2(60.0f, 20.0f); } } } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/Throwable.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/Throwable.cs index 45f4c2585..e3f4217c7 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/Throwable.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/Throwable.cs @@ -6,12 +6,10 @@ namespace Barotrauma.Items.Components { class Throwable : Holdable { - float throwForce; + private float throwForce, throwPos; + private bool throwing, throwDone; - float throwPos; - - bool throwing; - bool throwDone; + private bool midAir; [Serialize(1.0f, false)] public float ThrowForce @@ -57,7 +55,17 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - if (!item.body.Enabled) return; + if (!item.body.Enabled) { return; } + if (midAir) + { + if (item.body.LinearVelocity.LengthSquared() < 0.01f) + { + item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform; + midAir = false; + } + return; + } + if (picker == null || picker.Removed || !picker.HasSelectedItem(item)) { IsActive = false; @@ -113,6 +121,10 @@ namespace Barotrauma.Items.Components item.Drop(thrower, createNetworkEvent: GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer); item.body.ApplyLinearImpulse(throwVector * throwForce * item.body.Mass * 3.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + //disable platform collisions until the item comes back to rest again + item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel; + midAir = true; + ac.GetLimb(LimbType.Head).body.ApplyLinearImpulse(throwVector * 10.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); ac.GetLimb(LimbType.Torso).body.ApplyLinearImpulse(throwVector * 10.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/ItemComponent.cs index 93b1b199d..c07b58a9f 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/ItemComponent.cs @@ -82,13 +82,14 @@ namespace Barotrauma.Items.Components } } #endif - if (AITarget != null) AITarget.Enabled = value; isActive = value; } } private bool drawable = true; + public List IsActiveConditionals; + public bool Drawable { get { return drawable; } @@ -208,11 +209,6 @@ namespace Barotrauma.Items.Components set; } - public AITarget AITarget - { - get; - private set; - } /// /// How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not enforced). @@ -267,6 +263,15 @@ namespace Barotrauma.Items.Components { switch (subElement.Name.ToString().ToLowerInvariant()) { + case "activeconditional": + case "isactive": + IsActiveConditionals = IsActiveConditionals ?? new List(); + foreach (XAttribute attribute in subElement.Attributes()) + { + if (attribute.Name.ToString().ToLowerInvariant() == "targetitemcomponent") { continue; } + IsActiveConditionals.Add(new PropertyConditional(attribute)); + } + break; case "requireditem": case "requireditems": RelatedItem ri = RelatedItem.Load(subElement, item.Name); @@ -308,12 +313,6 @@ namespace Barotrauma.Items.Components effectList.Add(statusEffect); - break; - case "aitarget": - AITarget = new AITarget(item, subElement) - { - Enabled = isActive - }; break; default: if (LoadElemProjSpecific(subElement)) break; @@ -474,12 +473,6 @@ namespace Barotrauma.Items.Components delayedCorrectionCoroutine = null; } - if (AITarget != null) - { - AITarget.Remove(); - AITarget = null; - } - RemoveComponentSpecific(); } @@ -496,11 +489,6 @@ namespace Barotrauma.Items.Components loopingSoundChannel = null; } #endif - if (AITarget != null) - { - AITarget.Remove(); - AITarget = null; - } ShallowRemoveComponentSpecific(); } @@ -788,6 +776,7 @@ namespace Barotrauma.Items.Components public virtual void Reset() { SerializableProperties = SerializableProperty.DeserializeProperties(this, originalElement); + if (this is Pickable) { canBePicked = true; } ParseMsg(); OverrideRequiredItems(originalElement); } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Deconstructor.cs index a59edf726..4c0291752 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Deconstructor.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using Lidgren.Network; using System; using System.Linq; using System.Xml.Linq; @@ -84,6 +83,8 @@ namespace Barotrauma.Items.Components progressState = Math.Min(progressTimer / deconstructTime, 1.0f); if (progressTimer > deconstructTime) { + int emptySlots = outputContainer.Inventory.Items.Where(i => i == null).Count(); + foreach (DeconstructItem deconstructProduct in targetItem.Prefab.DeconstructItems) { float percentageHealth = targetItem.Condition / targetItem.Prefab.Health; @@ -100,13 +101,14 @@ namespace Barotrauma.Items.Components itemPrefab.Health * deconstructProduct.OutCondition; //container full, drop the items outside the deconstructor - if (outputContainer.Inventory.Items.All(i => i != null)) + if (emptySlots <= 0) { Entity.Spawner.AddToSpawnQueue(itemPrefab, item.Position, item.Submarine, condition); } else { Entity.Spawner.AddToSpawnQueue(itemPrefab, outputContainer.Inventory, condition); + emptySlots--; } } @@ -196,6 +198,7 @@ namespace Barotrauma.Items.Components if (inputContainer.Inventory.Items.All(i => i == null)) { active = false; } IsActive = active; + currPowerConsumption = IsActive ? powerConsumption : 0.0f; #if SERVER if (user != null) diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Engine.cs index 74d56b690..1bfa76d6d 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Engine.cs @@ -3,7 +3,6 @@ using System; using System.Globalization; using System.Xml.Linq; using Barotrauma.Networking; -using Lidgren.Network; namespace Barotrauma.Items.Components { @@ -98,11 +97,17 @@ namespace Barotrauma.Items.Components UpdatePropellerDamage(deltaTime); + if (item.AiTarget != null) + { + var aiTarget = item.AiTarget; + aiTarget.SoundRange = MathHelper.Lerp(aiTarget.MinSoundRange, aiTarget.MaxSoundRange, currForce.Length() / maxForce); + } if (item.CurrentHull != null) { - item.CurrentHull.AiTarget.SoundRange = Math.Max(currForce.Length(), item.CurrentHull.AiTarget.SoundRange); + var aiTarget = item.CurrentHull.AiTarget; + float noise = MathHelper.Lerp(aiTarget.MinSoundRange, aiTarget.MaxSoundRange, currForce.Length() / maxForce); + aiTarget.SoundRange = Math.Max(noise, aiTarget.SoundRange); } - #if CLIENT for (int i = 0; i < 5; i++) { diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Fabricator.cs index 59d850056..73e41e196 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Fabricator.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Pump.cs index 7998e5b1d..2f358e309 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Pump.cs @@ -12,6 +12,8 @@ namespace Barotrauma.Items.Components private float maxFlow; private float? targetLevel; + + private float controlLockTimer; private bool hasPower; @@ -57,18 +59,24 @@ namespace Barotrauma.Items.Components currFlow = 0.0f; hasPower = false; + controlLockTimer -= deltaTime; if (targetLevel != null) { float hullPercentage = 0.0f; - if (item.CurrentHull != null) hullPercentage = (item.CurrentHull.WaterVolume / item.CurrentHull.Volume) * 100.0f; + if (item.CurrentHull != null) { hullPercentage = (item.CurrentHull.WaterVolume / item.CurrentHull.Volume) * 100.0f; } FlowPercentage = ((float)targetLevel - hullPercentage) * 10.0f; + + if (controlLockTimer <= 0.0f) + { + targetLevel = null; + } } currPowerConsumption = powerConsumption * Math.Abs(flowPercentage / 100.0f); //pumps consume more power when in a bad condition currPowerConsumption *= MathHelper.Lerp(2.0f, 1.0f, item.Condition / item.MaxCondition); - if (voltage < minVoltage) return; + if (voltage < minVoltage) { return; } UpdateProjSpecific(deltaTime); @@ -109,6 +117,7 @@ namespace Barotrauma.Items.Components if (float.TryParse(signal, NumberStyles.Any, CultureInfo.InvariantCulture, out float tempSpeed)) { flowPercentage = MathHelper.Clamp(tempSpeed, -100.0f, 100.0f); + controlLockTimer = 0.1f; } } else if (connection.Name == "set_targetlevel") @@ -116,6 +125,7 @@ namespace Barotrauma.Items.Components if (float.TryParse(signal, NumberStyles.Any, CultureInfo.InvariantCulture, out float tempTarget)) { targetLevel = MathHelper.Clamp((tempTarget + 100.0f) / 2.0f, 0.0f, 100.0f); + controlLockTimer = 0.1f; } } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Reactor.cs index f0244b06b..31c995ea5 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Reactor.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -271,7 +270,7 @@ namespace Barotrauma.Items.Components //calculate how much external power there is in the grid //(power coming from somewhere else than this reactor, e.g. batteries) - float externalPower = CurrPowerConsumption - pt.CurrPowerConsumption; + float externalPower = Math.Max(CurrPowerConsumption - pt.CurrPowerConsumption, 0); //reduce the external power from the load to prevent overloading the grid load = Math.Max(load, pt.PowerLoad - externalPower); } @@ -288,10 +287,17 @@ namespace Barotrauma.Items.Components if (item.CurrentHull != null) { - //the sound can be heard from 20 000 display units away when running at full power - item.CurrentHull.SoundRange = Math.Max( - (-currPowerConsumption / MaxPowerOutput) * 20000.0f, - item.CurrentHull.AiTarget.SoundRange); + var aiTarget = item.CurrentHull.AiTarget; + float range = Math.Abs(currPowerConsumption) / MaxPowerOutput; + float noise = MathHelper.Lerp(aiTarget.MinSoundRange, aiTarget.MaxSoundRange, range); + aiTarget.SoundRange = Math.Max(aiTarget.SoundRange, noise); + } + + if (item.AiTarget != null) + { + var aiTarget = item.AiTarget; + float range = Math.Abs(currPowerConsumption) / MaxPowerOutput; + aiTarget.SoundRange = MathHelper.Lerp(aiTarget.MinSoundRange, aiTarget.MaxSoundRange, range); } } @@ -439,6 +445,8 @@ namespace Barotrauma.Items.Components { base.UpdateBroken(deltaTime, cam); + item.SendSignal(0, ((int)(temperature * 100.0f)).ToString(), "temperature_out", null); + currPowerConsumption = 0.0f; Temperature -= deltaTime * 1000.0f; targetFissionRate = Math.Max(targetFissionRate - deltaTime * 10.0f, 0.0f); diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Sonar.cs index 726272639..646a953cd 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Sonar.cs @@ -114,9 +114,9 @@ namespace Barotrauma.Items.Components if (value == Mode.Passive) { currentPingIndex = -1; - if (item.CurrentHull != null) + if (item.AiTarget != null) { - item.CurrentHull.AiTarget.SectorDegrees = 360.0f; + item.AiTarget.SectorDegrees = 360.0f; } } #if CLIENT @@ -168,15 +168,10 @@ namespace Barotrauma.Items.Components var activePing = activePings[currentPingIndex]; if (activePing.State > 1.0f) { - if (item.CurrentHull != null) - { - item.CurrentHull.AiTarget.SoundRange = Math.Max(Range * activePing.State / zoom, item.CurrentHull.AiTarget.SoundRange); - item.CurrentHull.AiTarget.SectorDegrees = activePing.IsDirectional ? DirectionalPingSector : 360.0f; - item.CurrentHull.AiTarget.SectorDir = new Vector2(pingDirection.X, -pingDirection.Y); - } if (item.AiTarget != null) { - item.AiTarget.SoundRange = Math.Max(Range * activePing.State / zoom, item.AiTarget.SoundRange); + float range = MathUtils.InverseLerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, Range * activePing.State / zoom); + item.AiTarget.SoundRange = MathHelper.Lerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, range); item.AiTarget.SectorDegrees = activePing.IsDirectional ? DirectionalPingSector : 360.0f; item.AiTarget.SectorDir = new Vector2(pingDirection.X, -pingDirection.Y); } @@ -200,9 +195,9 @@ namespace Barotrauma.Items.Components } else { - if (item.CurrentHull != null) + if (item.AiTarget != null) { - item.CurrentHull.AiTarget.SectorDegrees = 360.0f; + item.AiTarget.SectorDegrees = 360.0f; } currentPingIndex = -1; aiPingCheckPending = false; @@ -345,7 +340,7 @@ namespace Barotrauma.Items.Components } } - public void ServerRead(ClientNetObject type, Lidgren.Network.NetBuffer msg, Client c) + public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) { bool isActive = msg.ReadBoolean(); bool directionalPing = useDirectionalPing; @@ -388,7 +383,7 @@ namespace Barotrauma.Items.Components #endif } - public void ServerWrite(Lidgren.Network.NetBuffer msg, Client c, object[] extraData = null) + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { msg.Write(currentMode == Mode.Active); if (currentMode == Mode.Active) diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Steering.cs index 9821a38ce..d92f80dd6 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Steering.cs @@ -522,7 +522,7 @@ namespace Barotrauma.Items.Components } } - public void ServerRead(ClientNetObject type, Lidgren.Network.NetBuffer msg, Barotrauma.Networking.Client c) + public void ServerRead(ClientNetObject type, IReadMessage msg, Barotrauma.Networking.Client c) { bool autoPilot = msg.ReadBoolean(); bool dockingButtonClicked = msg.ReadBoolean(); @@ -537,8 +537,8 @@ namespace Barotrauma.Items.Components if (maintainPos) { newPosToMaintain = new Vector2( - msg.ReadFloat(), - msg.ReadFloat()); + msg.ReadSingle(), + msg.ReadSingle()); } else { @@ -547,7 +547,7 @@ namespace Barotrauma.Items.Components } else { - newSteeringInput = new Vector2(msg.ReadFloat(), msg.ReadFloat()); + newSteeringInput = new Vector2(msg.ReadSingle(), msg.ReadSingle()); } if (!item.CanClientAccess(c)) return; @@ -587,7 +587,7 @@ namespace Barotrauma.Items.Components unsentChanges = true; } - public void ServerWrite(Lidgren.Network.NetBuffer msg, Barotrauma.Networking.Client c, object[] extraData = null) + public void ServerWrite(IWriteMessage msg, Barotrauma.Networking.Client c, object[] extraData = null) { msg.Write(autoPilot); diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Power/PowerContainer.cs index f74e81242..8008375d6 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Power/PowerContainer.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Power/PowerTransfer.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Power/PowerTransfer.cs index b1b2350cb..90dfc7663 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Power/PowerTransfer.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Power/PowerTransfer.cs @@ -64,6 +64,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, false)] + public bool Overload + { + get; + set; + } + //can the component transfer power private bool canTransfer; public bool CanTransfer @@ -115,6 +122,8 @@ namespace Barotrauma.Items.Components { base.UpdateBroken(deltaTime, cam); + Overload = false; + if (!isBroken) { powerLoad = 0.0f; @@ -128,7 +137,8 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { RefreshConnections(); - if (!CanTransfer) return; + + if (!CanTransfer) { return; } if (isBroken) { @@ -143,6 +153,8 @@ namespace Barotrauma.Items.Components return; } + Overload = false; + //reset and recalculate the power generated/consumed //by the constructions connected to the grid fullPower = 0.0f; @@ -156,10 +168,11 @@ namespace Barotrauma.Items.Components foreach (Powered p in connectedList) { PowerTransfer pt = p as PowerTransfer; - if (pt == null || pt.updateCount == 0) continue; + if (pt == null || pt.updateCount == 0) { continue; } - if (pt is RelayComponent != this is RelayComponent) continue; + if (pt is RelayComponent != this is RelayComponent) { continue; } + pt.Overload = false; pt.powerLoad += (fullLoad - pt.powerLoad) / inertia; pt.currPowerConsumption += (-fullPower - pt.currPowerConsumption) / inertia; @@ -173,38 +186,38 @@ namespace Barotrauma.Items.Components pt.Item.SendSignal(0, "", "power", null, voltage); pt.Item.SendSignal(0, "", "power_out", null, voltage); -#if CLIENT - //damage the item if voltage is too high - //(except if running as a client) - if (GameMain.Client != null) continue; -#endif - //items in a bad condition are more sensitive to overvoltage - float maxOverVoltage = MathHelper.Lerp(OverloadVoltage * 0.75f, OverloadVoltage, item.Condition / item.MaxCondition); + float maxOverVoltage = MathHelper.Lerp(OverloadVoltage * 0.75f, OverloadVoltage, pt.item.Condition / pt.item.MaxCondition); maxOverVoltage = Math.Max(OverloadVoltage, 1.0f); //if the item can't be fixed, don't allow it to break - if (!item.Repairables.Any() || !CanBeOverloaded) continue; + if (!pt.item.Repairables.Any() || !pt.CanBeOverloaded) { continue; } //relays don't blow up if the power is higher than load, only if the output is high enough //(i.e. enough power passing through the relay) - if (this is RelayComponent) continue; + if (pt is RelayComponent) { continue; } - if (-pt.currPowerConsumption < Math.Max(pt.powerLoad, 200.0f) * maxOverVoltage) continue; + if (-pt.currPowerConsumption < Math.Max(pt.powerLoad, 200.0f) * maxOverVoltage) { continue; } + pt.Overload = true; +#if CLIENT + //damage the item if voltage is too high + //(except if running as a client) + if (GameMain.Client != null) { continue; } +#endif float prevCondition = pt.item.Condition; pt.item.Condition -= deltaTime * 10.0f; if (pt.item.Condition <= 0.0f && prevCondition > 0.0f) { #if CLIENT - SoundPlayer.PlaySound("zap", item.WorldPosition, hullGuess: item.CurrentHull); + SoundPlayer.PlaySound("zap", item.WorldPosition, hullGuess: pt.item.CurrentHull); Vector2 baseVel = Rand.Vector(300.0f); for (int i = 0; i < 10; i++) { var particle = GameMain.ParticleManager.CreateParticle("spark", pt.item.WorldPosition, - baseVel + Rand.Vector(100.0f), 0.0f, item.CurrentHull); + baseVel + Rand.Vector(100.0f), 0.0f, pt.item.CurrentHull); if (particle != null) particle.Size *= Rand.Range(0.5f, 1.0f); } @@ -214,8 +227,8 @@ namespace Barotrauma.Items.Components GameMain.GameSession.EventManager.CurrentIntensity : 0.5f; //higher probability for fires if the current intensity is low - if (FireProbability > 0.0f && - Rand.Range(0.0f, 1.0f) < MathHelper.Lerp(FireProbability, FireProbability * 0.1f, currentIntensity)) + if (pt.FireProbability > 0.0f && + Rand.Range(0.0f, 1.0f) < MathHelper.Lerp(pt.FireProbability, pt.FireProbability * 0.1f, currentIntensity)) { new FireSource(pt.item.WorldPosition); } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Projectile.cs index 22e00938a..3699aa7bd 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Projectile.cs @@ -409,11 +409,11 @@ namespace Barotrauma.Items.Components private bool OnProjectileCollision(Fixture target, Vector2 collisionNormal) { - if (User != null && User.Removed) User = null; + if (User != null && User.Removed) { User = null; } - if (IgnoredBodies.Contains(target.Body)) return false; + if (IgnoredBodies.Contains(target.Body)) { return false; } - if (target.UserData is Item) return false; + if (target.UserData is Item) { return false; } if (target.CollisionCategories == Physics.CollisionCharacter && !(target.Body.UserData is Limb)) { @@ -447,10 +447,21 @@ namespace Barotrauma.Items.Components if (attack != null) { attackResult = attack.DoDamage(User, structure, item.WorldPosition, 1.0f); } } - if (character != null) character.LastDamageSource = item; - ApplyStatusEffects(ActionType.OnUse, 1.0f, character, target.Body.UserData as Limb, user: user); - ApplyStatusEffects(ActionType.OnImpact, 1.0f, character, target.Body.UserData as Limb, user: user); - + if (character != null) { character.LastDamageSource = item; } + + if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) + { + ApplyStatusEffects(ActionType.OnUse, 1.0f, character, target.Body.UserData as Limb, user: user); + ApplyStatusEffects(ActionType.OnImpact, 1.0f, character, target.Body.UserData as Limb, user: user); +#if SERVER + if (GameMain.NetworkMember.IsServer) + { + GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnUse }); + GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnImpact }); + } +#endif + } + item.body.FarseerBody.OnCollision -= OnProjectileCollision; item.body.CollisionCategories = Physics.CollisionItem; diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Repairable.cs index 183fed5da..012e970f9 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Repairable.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Linq; @@ -10,12 +9,15 @@ namespace Barotrauma.Items.Components partial class Repairable : ItemComponent, IServerSerializable, IClientSerializable { public static float SkillIncreasePerRepair = 5.0f; + public static float SkillIncreasePerSabotage = 3.0f; private string header; private float deteriorationTimer; + private float deteriorateAlwaysResetTimer; bool wasBroken; + bool wasGoodCondition; public float LastActiveTime; @@ -40,14 +42,21 @@ namespace Barotrauma.Items.Components set; } - [Serialize(50.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, ToolTip = "The item won't deteriorate spontaneously if the condition is below this value. For example, if set to 10, the condition will spontaneously drop to 10 and then stop dropping (unless the item is damaged further by external factors).")] + [Serialize(50.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, ToolTip = "The item won't deteriorate spontaneously if the condition is below this value. For example, if set to 10, the condition will spontaneously drop to 10 and then stop dropping (unless the item is damaged further by external factors). Percentages of max condition.")] public float MinDeteriorationCondition { get; set; } - [Serialize(80.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, ToolTip = "The condition of the item has to be below this before the repair UI becomes usable.")] + [Serialize(0f, true)] + public float MinSabotageCondition + { + get; + set; + } + + [Serialize(80.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, ToolTip = "The condition of the item has to be below this before the repair UI becomes usable. Percentages of max condition.")] public float ShowRepairUIThreshold { get; @@ -76,16 +85,20 @@ namespace Barotrauma.Items.Components set; } - private Character currentFixer; - public Character CurrentFixer + public Character CurrentFixer { get; private set; } + + public enum FixActions : int { - get { return currentFixer; } - set - { - if (currentFixer == value || item.IsFullCondition) return; - if (currentFixer != null) currentFixer.AnimController.Anim = AnimController.Animation.None; - currentFixer = value; - } + None = 0, + Repair = 1, + Sabotage = 2 + } + + private FixActions currentFixerAction = FixActions.None; + public FixActions CurrentFixerAction + { + get => currentFixerAction; + private set { currentFixerAction = value; } } public Repairable(Item item, XElement element) @@ -105,18 +118,41 @@ namespace Barotrauma.Items.Components public override void OnItemLoaded() { deteriorationTimer = Rand.Range(MinDeteriorationDelay, MaxDeteriorationDelay); - -#if SERVER - //let the clients know the initial deterioration delay - item.CreateServerEvent(this); -#endif } partial void InitProjSpecific(XElement element); - public void StartRepairing(Character character) + public bool StartRepairing(Character character, FixActions action) { - CurrentFixer = character; + if (character == null || character.IsDead || action == FixActions.None) + { + DebugConsole.ThrowError("Invalid repair command!"); + return false; + } + else + { + CurrentFixer = character; + CurrentFixerAction = action; + return true; + } + } + + public bool StopRepairing(Character character) + { + if (CurrentFixer == character) + { + CurrentFixer.AnimController.Anim = AnimController.Animation.None; + CurrentFixer = null; + currentFixerAction = FixActions.None; +#if SERVER + item.CreateServerEvent(this); +#endif + return true; + } + else + { + return false; + } } public override void UpdateBroken(float deltaTime, Camera cam) @@ -129,7 +165,7 @@ namespace Barotrauma.Items.Components deteriorationTimer = Rand.Range(MinDeteriorationDelay, MaxDeteriorationDelay); item.Condition = item.Prefab.Health; #if SERVER - //let the clients know the initial deterioration delay + //let the clients know the deterioration delay item.CreateServerEvent(this); #endif } @@ -140,6 +176,18 @@ namespace Barotrauma.Items.Components if (CurrentFixer == null) { + if (deteriorateAlwaysResetTimer > 0.0f) + { + deteriorateAlwaysResetTimer -= deltaTime; + if (deteriorateAlwaysResetTimer <= 0.0f) + { + DeteriorateAlways = false; +#if SERVER + //let the clients know the deterioration delay + item.CreateServerEvent(this); +#endif + } + } if (!ShouldDeteriorate()) { return; } if (item.Condition > 0.0f) { @@ -155,7 +203,7 @@ namespace Barotrauma.Items.Components return; } - if (item.Condition > MinDeteriorationCondition) + if (item.ConditionPercentage > MinDeteriorationCondition) { item.Condition -= DeteriorationSpeed * deltaTime; } @@ -163,10 +211,9 @@ namespace Barotrauma.Items.Components return; } - if (Item.IsFullCondition || CurrentFixer.SelectedConstruction != item || !currentFixer.CanInteractWith(item)) + if (CurrentFixer != null && (CurrentFixer.SelectedConstruction != item || !CurrentFixer.CanInteractWith(item) || CurrentFixer.IsDead)) { - currentFixer.AnimController.Anim = AnimController.Animation.None; - currentFixer = null; + StopRepairing(CurrentFixer); return; } @@ -177,44 +224,89 @@ namespace Barotrauma.Items.Components float successFactor = requiredSkills.Count == 0 ? 1.0f : 0.0f; //item must have been below the repair threshold for the player to get an achievement or XP for repairing it - if (item.Condition < ShowRepairUIThreshold) + if (item.ConditionPercentage < ShowRepairUIThreshold) { wasBroken = true; } + if (item.ConditionPercentage > MinSabotageCondition) + { + wasGoodCondition = true; + } float fixDuration = MathHelper.Lerp(FixDurationLowSkill, FixDurationHighSkill, successFactor); - if (fixDuration <= 0.0f) + if (currentFixerAction == FixActions.Repair) { - item.Condition = item.MaxCondition; + if (fixDuration <= 0.0f) + { + item.Condition = item.MaxCondition; + } + else + { + float conditionIncrease = deltaTime / (fixDuration / item.MaxCondition); + item.Condition += conditionIncrease; +#if SERVER + GameMain.Server.KarmaManager.OnItemRepaired(CurrentFixer, this, conditionIncrease); +#endif + } + + if (item.IsFullCondition) + { + if (wasBroken) + { + foreach (Skill skill in requiredSkills) + { + float characterSkillLevel = CurrentFixer.GetSkillLevel(skill.Identifier); + CurrentFixer.Info.IncreaseSkillLevel(skill.Identifier, + SkillIncreasePerRepair / Math.Max(characterSkillLevel, 1.0f), + CurrentFixer.WorldPosition + Vector2.UnitY * 100.0f); + } + + SteamAchievementManager.OnItemRepaired(item, CurrentFixer); + deteriorationTimer = Rand.Range(MinDeteriorationDelay, MaxDeteriorationDelay); + wasBroken = false; + } + StopRepairing(CurrentFixer); + } + } + else if (currentFixerAction == FixActions.Sabotage) + { + if (fixDuration <= 0.0f) + { + item.Condition = item.MaxCondition * (MinSabotageCondition / 100); + } + else + { + float conditionDecrease = deltaTime / (fixDuration / item.MaxCondition); + item.Condition -= conditionDecrease; + } + + if (item.ConditionPercentage <= MinSabotageCondition) + { + if (wasGoodCondition) + { + foreach (Skill skill in requiredSkills) + { + float characterSkillLevel = CurrentFixer.GetSkillLevel(skill.Identifier); + CurrentFixer.Info.IncreaseSkillLevel(skill.Identifier, + SkillIncreasePerSabotage / Math.Max(characterSkillLevel, 1.0f), + CurrentFixer.WorldPosition + Vector2.UnitY * 100.0f); + } + + deteriorationTimer = 0.0f; + deteriorateAlwaysResetTimer = item.Condition / DeteriorationSpeed; + DeteriorateAlways = true; + item.Condition = item.MaxCondition * (MinSabotageCondition / 100); + wasGoodCondition = false; + } + StopRepairing(CurrentFixer); + } } else { - float conditionIncrease = deltaTime / (fixDuration / item.MaxCondition); - item.Condition += conditionIncrease; -#if SERVER - GameMain.Server.KarmaManager.OnItemRepaired(CurrentFixer, this, conditionIncrease); -#endif - } - - if (wasBroken && item.IsFullCondition) - { - foreach (Skill skill in requiredSkills) - { - float characterSkillLevel = CurrentFixer.GetSkillLevel(skill.Identifier); - CurrentFixer.Info.IncreaseSkillLevel(skill.Identifier, - SkillIncreasePerRepair / Math.Max(characterSkillLevel, 1.0f), - CurrentFixer.WorldPosition + Vector2.UnitY * 100.0f); - } - SteamAchievementManager.OnItemRepaired(item, currentFixer); - deteriorationTimer = Rand.Range(MinDeteriorationDelay, MaxDeteriorationDelay); - wasBroken = false; -#if SERVER - item.CreateServerEvent(this); -#endif + throw new NotImplementedException(currentFixerAction.ToString()); } } - partial void UpdateProjSpecific(float deltaTime); private bool ShouldDeteriorate() diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/Connection.cs index 541036a31..cb9a09a9d 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/Connection.cs @@ -218,6 +218,16 @@ namespace Barotrauma.Items.Components public void SetWire(int index, Wire wire) { + Wire previousWire = wires[index]; + if (wire != previousWire && previousWire != null) + { + var otherConnection = previousWire.OtherConnection(this); + if (otherConnection != null) + { + otherConnection.recipientsDirty = true; + } + } + wires[index] = wire; recipientsDirty = true; if (wire != null) diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/ConnectionPanel.cs index 781568b39..1416ce582 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/ConnectionPanel.cs @@ -1,6 +1,5 @@ using Barotrauma.Networking; using FarseerPhysics; -using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -19,7 +18,9 @@ namespace Barotrauma.Items.Components /// Wires that have been disconnected from the panel, but not removed completely (visible at the bottom of the connection panel). /// public readonly HashSet DisconnectedWires = new HashSet(); - + + private List disconnectedWireIds; + [Serialize(false, true), Editable(ToolTip = "Locked connection panels cannot be rewired in-game.")] public bool Locked { @@ -64,6 +65,19 @@ namespace Barotrauma.Items.Components { c.ConnectLinked(); } + + if (disconnectedWireIds != null) + { + foreach (ushort disconnectedWireId in disconnectedWireIds) + { + if (!(Entity.FindEntityByID(disconnectedWireId) is Item wireItem)) { continue; } + Wire wire = wireItem.GetComponent(); + if (wire != null) + { + DisconnectedWires.Add(wire); + } + } + } } public override void OnItemLoaded() @@ -193,6 +207,8 @@ namespace Barotrauma.Items.Components { loadedConnections[i].wireId.CopyTo(Connections[i].wireId, 0); } + + disconnectedWireIds = element.GetAttributeUshortArray("disconnectedwires", new ushort[0]).ToList(); } public override XElement Save(XElement parentElement) @@ -204,6 +220,11 @@ namespace Barotrauma.Items.Components c.Save(componentElement); } + if (DisconnectedWires.Count > 0) + { + componentElement.Add(new XAttribute("disconnectedwires", string.Join(",", DisconnectedWires.Select(w => w.Item.ID)))); + } + return componentElement; } @@ -214,6 +235,14 @@ namespace Barotrauma.Items.Components protected override void RemoveComponentSpecific() { + foreach (Wire wire in DisconnectedWires.ToList()) + { + if (wire.OtherConnection(null) == null) //wire not connected to anything else + { + wire.Item.Drop(null); + } + } + DisconnectedWires.Clear(); foreach (Connection c in Connections) { @@ -233,7 +262,7 @@ namespace Barotrauma.Items.Components } } - public void ClientWrite(NetBuffer msg, object[] extraData = null) + public void ClientWrite(IWriteMessage msg, object[] extraData = null) { foreach (Connection connection in Connections) { diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/CustomInterface.cs index a91d38da2..5cd9ab35b 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/CustomInterface.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using Lidgren.Network; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/LightComponent.cs index 6ce8d7502..47d479c7b 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/LightComponent.cs @@ -2,7 +2,6 @@ using System; using System.Xml.Linq; using Barotrauma.Networking; -using Lidgren.Network; #if CLIENT using Microsoft.Xna.Framework.Graphics; using Barotrauma.Lights; @@ -251,10 +250,6 @@ namespace Barotrauma.Items.Components light.Range = range; #endif } - if (AITarget != null) - { - UpdateAITarget(AITarget); - } if (item.AiTarget != null) { UpdateAITarget(item.AiTarget); @@ -267,6 +262,7 @@ namespace Barotrauma.Items.Components public override void UpdateBroken(float deltaTime, Camera cam) { light.Color = Color.Transparent; + lightBrightness = 0.0f; } protected override void RemoveComponentSpecific() @@ -298,7 +294,7 @@ namespace Barotrauma.Items.Components } } - public void ServerWrite(NetBuffer msg, Client c, object[] extraData = null) + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { msg.Write(IsOn); } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/RelayComponent.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/RelayComponent.cs index 3ea9643ad..e9db3ebc7 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/RelayComponent.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/RelayComponent.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using Lidgren.Network; using System; using System.Collections.Generic; using System.Xml.Linq; @@ -104,12 +103,12 @@ namespace Barotrauma.Items.Components IsOn = on; } - public void ServerWrite(NetBuffer msg, Client c, object[] extraData = null) + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { msg.Write(isOn); } - public void ClientRead(ServerNetObject type, NetBuffer msg, float sendingTime) + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { SetState(msg.ReadBoolean(), true); } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/Wire.cs index 6b4c8736e..3ad02ff77 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/Wire.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -282,7 +281,7 @@ namespace Barotrauma.Items.Components Structure attachTarget = Structure.GetAttachTarget(item.WorldPosition); canPlaceNode = attachTarget != null; - sub = attachTarget?.Submarine; + sub = sub ?? attachTarget?.Submarine; newNodePos = sub == null ? item.WorldPosition : item.WorldPosition - sub.Position - sub.HiddenSubPosition; @@ -333,7 +332,8 @@ namespace Barotrauma.Items.Components } else { - newNodePos = RoundNode(item.Position, item.CurrentHull) - sub.HiddenSubPosition; + newNodePos = RoundNode(item.Position, item.CurrentHull); + if (sub != null) { newNodePos -= sub.HiddenSubPosition; } canPlaceNode = true; } @@ -724,7 +724,7 @@ namespace Barotrauma.Items.Components base.RemoveComponentSpecific(); } - public void ClientRead(ServerNetObject type, NetBuffer msg, float sendingTime) + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { int eventIndex = msg.ReadRangedInteger(0, (int)Math.Ceiling(MaxNodeCount / (float)MaxNodesPerNetworkEvent)); int nodeCount = msg.ReadRangedInteger(0, MaxNodesPerNetworkEvent); @@ -738,7 +738,7 @@ namespace Barotrauma.Items.Components for (int i = 0; i < nodeCount; i++) { - nodePositions[nodeStartIndex + i] = new Vector2(msg.ReadFloat(), msg.ReadFloat()); + nodePositions[nodeStartIndex + i] = new Vector2(msg.ReadSingle(), msg.ReadSingle()); } if (nodePositions.Any(n => !MathUtils.IsValid(n))) diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Turret.cs index e839e8c8b..1d4fb631f 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Turret.cs @@ -1,6 +1,5 @@ using Barotrauma.Networking; using FarseerPhysics; -using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -674,7 +673,7 @@ namespace Barotrauma.Items.Components } } - public void ServerWrite(NetBuffer msg, Client c, object[] extraData = null) + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { Item item = (Item)extraData[2]; msg.Write(item.Removed ? (ushort)0 : item.ID); diff --git a/Barotrauma/BarotraumaShared/Source/Items/Inventory.cs b/Barotrauma/BarotraumaShared/Source/Items/Inventory.cs index 85d352153..5848bbfc7 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Inventory.cs @@ -1,6 +1,5 @@ using Barotrauma.Items.Components; using Barotrauma.Networking; -using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -375,7 +374,7 @@ namespace Barotrauma } } - public void SharedWrite(NetBuffer msg, object[] extraData = null) + public void SharedWrite(IWriteMessage msg, object[] extraData = null) { for (int i = 0; i < capacity; i++) { diff --git a/Barotrauma/BarotraumaShared/Source/Items/Item.cs b/Barotrauma/BarotraumaShared/Source/Items/Item.cs index 3ac7e5fe3..1606e1a78 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Item.cs @@ -3,7 +3,6 @@ using Barotrauma.Networking; using FarseerPhysics; using FarseerPhysics.Dynamics; using FarseerPhysics.Dynamics.Contacts; -using Lidgren.Network; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; @@ -127,6 +126,24 @@ namespace Barotrauma } } + public delegate bool InventoryFilter(Inventory inventory); + public Inventory FindParentInventory(InventoryFilter filter) + { + if (parentInventory != null) + { + if (filter(parentInventory)) + { + return parentInventory; + } + var owner = parentInventory.Owner as Item; + if (owner != null) + { + return owner.FindParentInventory(filter); + } + } + return null; + } + private Item container; public Item Container { @@ -597,12 +614,15 @@ namespace Barotrauma case "fabricate": case "fabricable": case "fabricableitem": + case "upgrade": break; case "staticbody": StaticBodyConfig = subElement; break; case "aitarget": aiTarget = new AITarget(this, subElement); + aiTarget.SoundRange = aiTarget.MinSoundRange; + aiTarget.SightRange = aiTarget.MinSightRange; break; default: ItemComponent ic = ItemComponent.Load(subElement, this, itemPrefab.ConfigFile); @@ -674,9 +694,6 @@ namespace Barotrauma } InitProjSpecific(); - - InsertToList(); - ItemList.Add(this); if (callOnItemLoaded) { @@ -686,6 +703,9 @@ namespace Barotrauma } } + InsertToList(); + ItemList.Add(this); + DebugConsole.Log("Created " + Name + " (" + ID + ")"); } @@ -988,7 +1008,24 @@ namespace Barotrauma } return false; } - + + private bool ConditionalMatches(PropertyConditional conditional) + { + if (string.IsNullOrEmpty(conditional.TargetItemComponentName)) + { + if (!conditional.Matches(this)) { return false; } + } + else + { + foreach (ItemComponent component in components) + { + if (component.Name != conditional.TargetItemComponentName) { continue; } + if (!conditional.Matches(component)) { return false; } + } + } + return true; + } + public void ApplyStatusEffects(ActionType type, float deltaTime, Character character = null, Limb limb = null, bool isNetworkEvent = false) { if (!hasStatusEffectsOfType[(int)type]) { return; } @@ -1103,6 +1140,7 @@ namespace Barotrauma public override void Update(float deltaTime, Camera cam) { + base.Update(deltaTime, cam); //aitarget goes silent/invisible if the components don't keep it active if (aiTarget != null) { @@ -1131,7 +1169,11 @@ namespace Barotrauma foreach (ItemComponent ic in components) { - if (ic.Parent != null) ic.IsActive = ic.Parent.IsActive; + if (ic.Parent != null) { ic.IsActive = ic.Parent.IsActive; } + if (ic.IsActiveConditionals != null) + { + ic.IsActive = ic.IsActiveConditionals.All(conditional => ConditionalMatches(conditional)); + } #if CLIENT if (!ic.WasUsed) @@ -1827,7 +1869,7 @@ namespace Barotrauma return allProperties; } - private void WritePropertyChange(NetBuffer msg, object[] extraData, bool inGameEditableOnly) + private void WritePropertyChange(IWriteMessage msg, object[] extraData, bool inGameEditableOnly) { var allProperties = inGameEditableOnly ? GetProperties() : GetProperties(); SerializableProperty property = extraData[1] as SerializableProperty; @@ -1836,7 +1878,7 @@ namespace Barotrauma var propertyOwner = allProperties.Find(p => p.Second == property); if (allProperties.Count > 1) { - msg.WriteRangedInteger(0, allProperties.Count - 1, allProperties.FindIndex(p => p.Second == property)); + msg.WriteRangedIntegerDeprecated(0, allProperties.Count - 1, allProperties.FindIndex(p => p.Second == property)); } object value = property.GetValue(propertyOwner.First); @@ -1908,7 +1950,7 @@ namespace Barotrauma } } - private void ReadPropertyChange(NetBuffer msg, bool inGameEditableOnly, Client sender = null) + private void ReadPropertyChange(IReadMessage msg, bool inGameEditableOnly, Client sender = null) { var allProperties = inGameEditableOnly ? GetProperties() : GetProperties(); if (allProperties.Count == 0) { return; } @@ -1940,7 +1982,7 @@ namespace Barotrauma } else if (type == typeof(float)) { - float val = msg.ReadFloat(); + float val = msg.ReadSingle(); if (allowEditing) property.TrySetValue(parentObject, val); } else if (type == typeof(int)) @@ -1960,17 +2002,17 @@ namespace Barotrauma } else if (type == typeof(Vector2)) { - Vector2 val = new Vector2(msg.ReadFloat(), msg.ReadFloat()); + Vector2 val = new Vector2(msg.ReadSingle(), msg.ReadSingle()); if (allowEditing) property.TrySetValue(parentObject, val); } else if (type == typeof(Vector3)) { - Vector3 val = new Vector3(msg.ReadFloat(), msg.ReadFloat(), msg.ReadFloat()); + Vector3 val = new Vector3(msg.ReadSingle(), msg.ReadSingle(), msg.ReadSingle()); if (allowEditing) property.TrySetValue(parentObject, val); } else if (type == typeof(Vector4)) { - Vector4 val = new Vector4(msg.ReadFloat(), msg.ReadFloat(), msg.ReadFloat(), msg.ReadFloat()); + Vector4 val = new Vector4(msg.ReadSingle(), msg.ReadSingle(), msg.ReadSingle(), msg.ReadSingle()); if (allowEditing) property.TrySetValue(parentObject, val); } else if (type == typeof(Point)) @@ -2038,7 +2080,7 @@ namespace Barotrauma { return null; } - + Rectangle rect = element.GetAttributeRect("rect", Rectangle.Empty); if (rect.Width == 0 && rect.Height == 0) { @@ -2090,7 +2132,7 @@ namespace Barotrauma foreach (XElement subElement in element.Elements()) { ItemComponent component = unloadedComponents.Find(x => x.Name == subElement.Name.ToString()); - if (component == null) continue; + if (component == null) { continue; } component.Load(subElement); unloadedComponents.Remove(component); @@ -2104,6 +2146,11 @@ namespace Barotrauma item.SetActiveSprite(); + if (submarine?.GameVersion != null) + { + SerializableProperty.UpgradeGameVersion(item, item.Prefab.ConfigElement, submarine.GameVersion); + } + foreach (ItemComponent component in item.components) { component.OnItemLoaded(); @@ -2164,6 +2211,7 @@ namespace Barotrauma SerializableProperties = SerializableProperty.DeserializeProperties(this, Prefab.ConfigElement); Sprite.ReloadXML(); SpriteDepth = Sprite.Depth; + condition = Prefab.Health; components.ForEach(c => c.Reset()); } @@ -2249,4 +2297,4 @@ namespace Barotrauma partial void RemoveProjSpecific(); } -} \ No newline at end of file +} diff --git a/Barotrauma/BarotraumaShared/Source/Map/Hull.cs b/Barotrauma/BarotraumaShared/Source/Map/Hull.cs index 2e8333b2c..602c23f6c 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Hull.cs @@ -1,7 +1,6 @@ using Barotrauma.Networking; using FarseerPhysics; using FarseerPhysics.Dynamics; -using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -229,7 +228,13 @@ namespace Barotrauma surface = rect.Y - rect.Height; - aiTarget = new AITarget(this); + aiTarget = new AITarget(this) + { + MinSightRange = 2000, + MaxSightRange = 5000, + MaxSoundRange = 5000, + SoundRange = 0 + }; hullList.Add(this); @@ -418,13 +423,14 @@ namespace Barotrauma public override void Update(float deltaTime, Camera cam) { + base.Update(deltaTime, cam); UpdateProjSpecific(deltaTime, cam); Oxygen -= OxygenDeteriorationSpeed * deltaTime; FireSource.UpdateAll(FireSources, deltaTime); - aiTarget.SightRange = Submarine == null ? 0.0f : Math.Max(Submarine.Velocity.Length() * 2000.0f, AITarget.StaticSightRange); + aiTarget.SightRange = Submarine == null ? aiTarget.MinSightRange : Submarine.Velocity.Length() / 2 * aiTarget.MaxSightRange; aiTarget.SoundRange -= deltaTime * 1000.0f; if (!update) diff --git a/Barotrauma/BarotraumaShared/Source/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/Source/Map/Levels/Level.cs index 5c93cc301..28c012bf4 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Levels/Level.cs @@ -5,7 +5,6 @@ using Barotrauma.RuinGeneration; using FarseerPhysics; using FarseerPhysics.Dynamics; using FarseerPhysics.Factories; -using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -1673,7 +1672,7 @@ namespace Barotrauma loaded = null; } - public void ServerWrite(NetBuffer msg, Client c, object[] extraData = null) + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { foreach (LevelWall levelWall in extraWalls) { diff --git a/Barotrauma/BarotraumaShared/Source/Map/Levels/LevelObjects/LevelObject.cs b/Barotrauma/BarotraumaShared/Source/Map/Levels/LevelObjects/LevelObject.cs index 723dcdd31..08432da37 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Levels/LevelObjects/LevelObject.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Levels/LevelObjects/LevelObject.cs @@ -1,6 +1,5 @@ using Barotrauma.Networking; using FarseerPhysics; -using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -122,7 +121,7 @@ namespace Barotrauma return "LevelObject (" + ActivePrefab.Name + ")"; } - public void ServerWrite(NetBuffer msg, Client c) + public void ServerWrite(IWriteMessage msg, Client c) { for (int j = 0; j < Triggers.Count; j++) { diff --git a/Barotrauma/BarotraumaShared/Source/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaShared/Source/Map/Levels/LevelObjects/LevelObjectManager.cs index 19f6b09e5..3528cfee9 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -3,7 +3,6 @@ using Barotrauma.Particles; #endif using Barotrauma.Networking; using FarseerPhysics; -using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -407,10 +406,10 @@ namespace Barotrauma partial void RemoveProjSpecific(); - public void ServerWrite(NetBuffer msg, Client c, object[] extraData = null) + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { LevelObject obj = extraData[0] as LevelObject; - msg.WriteRangedInteger(0, objects.Count, objects.IndexOf(obj)); + msg.WriteRangedIntegerDeprecated(0, objects.Count, objects.IndexOf(obj)); obj.ServerWrite(msg, c); } } diff --git a/Barotrauma/BarotraumaShared/Source/Map/Levels/LevelObjects/LevelTrigger.cs b/Barotrauma/BarotraumaShared/Source/Map/Levels/LevelObjects/LevelTrigger.cs index 0bcb4a4e7..0d27b15b7 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Levels/LevelObjects/LevelTrigger.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Levels/LevelObjects/LevelTrigger.cs @@ -2,7 +2,6 @@ using FarseerPhysics; using FarseerPhysics.Dynamics; using FarseerPhysics.Dynamics.Contacts; -using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -598,7 +597,7 @@ namespace Barotrauma return vel.ClampLength(ConvertUnits.ToDisplayUnits(ForceVelocityLimit)) * currentForceFluctuation; } - public void ServerWrite(NetBuffer msg, Client c) + public void ServerWrite(IWriteMessage msg, Client c) { if (ForceFluctuationStrength > 0.0f) { diff --git a/Barotrauma/BarotraumaShared/Source/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/Source/Map/MapEntity.cs index 97e93dd95..53c6e4171 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/MapEntity.cs @@ -339,6 +339,11 @@ namespace Barotrauma hull.Update(deltaTime, cam); } + foreach (Structure structure in Structure.WallList) + { + structure.Update(deltaTime, cam); + } + foreach (Gap gap in Gap.GapList) { gap.Update(deltaTime, cam); diff --git a/Barotrauma/BarotraumaShared/Source/Map/MapEntityPrefab.cs b/Barotrauma/BarotraumaShared/Source/Map/MapEntityPrefab.cs index 7e308f7f4..732d6f999 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/MapEntityPrefab.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/MapEntityPrefab.cs @@ -40,6 +40,18 @@ namespace Barotrauma get { return name; } } + public string GetItemNameTextId() + { + var textId = $"entityname.{Identifier}"; + return TextManager.ContainsTag(textId) ? textId : null; + } + + public string GetHullNameTextId() + { + var textId = $"roomname.{Identifier}"; + return TextManager.ContainsTag(textId) ? textId : null; + } + //Used to differentiate between items when saving/loading //Allows changing the name of an item without breaking existing subs or having multiple items with the same name public string Identifier diff --git a/Barotrauma/BarotraumaShared/Source/Map/Structure.cs b/Barotrauma/BarotraumaShared/Source/Map/Structure.cs index 0211c3d84..7808fb626 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Structure.cs @@ -4,7 +4,6 @@ using FarseerPhysics; using FarseerPhysics.Dynamics; using FarseerPhysics.Dynamics.Contacts; using FarseerPhysics.Factories; -using Lidgren.Network; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; @@ -373,7 +372,12 @@ namespace Barotrauma // Only add ai targets automatically to submarine/outpost walls if (aiTarget == null && HasBody && Tags.Contains("wall") && submarine != null && !Prefab.NoAITarget) { - aiTarget = new AITarget(this); + aiTarget = new AITarget(this) + { + MinSightRange = 2000, + MaxSightRange = 5000, + MaxSoundRange = 0 + }; } InsertToList(); @@ -460,15 +464,16 @@ namespace Barotrauma { if (IsHorizontal) { - xsections = (int)Math.Ceiling((float)rect.Width / WallSectionSize); + //equivalent to (int)Math.Ceiling((double)rect.Width / WallSectionSize) without the potential for floating point indeterminism + xsections = (rect.Width + WallSectionSize - 1) / WallSectionSize; Sections = new WallSection[xsections]; - width = (int)WallSectionSize; + width = WallSectionSize; } else { - ysections = (int)Math.Ceiling((float)rect.Height / WallSectionSize); + ysections = (rect.Height + WallSectionSize - 1) / WallSectionSize; Sections = new WallSection[ysections]; - height = (int)WallSectionSize; + height = WallSectionSize; } } @@ -1184,21 +1189,32 @@ namespace Barotrauma ID = (ushort)int.Parse(element.Attribute("ID").Value) }; + SerializableProperty.DeserializeProperties(s, element); + foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString()) { case "section": int index = subElement.GetAttributeInt("i", -1); - if (index == -1) continue; - s.Sections[index].damage = subElement.GetAttributeFloat("damage", 0.0f); + if (index == -1) { continue; } + + if (index < 0 || index >= s.SectionCount) + { + string errorMsg = $"Error while loading structure \"{s.Name}\". Section damage index out of bounds. Index: {index}, section count: {s.SectionCount}."; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Structure.Load:SectionIndexOutOfBounds", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + } + else + { + s.Sections[index].damage = subElement.GetAttributeFloat("damage", 0.0f); + } break; } } if (element.GetAttributeBool("flippedx", false)) s.FlipX(false); if (element.GetAttributeBool("flippedy", false)) s.FlipY(false); - SerializableProperty.DeserializeProperties(s, element); //structures with a body drop a shadow by default if (element.Attribute("usedropshadow") == null) @@ -1277,5 +1293,14 @@ namespace Barotrauma { SerializableProperties = SerializableProperty.DeserializeProperties(this, Prefab.ConfigElement); } + + public override void Update(float deltaTime, Camera cam) + { + base.Update(deltaTime, cam); + if (aiTarget != null) + { + aiTarget.SightRange = Submarine == null ? aiTarget.MinSightRange : Submarine.Velocity.Length() / 2 * aiTarget.MaxSightRange; + } + } } } diff --git a/Barotrauma/BarotraumaShared/Source/Map/Submarine.cs b/Barotrauma/BarotraumaShared/Source/Map/Submarine.cs index 6d25e9c13..8331c8af6 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Submarine.cs @@ -3,7 +3,6 @@ using Barotrauma.Networking; using Barotrauma.RuinGeneration; using FarseerPhysics; using FarseerPhysics.Dynamics; -using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -1143,7 +1142,6 @@ namespace Barotrauma savedSubmarines.Add(sub); } - public static void RefreshSavedSub(string filePath) { string fullPath = Path.GetFullPath(filePath); @@ -1154,12 +1152,15 @@ namespace Barotrauma savedSubmarines[i].Dispose(); } } - var sub = new Submarine(filePath); - if (!sub.IsFileCorrupted) + if (File.Exists(filePath)) { - savedSubmarines.Add(sub); + var sub = new Submarine(filePath); + if (!sub.IsFileCorrupted) + { + savedSubmarines.Add(sub); + } + savedSubmarines = savedSubmarines.OrderBy(s => s.filePath ?? "").ToList(); } - savedSubmarines = savedSubmarines.OrderBy(s => s.filePath ?? "").ToList(); } public static void RefreshSavedSubs() @@ -1210,6 +1211,15 @@ namespace Barotrauma } } + var contentPackageSubs = ContentPackage.GetFilesOfType(GameMain.Config.SelectedContentPackages, ContentType.Submarine); + foreach (string subPath in contentPackageSubs) + { + if (!filePaths.Any(fp => Path.GetFullPath(fp) == Path.GetFullPath(subPath))) + { + filePaths.Add(subPath); + } + } + foreach (string path in filePaths) { var sub = new Submarine(path); diff --git a/Barotrauma/BarotraumaShared/Source/Networking/ChatMessage.cs b/Barotrauma/BarotraumaShared/Source/Networking/ChatMessage.cs index b89da7dfc..984a42c28 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/ChatMessage.cs @@ -1,5 +1,4 @@ using Barotrauma.Items.Components; -using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Linq; @@ -7,9 +6,9 @@ using System.Text; namespace Barotrauma.Networking { - enum ChatMessageType + public enum ChatMessageType { - Default, Error, Dead, Server, Radio, Private, Console, MessageBox, Order, ServerLog + Default, Error, Dead, Server, Radio, Private, Console, MessageBox, Order, ServerLog, ServerMessageBox } partial class ChatMessage diff --git a/Barotrauma/BarotraumaShared/Source/Networking/Client.cs b/Barotrauma/BarotraumaShared/Source/Networking/Client.cs index 192b4eb19..8eab04a50 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/Client.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/Client.cs @@ -1,4 +1,4 @@ -using Lidgren.Network; +using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -12,7 +12,8 @@ namespace Barotrauma.Networking public string Name; public byte ID; - + public UInt64 SteamID; + public Character.TeamType TeamID; private Character character; @@ -38,6 +39,12 @@ namespace Barotrauma.Networking HasSpawned = true; #if CLIENT GameMain.GameSession?.CrewManager?.SetPlayerVoiceIconState(this, muted, mutedLocally); + + if (character == GameMain.Client.Character && GameMain.Client.SpawnAsTraitor) + { + character.IsTraitor = true; + character.TraitorCurrentObjective = GameMain.Client.TraitorFirstObjective; + } #endif } } @@ -179,7 +186,7 @@ namespace Barotrauma.Networking } } - public void WritePermissions(NetBuffer msg) + public void WritePermissions(IWriteMessage msg) { msg.Write(ID); msg.Write((UInt16)Permissions); @@ -192,7 +199,7 @@ namespace Barotrauma.Networking } } } - public static void ReadPermissions(NetBuffer inc, out ClientPermissions permissions, out List permittedCommands) + public static void ReadPermissions(IReadMessage inc, out ClientPermissions permissions, out List permittedCommands) { UInt16 permissionsInt = inc.ReadUInt16(); @@ -221,7 +228,7 @@ namespace Barotrauma.Networking } } - public void ReadPermissions(NetIncomingMessage inc) + public void ReadPermissions(IReadMessage inc) { ClientPermissions permissions = ClientPermissions.None; List permittedCommands = new List(); diff --git a/Barotrauma/BarotraumaShared/Source/Networking/ClientPermissions.cs b/Barotrauma/BarotraumaShared/Source/Networking/ClientPermissions.cs index 95adfe26a..42163b1c3 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/ClientPermissions.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/ClientPermissions.cs @@ -7,7 +7,7 @@ using System.Xml.Linq; namespace Barotrauma.Networking { [Flags] - enum ClientPermissions + public enum ClientPermissions { None = 0x0, ManageRound = 0x1, diff --git a/Barotrauma/BarotraumaShared/Source/Networking/INetSerializable.cs b/Barotrauma/BarotraumaShared/Source/Networking/INetSerializable.cs index ddef41f2a..9313fec02 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/INetSerializable.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/INetSerializable.cs @@ -1,6 +1,4 @@ -using Lidgren.Network; - -namespace Barotrauma.Networking +namespace Barotrauma.Networking { interface INetSerializable { } @@ -10,10 +8,10 @@ namespace Barotrauma.Networking interface IClientSerializable : INetSerializable { #if CLIENT - void ClientWrite(NetBuffer msg, object[] extraData = null); + void ClientWrite(IWriteMessage msg, object[] extraData = null); #endif #if SERVER - void ServerRead(ClientNetObject type, NetBuffer msg, Client c); + void ServerRead(ClientNetObject type, IReadMessage msg, Client c); #endif } @@ -23,10 +21,10 @@ namespace Barotrauma.Networking interface IServerSerializable : INetSerializable { #if SERVER - void ServerWrite(NetBuffer msg, Client c, object[] extraData = null); + void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null); #endif #if CLIENT - void ClientRead(ServerNetObject type, NetBuffer msg, float sendingTime); + void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime); #endif } } diff --git a/Barotrauma/BarotraumaShared/Source/Networking/KarmaManager.cs b/Barotrauma/BarotraumaShared/Source/Networking/KarmaManager.cs index dacacbe4d..18692dfb5 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/KarmaManager.cs @@ -53,12 +53,12 @@ namespace Barotrauma public float ExtinguishFireKarmaIncrease { get; set; } - private float allowedWireDisconnectionsPerMinute; - [Serialize(5.0f, true)] - public float AllowedWireDisconnectionsPerMinute + private int allowedWireDisconnectionsPerMinute; + [Serialize(5, true)] + public int AllowedWireDisconnectionsPerMinute { get { return allowedWireDisconnectionsPerMinute; } - set { allowedWireDisconnectionsPerMinute = Math.Max(0.0f, value); } + set { allowedWireDisconnectionsPerMinute = Math.Max(0, value); } } [Serialize(6.0f, true)] @@ -76,6 +76,9 @@ namespace Barotrauma [Serialize(1.0f, true)] public float KickBanThreshold { get; set; } + [Serialize(0, true)] + public int KicksBeforeBan { get; set; } + [Serialize(10.0f, true)] public float KarmaNotificationInterval { get; set; } diff --git a/Barotrauma/BarotraumaShared/Source/Networking/NetEntityEvent/NetEntityEvent.cs b/Barotrauma/BarotraumaShared/Source/Networking/NetEntityEvent/NetEntityEvent.cs index d93b9258e..4d642e749 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/NetEntityEvent/NetEntityEvent.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/NetEntityEvent/NetEntityEvent.cs @@ -1,5 +1,4 @@ -using Lidgren.Network; -using System; +using System; namespace Barotrauma.Networking { diff --git a/Barotrauma/BarotraumaShared/Source/Networking/NetEntityEvent/NetEntityEventManager.cs b/Barotrauma/BarotraumaShared/Source/Networking/NetEntityEvent/NetEntityEventManager.cs index ed4b325f2..b335e23c0 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/NetEntityEvent/NetEntityEventManager.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/NetEntityEvent/NetEntityEventManager.cs @@ -1,5 +1,4 @@ -using Lidgren.Network; -using System; +using System; using System.Collections.Generic; namespace Barotrauma.Networking @@ -11,10 +10,10 @@ namespace Barotrauma.Networking /// /// Write the events to the outgoing message. The recipient parameter is only needed for ServerEntityEventManager /// - protected void Write(NetOutgoingMessage msg, List eventsToSync, out List sentEvents, Client recipient = null) + protected void Write(IWriteMessage msg, List eventsToSync, out List sentEvents, Client recipient = null) { //write into a temporary buffer so we can write the number of events before the actual data - NetBuffer tempBuffer = new NetBuffer(); + IWriteMessage tempBuffer = new WriteOnlyMessage(); sentEvents = new List(); @@ -22,7 +21,7 @@ namespace Barotrauma.Networking foreach (NetEntityEvent e in eventsToSync) { //write into a temporary buffer so we can write the length before the actual data - NetBuffer tempEventBuffer = new NetBuffer(); + IWriteMessage tempEventBuffer = new WriteOnlyMessage(); try { WriteEvent(tempEventBuffer, e, recipient); @@ -67,7 +66,7 @@ namespace Barotrauma.Networking tempBuffer.Write(e.EntityID); tempBuffer.Write((byte)tempEventBuffer.LengthBytes); - tempBuffer.Write(tempEventBuffer); + tempBuffer.Write(tempEventBuffer.Buffer, 0, tempEventBuffer.LengthBytes); tempBuffer.WritePadBits(); sentEvents.Add(e); @@ -78,10 +77,10 @@ namespace Barotrauma.Networking { msg.Write(eventsToSync[0].ID); msg.Write((byte)eventCount); - msg.Write(tempBuffer); + msg.Write(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); } } - protected abstract void WriteEvent(NetBuffer buffer, NetEntityEvent entityEvent, Client recipient = null); + protected abstract void WriteEvent(IWriteMessage buffer, NetEntityEvent entityEvent, Client recipient = null); } } diff --git a/Barotrauma/BarotraumaShared/Source/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/Source/Networking/NetworkMember.cs index b4d1b39a3..f85e882a8 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/NetworkMember.cs @@ -1,5 +1,4 @@ using Barotrauma.Items.Components; -using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -9,9 +8,6 @@ namespace Barotrauma.Networking { enum ClientPacketHeader { - REQUEST_AUTH, //ask the server if a password is needed, if so we'll get nonce for encryption - REQUEST_STEAMAUTH, //the same as REQUEST_AUTH, but in addition we want to authenticate the player's Steam ID - REQUEST_INIT, //ask the server to give you initialization UPDATE_LOBBY, //update state in lobby UPDATE_INGAME, //update state ingame @@ -64,7 +60,9 @@ namespace Barotrauma.Networking QUERY_STARTGAME, //ask the clients whether they're ready to start STARTGAME, //start a new round - ENDGAME + ENDGAME, + + TRAITOR_MESSAGE } enum ServerNetObject { @@ -78,6 +76,14 @@ namespace Barotrauma.Networking ENTITY_EVENT_INITIAL, } + enum TraitorMessageType + { + Server, + ServerMessageBox, + Objective, + Console + } + enum VoteType { Unknown, @@ -94,6 +100,7 @@ namespace Barotrauma.Networking Banned, Kicked, ServerShutdown, + ServerCrashed, ServerFull, AuthenticationRequired, SteamAuthenticationRequired, @@ -132,13 +139,7 @@ namespace Barotrauma.Networking #if DEBUG public Dictionary messageCount = new Dictionary(); #endif - - public NetPeer NetPeer - { - get; - protected set; - } - + protected string name; protected ServerSettings serverSettings; @@ -154,12 +155,6 @@ namespace Barotrauma.Networking public bool ShowNetStats; - public int Port - { - get; - set; - } - public int TickRate { get { return serverSettings.TickRate; } @@ -205,13 +200,7 @@ namespace Barotrauma.Networking { get { return serverSettings; } } - - public NetPeerConfiguration NetPeerConfiguration - { - get; - protected set; - } - + public bool CanUseRadio(Character sender) { if (sender == null) return false; diff --git a/Barotrauma/BarotraumaShared/Source/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaShared/Source/Networking/OrderChatMessage.cs index 85523d2fa..9892bb379 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/OrderChatMessage.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Text; -using Lidgren.Network; namespace Barotrauma.Networking { diff --git a/Barotrauma/BarotraumaShared/Source/Networking/Primitives/Enums.cs b/Barotrauma/BarotraumaShared/Source/Networking/Primitives/Enums.cs new file mode 100644 index 000000000..7d9151a33 --- /dev/null +++ b/Barotrauma/BarotraumaShared/Source/Networking/Primitives/Enums.cs @@ -0,0 +1,37 @@ +using System; + +namespace Barotrauma.Networking +{ + public enum DeliveryMethod : byte + { + Unreliable = 0x0, + Reliable = 0x1, + ReliableOrdered = 0x2 + } + + public enum ConnectionInitialization : byte + { + //used by all peer implementations + SteamTicketAndVersion = 0x1, + Password = 0x2, + Success = 0x0, + + //used only by SteamP2P implementations + ConnectionStarted = 0x3 + } + + [Flags] + public enum PacketHeader : byte + { + //used by all peer implementations + None = 0x0, + IsCompressed = 0x1, + IsConnectionInitializationStep = 0x2, + + //used only by SteamP2P implementations + IsDisconnectMessage = 0x4, + IsServerMessage = 0x8, + IsHeartbeatMessage = 0x10 + } +} + diff --git a/Barotrauma/BarotraumaShared/Source/Networking/Primitives/Message/IReadMessage.cs b/Barotrauma/BarotraumaShared/Source/Networking/Primitives/Message/IReadMessage.cs new file mode 100644 index 000000000..8a6d8f9e4 --- /dev/null +++ b/Barotrauma/BarotraumaShared/Source/Networking/Primitives/Message/IReadMessage.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Barotrauma.Networking +{ + public interface IReadMessage + { + bool ReadBoolean(); + void ReadPadBits(); + byte ReadByte(); + UInt16 ReadUInt16(); + Int16 ReadInt16(); + UInt32 ReadUInt32(); + Int32 ReadInt32(); + UInt64 ReadUInt64(); + Int64 ReadInt64(); + Single ReadSingle(); + Double ReadDouble(); + UInt32 ReadVariableUInt32(); + String ReadString(); + int ReadRangedInteger(int min, int max); + Single ReadRangedSingle(Single min, Single max, int bitCount); + byte[] ReadBytes(int numberOfBytes); + + int BitPosition { get; set; } + int BytePosition { get; } + byte[] Buffer { get; } + int LengthBits { get; set; } + int LengthBytes { get; } + + NetworkConnection Sender { get; } + } +} diff --git a/Barotrauma/BarotraumaShared/Source/Networking/Primitives/Message/IWriteMessage.cs b/Barotrauma/BarotraumaShared/Source/Networking/Primitives/Message/IWriteMessage.cs new file mode 100644 index 000000000..4e5e87453 --- /dev/null +++ b/Barotrauma/BarotraumaShared/Source/Networking/Primitives/Message/IWriteMessage.cs @@ -0,0 +1,33 @@ +using System; + +namespace Barotrauma.Networking +{ + public interface IWriteMessage + { + void Write(bool val); + void WritePadBits(); + void Write(byte val); + void Write(Int16 val); + void Write(UInt16 val); + void Write(Int32 val); + void Write(UInt32 val); + void Write(Int64 val); + void Write(UInt64 val); + void Write(Single val); + void Write(Double val); + void WriteVariableUInt32(UInt32 val); + void Write(string val); + void WriteRangedIntegerDeprecated(int min, int max, int val); //TODO: remove this, val should be first parameter >:( + void WriteRangedInteger(int val, int min, int max); + void WriteRangedSingle(Single val, Single min, Single max, int bitCount); + void Write(byte[] val, int startIndex, int length); + + void PrepareForSending(ref byte[] outBuf, out bool isCompressed, out int outLength); + + int BitPosition { get; set; } + int BytePosition { get; } + byte[] Buffer { get; } + int LengthBits { get; set; } + int LengthBytes { get; } + } +} diff --git a/Barotrauma/BarotraumaShared/Source/Networking/Primitives/Message/Message.cs b/Barotrauma/BarotraumaShared/Source/Networking/Primitives/Message/Message.cs new file mode 100644 index 000000000..d9b84ab1d --- /dev/null +++ b/Barotrauma/BarotraumaShared/Source/Networking/Primitives/Message/Message.cs @@ -0,0 +1,955 @@ +using Lidgren.Network; +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Runtime.InteropServices; +using System.Text; + +namespace Barotrauma.Networking +{ + public static class MsgConstants + { + public const int MTU = 1200; + public const int CompressionThreshold = 1000; + public const int InitialBufferSize = 256; + public const int BufferOverAllocateAmount = 4; + } + + /// + /// Utility struct for writing Singles + /// + [StructLayout(LayoutKind.Explicit)] + public struct SingleUIntUnion + { + /// + /// Value as a 32 bit float + /// + [FieldOffset(0)] + public float SingleValue; + + /// + /// Value as an unsigned 32 bit integer + /// + [FieldOffset(0)] + public uint UIntValue; + } + + internal static class MsgWriter + { + internal static void Write(ref byte[] buf, ref int bitPos, bool val) + { +#if DEBUG + int resetPos = bitPos; +#endif + + EnsureBufferSize(ref buf, bitPos + 1); + + int bytePos = bitPos / 8; + int bitOffset = bitPos % 8; + byte bitFlag = (byte)(1 << bitOffset); + byte bitMask = (byte)((~bitFlag) & 0xff); + buf[bytePos] &= bitMask; + if (val) buf[bytePos] |= bitFlag; + bitPos++; + +#if DEBUG + bool testVal = MsgReader.ReadBoolean(buf, ref resetPos); + if (testVal != val || resetPos != bitPos) + { + DebugConsole.ThrowError("Boolean written incorrectly! " + testVal + ", " + val + "; " + resetPos + ", " + bitPos); + } +#endif + } + + internal static void WritePadBits(ref byte[] buf, ref int bitPos) + { + int bitOffset = bitPos % 8; + bitPos += ((8 - bitOffset) % 8); + EnsureBufferSize(ref buf, bitPos); + } + + internal static void Write(ref byte[] buf, ref int bitPos, byte val) + { + EnsureBufferSize(ref buf, bitPos + 8); + NetBitWriter.WriteByte(val, 8, buf, bitPos); + bitPos += 8; + } + + internal static void Write(ref byte[] buf, ref int bitPos, UInt16 val) + { + EnsureBufferSize(ref buf, bitPos + 16); + NetBitWriter.WriteUInt16(val, 16, buf, bitPos); + bitPos += 16; + } + + internal static void Write(ref byte[] buf, ref int bitPos, Int16 val) + { + EnsureBufferSize(ref buf, bitPos + 16); + NetBitWriter.WriteUInt16((UInt16)val, 16, buf, bitPos); + bitPos += 16; + } + + internal static void Write(ref byte[] buf, ref int bitPos, UInt32 val) + { + EnsureBufferSize(ref buf, bitPos + 32); + NetBitWriter.WriteUInt32(val, 32, buf, bitPos); + bitPos += 32; + } + + internal static void Write(ref byte[] buf, ref int bitPos, Int32 val) + { + EnsureBufferSize(ref buf, bitPos + 32); + NetBitWriter.WriteUInt32((UInt32)val, 32, buf, bitPos); + bitPos += 32; + } + + internal static void Write(ref byte[] buf, ref int bitPos, UInt64 val) + { + EnsureBufferSize(ref buf, bitPos + 64); + NetBitWriter.WriteUInt64(val, 64, buf, bitPos); + bitPos += 64; + } + + internal static void Write(ref byte[] buf, ref int bitPos, Int64 val) + { + EnsureBufferSize(ref buf, bitPos + 64); + NetBitWriter.WriteUInt64((UInt64)val, 64, buf, bitPos); + bitPos += 64; + } + + internal static void Write(ref byte[] buf, ref int bitPos, Single val) + { + // Use union to avoid BitConverter.GetBytes() which allocates memory on the heap + SingleUIntUnion su; + su.UIntValue = 0; // must initialize every member of the union to avoid warning + su.SingleValue = val; + + EnsureBufferSize(ref buf, bitPos + 32); + + NetBitWriter.WriteUInt32(su.UIntValue, 32, buf, bitPos); + bitPos += 32; + } + + internal static void Write(ref byte[] buf, ref int bitPos, Double val) + { + EnsureBufferSize(ref buf, bitPos + 64); + + byte[] bytes = BitConverter.GetBytes(val); + WriteBytes(ref buf, ref bitPos, bytes, 0, bytes.Length); + bitPos += 64; + } + internal static void Write(ref byte[] buf, ref int bitPos, string val) + { + if (string.IsNullOrEmpty(val)) + { + WriteVariableUInt32(ref buf, ref bitPos, (uint)0); + return; + } + + byte[] bytes = Encoding.UTF8.GetBytes(val); + WriteVariableUInt32(ref buf, ref bitPos, (uint)bytes.Length); + WriteBytes(ref buf, ref bitPos, bytes, 0, bytes.Length); + } + + internal static int WriteVariableUInt32(ref byte[] buf, ref int bitPos, uint value) + { + int retval = 1; + uint num1 = (uint)value; + while (num1 >= 0x80) + { + Write(ref buf, ref bitPos, (byte)(num1 | 0x80)); + num1 = num1 >> 7; + retval++; + } + Write(ref buf, ref bitPos, (byte)num1); + return retval; + } + + internal static void WriteRangedInteger(ref byte[] buf, ref int bitPos, int val, int min, int max) + { + uint range = (uint)(max - min); + int numberOfBits = NetUtility.BitsToHoldUInt(range); + + EnsureBufferSize(ref buf, bitPos + numberOfBits); + + uint rvalue = (uint)(val - min); + NetBitWriter.WriteUInt32(rvalue, numberOfBits, buf, bitPos); + bitPos += numberOfBits; + } + + internal static void WriteRangedSingle(ref byte[] buf, ref int bitPos, Single val, Single min, Single max, int numberOfBits) + { + float range = max - min; + float unit = ((val - min) / range); + int maxVal = (1 << numberOfBits) - 1; + + EnsureBufferSize(ref buf, bitPos + numberOfBits); + + NetBitWriter.WriteUInt32((UInt32)((float)maxVal * unit), numberOfBits, buf, bitPos); + bitPos += numberOfBits; + } + + internal static void WriteBytes(ref byte[] buf, ref int bitPos, byte[] val, int pos, int length) + { + EnsureBufferSize(ref buf, bitPos + length * 8); + NetBitWriter.WriteBytes(val, pos, length, buf, bitPos); + bitPos += length * 8; + } + + internal static void EnsureBufferSize(ref byte[] buf, int numberOfBits) + { + int byteLen = ((numberOfBits + 7) >> 3); + if (buf == null) + { + buf = new byte[byteLen + MsgConstants.BufferOverAllocateAmount]; + return; + } + if (buf.Length < byteLen) + { + Array.Resize(ref buf, byteLen + MsgConstants.BufferOverAllocateAmount); + } + } + } + + internal static class MsgReader + { + internal static bool ReadBoolean(byte[] buf, ref int bitPos) + { + byte retval = NetBitWriter.ReadByte(buf, 1, bitPos); + bitPos++; + return (retval > 0 ? true : false); + } + + internal static void ReadPadBits(byte[] buf, ref int bitPos) + { + int bitOffset = bitPos % 8; + bitPos += (8 - bitOffset) % 8; + } + + internal static byte ReadByte(byte[] buf, ref int bitPos) + { + byte retval = NetBitWriter.ReadByte(buf, 8, bitPos); + bitPos += 8; + return retval; + } + + internal static UInt16 ReadUInt16(byte[] buf, ref int bitPos) + { + uint retval = NetBitWriter.ReadUInt16(buf, 16, bitPos); + bitPos += 16; + return (ushort)retval; + } + + internal static Int16 ReadInt16(byte[] buf, ref int bitPos) + { + return (Int16)ReadUInt16(buf, ref bitPos); + } + + internal static UInt32 ReadUInt32(byte[] buf, ref int bitPos) + { + uint retval = NetBitWriter.ReadUInt32(buf, 32, bitPos); + bitPos += 32; + return retval; + } + + internal static Int32 ReadInt32(byte[] buf, ref int bitPos) + { + return (Int32)ReadUInt32(buf, ref bitPos); + } + + internal static UInt64 ReadUInt64(byte[] buf, ref int bitPos) + { + ulong low = NetBitWriter.ReadUInt32(buf, 32, bitPos); + bitPos += 32; + ulong high = NetBitWriter.ReadUInt32(buf, 32, bitPos); + ulong retval = low + (high << 32); + bitPos += 32; + return retval; + } + + internal static Int64 ReadInt64(byte[] buf, ref int bitPos) + { + return (Int64)ReadUInt64(buf, ref bitPos); + } + + internal static Single ReadSingle(byte[] buf, ref int bitPos) + { + if ((bitPos & 7) == 0) // read directly + { + float retval = BitConverter.ToSingle(buf, bitPos >> 3); + bitPos += 32; + return retval; + } + + byte[] bytes = ReadBytes(buf, ref bitPos, 4); + return BitConverter.ToSingle(bytes, 0); + } + + internal static Double ReadDouble(byte[] buf, ref int bitPos) + { + if ((bitPos & 7) == 0) // read directly + { + // read directly + double retval = BitConverter.ToDouble(buf, bitPos >> 3); + bitPos += 64; + return retval; + } + + byte[] bytes = ReadBytes(buf, ref bitPos, 8); + return BitConverter.ToDouble(bytes, 0); + } + + internal static UInt32 ReadVariableUInt32(byte[] buf, ref int bitPos) + { + int bitLength = buf.Length * 8; + + int num1 = 0; + int num2 = 0; + while (bitLength - bitPos >= 8) + { + byte num3 = ReadByte(buf, ref bitPos); + num1 |= (num3 & 0x7f) << num2; + num2 += 7; + if ((num3 & 0x80) == 0) + return (uint)num1; + } + + // ouch; failed to find enough bytes; malformed variable length number? + return (uint)num1; + } + + internal static String ReadString(byte[] buf, ref int bitPos) + { + int bitLength = buf.Length * 8; + int byteLen = (int)ReadVariableUInt32(buf, ref bitPos); + + if (byteLen <= 0) { return String.Empty; } + + if ((ulong)(bitLength - bitPos) < ((ulong)byteLen * 8)) + { + // not enough data + return null; + } + + if ((bitPos & 7) == 0) + { + // read directly + string retval = System.Text.Encoding.UTF8.GetString(buf, bitPos >> 3, byteLen); + bitPos += (8 * byteLen); + return retval; + } + + byte[] bytes = ReadBytes(buf, ref bitPos, byteLen); + return System.Text.Encoding.UTF8.GetString(bytes, 0, bytes.Length); + } + + internal static int ReadRangedInteger(byte[] buf, ref int bitPos, int min, int max) + { + uint range = (uint)(max - min); + int numBits = NetUtility.BitsToHoldUInt(range); + + uint rvalue = NetBitWriter.ReadUInt32(buf, numBits, bitPos); + bitPos += numBits; + + return (int)(min + rvalue); + } + + internal static Single ReadRangedSingle(byte[] buf, ref int bitPos, Single min, Single max, int bitCount) + { + int maxInt = (1 << bitCount) - 1; + int intVal = ReadRangedInteger(buf, ref bitPos, 0, maxInt); + Single range = max - min; + return min + (range * ((Single)intVal) / ((Single)maxInt)); + } + + internal static byte[] ReadBytes(byte[] buf, ref int bitPos, int numberOfBytes) + { + byte[] retval = new byte[numberOfBytes]; + NetBitWriter.ReadBytes(buf, numberOfBytes, bitPos, retval, 0); + bitPos += (8 * numberOfBytes); + return retval; + } + } + + public class WriteOnlyMessage : IWriteMessage + { + private byte[] buf = new byte[MsgConstants.InitialBufferSize]; + private int seekPos = 0; + private int lengthBits = 0; + + public int BitPosition + { + get + { + return seekPos; + } + set + { + seekPos = value; + } + } + + public int BytePosition + { + get + { + return seekPos / 8; + } + } + + public byte[] Buffer + { + get + { + return buf; + } + } + + public int LengthBits + { + get + { + lengthBits = seekPos > lengthBits ? seekPos : lengthBits; + return lengthBits; + } + set + { + lengthBits = value; + seekPos = seekPos > lengthBits ? lengthBits : seekPos; + } + } + + public int LengthBytes + { + get + { + return (LengthBits + ((8 - (LengthBits % 8)) % 8)) / 8; + } + } + + public void Write(bool val) + { + MsgWriter.Write(ref buf, ref seekPos, val); + } + + public void WritePadBits() + { + MsgWriter.WritePadBits(ref buf, ref seekPos); + } + + public void Write(byte val) + { + MsgWriter.Write(ref buf, ref seekPos, val); + } + + public void Write(UInt16 val) + { + MsgWriter.Write(ref buf, ref seekPos, val); + } + + public void Write(Int16 val) + { + MsgWriter.Write(ref buf, ref seekPos, val); + } + + public void Write(UInt32 val) + { + MsgWriter.Write(ref buf, ref seekPos, val); + } + + public void Write(Int32 val) + { + MsgWriter.Write(ref buf, ref seekPos, val); + } + + public void Write(UInt64 val) + { + MsgWriter.Write(ref buf, ref seekPos, val); + } + + public void Write(Int64 val) + { + MsgWriter.Write(ref buf, ref seekPos, val); + } + + public void Write(Single val) + { + MsgWriter.Write(ref buf, ref seekPos, val); + } + + public void Write(Double val) + { + MsgWriter.Write(ref buf, ref seekPos, val); + } + + public void WriteVariableUInt32(UInt32 val) + { + MsgWriter.WriteVariableUInt32(ref buf, ref seekPos, val); + } + + public void Write(String val) + { + MsgWriter.Write(ref buf, ref seekPos, val); + } + + public void WriteRangedIntegerDeprecated(int min, int max, int val) + { + MsgWriter.WriteRangedInteger(ref buf, ref seekPos, val, min, max); + } + + public void WriteRangedInteger(int val, int min, int max) + { + MsgWriter.WriteRangedInteger(ref buf, ref seekPos, val, min, max); + } + + public void WriteRangedSingle(Single val, Single min, Single max, int bitCount) + { + MsgWriter.WriteRangedSingle(ref buf, ref seekPos, val, min, max, bitCount); + } + + public void Write(byte[] val, int startPos, int length) + { + MsgWriter.WriteBytes(ref buf, ref seekPos, val, startPos, length); + } + + public void PrepareForSending(ref byte[] outBuf, out bool isCompressed, out int length) + { + if (LengthBytes <= MsgConstants.CompressionThreshold) + { + isCompressed = false; + if (LengthBytes > outBuf.Length) { Array.Resize(ref outBuf, LengthBytes); } + Array.Copy(buf, outBuf, LengthBytes); + length = LengthBytes; + } + else + { + using (MemoryStream output = new MemoryStream()) + { + using (DeflateStream dstream = new DeflateStream(output, CompressionLevel.Fastest)) + { + dstream.Write(buf, 0, LengthBytes); + } + + byte[] compressedBuf = output.ToArray(); + //don't send the data as compressed if the data takes up more space after compression + //(which may happen when sending a sub/save file that's already been compressed with a better compression ratio) + if (compressedBuf.Length >= outBuf.Length) + { + isCompressed = false; + if (LengthBytes > outBuf.Length) { Array.Resize(ref outBuf, LengthBytes); } + Array.Copy(buf, outBuf, LengthBytes); + length = LengthBytes; + } + else + { + isCompressed = true; + if (compressedBuf.Length > outBuf.Length) { Array.Resize(ref outBuf, compressedBuf.Length); } + Array.Copy(compressedBuf, outBuf, compressedBuf.Length); + length = compressedBuf.Length; + DebugConsole.NewMessage("Compressed message: " + LengthBytes + " to " + length); + } + } + } + } + } + + public class ReadOnlyMessage : IReadMessage + { + private byte[] buf; + private int seekPos = 0; + private int lengthBits = 0; + + public int BitPosition + { + get + { + return seekPos; + } + set + { + seekPos = value; + } + } + + public int BytePosition + { + get + { + return seekPos / 8; + } + } + + public byte[] Buffer + { + get + { + return buf; + } + } + + public int LengthBits + { + get + { + lengthBits = seekPos > lengthBits ? seekPos : lengthBits; + return lengthBits; + } + set + { + lengthBits = value; + seekPos = seekPos > lengthBits ? lengthBits : seekPos; + } + } + + public int LengthBytes + { + get + { + return lengthBits / 8; + } + } + + public NetworkConnection Sender { get; private set; } + + public ReadOnlyMessage(byte[] inBuf, bool isCompressed, int startPos, int inLength, NetworkConnection sender) + { + Sender = sender; + if (isCompressed) + { + byte[] decompressedData; + using (MemoryStream input = new MemoryStream(inBuf, startPos, inLength)) + { + using (MemoryStream output = new MemoryStream()) + { + using (DeflateStream dstream = new DeflateStream(input, CompressionMode.Decompress)) + { + dstream.CopyTo(output); + } + decompressedData = output.ToArray(); + } + } + buf = new byte[decompressedData.Length]; + Array.Copy(decompressedData, 0, buf, 0, decompressedData.Length); + lengthBits = decompressedData.Length * 8; + DebugConsole.NewMessage("Decompressing message: " + inLength + " to " + LengthBytes); + } + else + { + buf = new byte[inBuf.Length]; + Array.Copy(inBuf, startPos, buf, 0, inLength); + lengthBits = inLength * 8; + } + seekPos = 0; + } + + public bool ReadBoolean() + { + return MsgReader.ReadBoolean(buf, ref seekPos); + } + + public void ReadPadBits() + { + MsgReader.ReadPadBits(buf, ref seekPos); + } + + public byte ReadByte() + { + return MsgReader.ReadByte(buf, ref seekPos); + } + + public UInt16 ReadUInt16() + { + return MsgReader.ReadUInt16(buf, ref seekPos); + } + + public Int16 ReadInt16() + { + return MsgReader.ReadInt16(buf, ref seekPos); + } + + public UInt32 ReadUInt32() + { + return MsgReader.ReadUInt32(buf, ref seekPos); + } + + public Int32 ReadInt32() + { + return MsgReader.ReadInt32(buf, ref seekPos); + } + + public UInt64 ReadUInt64() + { + return MsgReader.ReadUInt64(buf, ref seekPos); + } + + public Int64 ReadInt64() + { + return MsgReader.ReadInt64(buf, ref seekPos); + } + + public Single ReadSingle() + { + return MsgReader.ReadSingle(buf, ref seekPos); + } + + public Double ReadDouble() + { + return MsgReader.ReadDouble(buf, ref seekPos); + } + + public UInt32 ReadVariableUInt32() + { + return MsgReader.ReadVariableUInt32(buf, ref seekPos); + } + + public String ReadString() + { + return MsgReader.ReadString(buf, ref seekPos); + } + + public int ReadRangedInteger(int min, int max) + { + return MsgReader.ReadRangedInteger(buf, ref seekPos, min, max); + } + + public Single ReadRangedSingle(Single min, Single max, int bitCount) + { + return MsgReader.ReadRangedSingle(buf, ref seekPos, min, max, bitCount); + } + + public byte[] ReadBytes(int numberOfBytes) + { + return MsgReader.ReadBytes(buf, ref seekPos, numberOfBytes); + } + } + + public class ReadWriteMessage : IWriteMessage, IReadMessage + { + private byte[] buf = new byte[MsgConstants.InitialBufferSize]; + private int seekPos = 0; + private int lengthBits = 0; + + public int BitPosition + { + get + { + return seekPos; + } + set + { + seekPos = value; + } + } + + public int BytePosition + { + get + { + return seekPos / 8; + } + } + + public byte[] Buffer + { + get + { + return buf; + } + } + + public int LengthBits + { + get + { + lengthBits = seekPos > lengthBits ? seekPos : lengthBits; + return lengthBits; + } + set + { + lengthBits = value; + seekPos = seekPos > lengthBits ? lengthBits : seekPos; + } + } + + public int LengthBytes + { + get + { + return (LengthBits + ((8 - (LengthBits % 8)) % 8)) / 8; + } + } + + public NetworkConnection Sender { get { return null; } } + + public void Write(bool val) + { + MsgWriter.Write(ref buf, ref seekPos, val); + } + + public void WritePadBits() + { + MsgWriter.WritePadBits(ref buf, ref seekPos); + } + + public void Write(byte val) + { + MsgWriter.Write(ref buf, ref seekPos, val); + } + + public void Write(UInt16 val) + { + MsgWriter.Write(ref buf, ref seekPos, val); + } + + public void Write(Int16 val) + { + MsgWriter.Write(ref buf, ref seekPos, val); + } + + public void Write(UInt32 val) + { + MsgWriter.Write(ref buf, ref seekPos, val); + } + + public void Write(Int32 val) + { + MsgWriter.Write(ref buf, ref seekPos, val); + } + + public void Write(UInt64 val) + { + MsgWriter.Write(ref buf, ref seekPos, val); + } + + public void Write(Int64 val) + { + MsgWriter.Write(ref buf, ref seekPos, val); + } + + public void Write(Single val) + { + MsgWriter.Write(ref buf, ref seekPos, val); + } + + public void Write(Double val) + { + MsgWriter.Write(ref buf, ref seekPos, val); + } + + public void WriteVariableUInt32(UInt32 val) + { + MsgWriter.WriteVariableUInt32(ref buf, ref seekPos, val); + } + + public void Write(String val) + { + MsgWriter.Write(ref buf, ref seekPos, val); + } + + public void WriteRangedIntegerDeprecated(int min, int max, int val) + { + MsgWriter.WriteRangedInteger(ref buf, ref seekPos, val, min, max); + } + + public void WriteRangedInteger(int val, int min, int max) + { + MsgWriter.WriteRangedInteger(ref buf, ref seekPos, val, min, max); + } + + public void WriteRangedSingle(Single val, Single min, Single max, int bitCount) + { + MsgWriter.WriteRangedSingle(ref buf, ref seekPos, val, min, max, bitCount); + } + + public void Write(byte[] val, int startPos, int length) + { + MsgWriter.WriteBytes(ref buf, ref seekPos, val, startPos, length); + } + + public bool ReadBoolean() + { + return MsgReader.ReadBoolean(buf, ref seekPos); + } + + public void ReadPadBits() + { + MsgReader.ReadPadBits(buf, ref seekPos); + } + + public byte ReadByte() + { + return MsgReader.ReadByte(buf, ref seekPos); + } + + public UInt16 ReadUInt16() + { + return MsgReader.ReadUInt16(buf, ref seekPos); + } + + public Int16 ReadInt16() + { + return MsgReader.ReadInt16(buf, ref seekPos); + } + + public UInt32 ReadUInt32() + { + return MsgReader.ReadUInt32(buf, ref seekPos); + } + + public Int32 ReadInt32() + { + return MsgReader.ReadInt32(buf, ref seekPos); + } + + public UInt64 ReadUInt64() + { + return MsgReader.ReadUInt64(buf, ref seekPos); + } + + public Int64 ReadInt64() + { + return MsgReader.ReadInt64(buf, ref seekPos); + } + + public Single ReadSingle() + { + return MsgReader.ReadSingle(buf, ref seekPos); + } + + public Double ReadDouble() + { + return MsgReader.ReadDouble(buf, ref seekPos); + } + + public UInt32 ReadVariableUInt32() + { + return MsgReader.ReadVariableUInt32(buf, ref seekPos); + } + + public String ReadString() + { + return MsgReader.ReadString(buf, ref seekPos); + } + + public int ReadRangedInteger(int min, int max) + { + return MsgReader.ReadRangedInteger(buf, ref seekPos, min, max); + } + + public Single ReadRangedSingle(Single min, Single max, int bitCount) + { + return MsgReader.ReadRangedSingle(buf, ref seekPos, min, max, bitCount); + } + + public byte[] ReadBytes(int numberOfBytes) + { + return MsgReader.ReadBytes(buf, ref seekPos, numberOfBytes); + } + + public void PrepareForSending(ref byte[] outBuf, out bool isCompressed, out int outLength) + { + throw new InvalidOperationException("ReadWriteMessages are not to be sent"); + } + } +} diff --git a/Barotrauma/BarotraumaShared/Source/Networking/Primitives/NetworkConnection/LidgrenConnection.cs b/Barotrauma/BarotraumaShared/Source/Networking/Primitives/NetworkConnection/LidgrenConnection.cs new file mode 100644 index 000000000..1825dd108 --- /dev/null +++ b/Barotrauma/BarotraumaShared/Source/Networking/Primitives/NetworkConnection/LidgrenConnection.cs @@ -0,0 +1,37 @@ +using System; +using System.Net; +using Lidgren.Network; + +namespace Barotrauma.Networking +{ + public class LidgrenConnection : NetworkConnection + { + public NetConnection NetConnection { get; private set; } + + public IPEndPoint IPEndPoint => NetConnection.RemoteEndPoint; + + public string IPString + { + get + { + return IPEndPoint.Address.IsIPv4MappedToIPv6 ? IPEndPoint.Address.MapToIPv4().ToString() : IPEndPoint.Address.ToString(); + } + } + + public UInt16 Port + { + get + { + return (UInt16)IPEndPoint.Port; + } + } + + public LidgrenConnection(string name, NetConnection netConnection, UInt64 steamId) + { + Name = name; + NetConnection = netConnection; + SteamID = steamId; + EndPointString = IPString; + } + } +} diff --git a/Barotrauma/BarotraumaShared/Source/Networking/Primitives/NetworkConnection/NetworkConnection.cs b/Barotrauma/BarotraumaShared/Source/Networking/Primitives/NetworkConnection/NetworkConnection.cs new file mode 100644 index 000000000..f1e377a7d --- /dev/null +++ b/Barotrauma/BarotraumaShared/Source/Networking/Primitives/NetworkConnection/NetworkConnection.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Net; + +namespace Barotrauma.Networking +{ + public enum NetworkConnectionStatus + { + Connected = 0x1, + Disconnected = 0x2 + } + + public abstract class NetworkConnection + { + public string Name; + + public UInt64 SteamID + { + get; + protected set; + } + + public string EndPointString + { + get; + protected set; + } + + public NetworkConnectionStatus Status = NetworkConnectionStatus.Disconnected; + } +} diff --git a/Barotrauma/BarotraumaShared/Source/Networking/Primitives/NetworkConnection/SteamP2PConnection.cs b/Barotrauma/BarotraumaShared/Source/Networking/Primitives/NetworkConnection/SteamP2PConnection.cs new file mode 100644 index 000000000..5b27c958d --- /dev/null +++ b/Barotrauma/BarotraumaShared/Source/Networking/Primitives/NetworkConnection/SteamP2PConnection.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Barotrauma.Networking +{ + public class SteamP2PConnection : NetworkConnection + { + public double Timeout = 0.0; + + public SteamP2PConnection(string name, UInt64 steamId) + { + SteamID = steamId; + EndPointString = SteamID.ToString(); + Name = name; + Heartbeat(); + } + + public void Decay(float deltaTime) + { + Timeout -= deltaTime; + } + + public void Heartbeat() + { + Timeout = 20.0; + } + } +} diff --git a/Barotrauma/BarotraumaShared/Source/Networking/RespawnManager.cs b/Barotrauma/BarotraumaShared/Source/Networking/RespawnManager.cs index 6bcd22d24..00bf1cd69 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/RespawnManager.cs @@ -282,7 +282,7 @@ namespace Barotrauma.Networking { RespawnCharactersProjSpecific(); } - + public Vector2 FindSpawnPos() { if (Level.Loaded == null || Submarine.MainSub == null) { return Vector2.Zero; } @@ -310,12 +310,11 @@ namespace Barotrauma.Networking //make sure there aren't any walls too close var tooCloseCells = Level.Loaded.GetTooCloseCells(potentialSpawnPos.Position.ToVector2(), Math.Max(minWidth, minHeight)); if (tooCloseCells.Any()) { continue; } - + //make sure the spawnpoint is far enough from other subs foreach (Submarine sub in Submarine.Loaded) { if (sub == RespawnShuttle || RespawnShuttle.DockedTo.Contains(sub)) { continue; } - float minDist = Math.Max(Math.Max(minWidth, minHeight) + Math.Max(sub.Borders.Width, sub.Borders.Height), 10000.0f); if (Vector2.DistanceSquared(sub.WorldPosition, potentialSpawnPos.Position.ToVector2()) < minDist * minDist) { diff --git a/Barotrauma/BarotraumaShared/Source/Networking/ServerLog.cs b/Barotrauma/BarotraumaShared/Source/Networking/ServerLog.cs index 70ff5dcba..02606590c 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/ServerLog.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/ServerLog.cs @@ -6,7 +6,7 @@ using System.Linq; namespace Barotrauma.Networking { - partial class ServerLog + public partial class ServerLog { private struct LogMessage { diff --git a/Barotrauma/BarotraumaShared/Source/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/Source/Networking/ServerSettings.cs index fec6d1e0f..479d87f3b 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/ServerSettings.cs @@ -1,5 +1,4 @@ -using Lidgren.Network; -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.ComponentModel; @@ -14,21 +13,21 @@ using System.Xml.Linq; namespace Barotrauma.Networking { - enum SelectionMode + public enum SelectionMode { Manual = 0, Random = 1, Vote = 2 } - enum YesNoMaybe + public enum YesNoMaybe { No = 0, Maybe = 1, Yes = 2 } - enum BotSpawnMode + public enum BotSpawnMode { Normal, Fill } - + partial class ServerSettings : ISerializableEntity { public const string SettingsFile = "serversettings.xml"; @@ -57,25 +56,17 @@ namespace Barotrauma.Networking public class SavedClientPermission { - public readonly string IP; + public readonly string EndPoint; public readonly ulong SteamID; public readonly string Name; public List PermittedCommands; public ClientPermissions Permissions; - public SavedClientPermission(string name, IPAddress ip, ClientPermissions permissions, List permittedCommands) + public SavedClientPermission(string name, string endpoint, ClientPermissions permissions, List permittedCommands) { this.Name = name; - this.IP = ip.IsIPv4MappedToIPv6 ? ip.MapToIPv4().ToString() : ip.ToString(); - this.Permissions = permissions; - this.PermittedCommands = permittedCommands; - } - public SavedClientPermission(string name, string ip, ClientPermissions permissions, List permittedCommands) - { - this.Name = name; - this.IP = ip; - + this.EndPoint = endpoint; this.Permissions = permissions; this.PermittedCommands = permittedCommands; } @@ -139,9 +130,9 @@ namespace Barotrauma.Networking } } - public void Read(NetBuffer msg) + public void Read(IReadMessage msg) { - long oldPos = msg.Position; + int oldPos = msg.BitPosition; UInt32 size = msg.ReadVariableUInt32(); float x; float y; float z; float w; @@ -152,7 +143,7 @@ namespace Barotrauma.Networking { case "float": if (size != 4) break; - property.SetValue(parentObject, msg.ReadFloat()); + property.SetValue(parentObject, msg.ReadSingle()); return; case "int": if (size != 4) break; @@ -160,23 +151,23 @@ namespace Barotrauma.Networking return; case "vector2": if (size != 8) break; - x = msg.ReadFloat(); - y = msg.ReadFloat(); + x = msg.ReadSingle(); + y = msg.ReadSingle(); property.SetValue(parentObject, new Vector2(x, y)); return; case "vector3": if (size != 12) break; - x = msg.ReadFloat(); - y = msg.ReadFloat(); - z = msg.ReadFloat(); + x = msg.ReadSingle(); + y = msg.ReadSingle(); + z = msg.ReadSingle(); property.SetValue(parentObject, new Vector3(x, y, z)); return; case "vector4": if (size != 16) break; - x = msg.ReadFloat(); - y = msg.ReadFloat(); - z = msg.ReadFloat(); - w = msg.ReadFloat(); + x = msg.ReadSingle(); + y = msg.ReadSingle(); + z = msg.ReadSingle(); + w = msg.ReadSingle(); property.SetValue(parentObject, new Vector4(x, y, z, w)); return; case "color": @@ -196,17 +187,17 @@ namespace Barotrauma.Networking property.SetValue(parentObject, new Rectangle(ix, iy, width, height)); return; default: - msg.Position = oldPos; //reset position to properly read the string + msg.BitPosition = oldPos; //reset position to properly read the string string incVal = msg.ReadString(); property.TrySetValue(parentObject, incVal); return; } //size didn't match: skip this - msg.Position += 8 * size; + msg.BitPosition += (int)(8 * size); } - public void Write(NetBuffer msg, object overrideValue = null) + public void Write(IWriteMessage msg, object overrideValue = null) { if (overrideValue == null) overrideValue = property.GetValue(parentObject); switch (typeString) @@ -287,7 +278,7 @@ namespace Barotrauma.Networking ServerName = serverName; Port = port; QueryPort = queryPort; - //EnableUPnP = enableUPnP; + EnableUPnP = enableUPnP; this.maxPlayers = maxPlayers; this.isPublic = isPublic; @@ -328,7 +319,7 @@ namespace Barotrauma.Networking } } } - + public string ServerName; private string serverMessageText; @@ -346,7 +337,9 @@ namespace Barotrauma.Networking public int Port; public int QueryPort; - + + public bool EnableUPnP; + public ServerLog ServerLog; public Voting Voting; @@ -354,10 +347,10 @@ namespace Barotrauma.Networking public Dictionary MonsterEnabled { get; private set; } public Dictionary ExtraCargo { get; private set; } - + private TimeSpan sparseUpdateInterval = new TimeSpan(0, 0, 0, 3); private float selectedLevelDifficulty; - private string password; + private byte[] password; public float AutoRestartTimer; @@ -370,7 +363,7 @@ namespace Barotrauma.Networking public List ClientPermissions { get; private set; } = new List(); public WhiteList Whitelist { get; private set; } - + [Serialize(20, true)] public int TickRate { @@ -446,7 +439,7 @@ namespace Barotrauma.Networking ServerDetailsChanged = true; } } - + [Serialize(true, true)] public bool EndRoundAtLevelEnd { @@ -514,9 +507,15 @@ namespace Barotrauma.Networking public bool HasPassword { - get { return !string.IsNullOrEmpty(password); } + get { return password != null; } +#if CLIENT + set + { + password = value ? (password ?? new byte[1]) : null; + } +#endif } - + [Serialize(true, true)] public bool AllowVoteKick { @@ -555,7 +554,7 @@ namespace Barotrauma.Networking ServerDetailsChanged = true; } } - + [Serialize(0, true)] public int BotCount { @@ -569,7 +568,8 @@ namespace Barotrauma.Networking get; set; } - + + [Serialize(BotSpawnMode.Normal, true)] public BotSpawnMode BotSpawnMode { get; @@ -588,7 +588,7 @@ namespace Barotrauma.Networking get; set; } - + [Serialize(true, true)] public bool AllowRewiring { @@ -604,6 +604,7 @@ namespace Barotrauma.Networking } private YesNoMaybe traitorsEnabled; + [Serialize(YesNoMaybe.No, true)] public YesNoMaybe TraitorsEnabled { get { return traitorsEnabled; } @@ -615,6 +616,13 @@ namespace Barotrauma.Networking } } + [Serialize(defaultValue: 1, isSaveable: true)] + public int TraitorsMinPlayerCount + { + get; + set; + } + private SelectionMode subSelectionMode; [Serialize(SelectionMode.Manual, true)] public SelectionMode SubSelectionMode @@ -642,7 +650,7 @@ namespace Barotrauma.Networking } public BanList BanList { get; private set; } - + [Serialize(0.6f, true)] public float EndVoteRequiredRatio { @@ -663,7 +671,7 @@ namespace Barotrauma.Networking get; private set; } - + [Serialize(120.0f, true)] public float KickAFKTime { @@ -671,20 +679,6 @@ namespace Barotrauma.Networking private set; } - [Serialize(true, true)] - public bool TraitorUseRatio - { - get; - private set; - } - - [Serialize(0.2f, true)] - public float TraitorRatio - { - get; - private set; - } - private bool karmaEnabled; [Serialize(false, true)] public bool KarmaEnabled @@ -719,7 +713,7 @@ namespace Barotrauma.Networking get; set; } - + public int MaxPlayers { get { return maxPlayers; } @@ -745,26 +739,42 @@ namespace Barotrauma.Networking get; private set; } - + public void SetPassword(string password) { if (string.IsNullOrEmpty(password)) { - this.password = ""; + this.password = null; } else { - this.password = Encoding.UTF8.GetString(NetUtility.ComputeSHAHash(Encoding.UTF8.GetBytes(password))); + this.password = Lidgren.Network.NetUtility.ComputeSHAHash(Encoding.UTF8.GetBytes(password)); } } - public bool IsPasswordCorrect(string input, int nonce) + public static byte[] SaltPassword(byte[] password, int salt) + { + byte[] saltedPw = new byte[password.Length*2]; + for (int i = 0; i < password.Length; i++) + { + saltedPw[(i * 2)] = password[i]; + saltedPw[(i * 2) + 1] = (byte)((salt >> (8 * (i % 4))) & 0xff); + } + saltedPw = Lidgren.Network.NetUtility.ComputeSHAHash(saltedPw); + return saltedPw; + } + + public bool IsPasswordCorrect(byte[] input, int salt) { if (!HasPassword) return true; - string saltedPw = password; - saltedPw = saltedPw + Convert.ToString(nonce); - saltedPw = Encoding.UTF8.GetString(NetUtility.ComputeSHAHash(Encoding.UTF8.GetBytes(saltedPw))); - return input == saltedPw; + byte[] saltedPw = SaltPassword(password, salt); + DebugConsole.NewMessage(ToolBox.ByteArrayToString(input)+" "+ToolBox.ByteArrayToString(saltedPw)); + if (input.Length != saltedPw.Length) return false; + for (int i=0;i @@ -795,7 +805,7 @@ namespace Barotrauma.Networking } } - public void ReadMonsterEnabled(NetBuffer inc) + public void ReadMonsterEnabled(IReadMessage inc) { InitMonstersEnabled(); List monsterNames = MonsterEnabled.Keys.ToList(); @@ -806,7 +816,7 @@ namespace Barotrauma.Networking inc.ReadPadBits(); } - public void WriteMonsterEnabled(NetBuffer msg, Dictionary monsterEnabled = null) + public void WriteMonsterEnabled(IWriteMessage msg, Dictionary monsterEnabled = null) { //monster spawn settings if (monsterEnabled == null) monsterEnabled = MonsterEnabled; @@ -819,7 +829,7 @@ namespace Barotrauma.Networking msg.WritePadBits(); } - public bool ReadExtraCargo(NetBuffer msg) + public bool ReadExtraCargo(IReadMessage msg) { bool changed = false; UInt32 count = msg.ReadUInt32(); @@ -844,7 +854,7 @@ namespace Barotrauma.Networking return changed; } - public void WriteExtraCargo(NetBuffer msg) + public void WriteExtraCargo(IWriteMessage msg) { if (ExtraCargo == null) { diff --git a/Barotrauma/BarotraumaShared/Source/Networking/SteamManager.cs b/Barotrauma/BarotraumaShared/Source/Networking/SteamManager.cs index 937048f9c..04ab2ad26 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/SteamManager.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/SteamManager.cs @@ -9,10 +9,20 @@ namespace Barotrauma.Steam { public const bool USE_STEAM = true; + public const int STEAMP2P_OWNER_PORT = 30000; + public const uint AppID = 602960; - + private Facepunch.Steamworks.Client client; - private Server server; + private Facepunch.Steamworks.Server server; + + private static List initializationErrors = new List(); + public static IEnumerable InitializationErrors + { + get { return initializationErrors; } + } + + public const string MetadataFileName = "filelist.xml"; private Dictionary tagCommonness = new Dictionary() { @@ -63,6 +73,34 @@ namespace Barotrauma.Steam instance = new SteamManager(); } + public static ulong GetSteamID() + { + if (instance == null || !instance.isInitialized) + { + return 0; + } + + if (instance.client != null) + { + return instance.client.SteamId; + } + else if (instance.server != null) + { + return instance.server.SteamId; + } + + return 0; + } + + public static string GetUsername() + { + if (instance == null || !instance.isInitialized || instance.client == null) + { + return ""; + } + return instance.client.Username; + } + public static void OverlayCustomURL(string url) { if (instance == null || !instance.isInitialized || instance.client == null) @@ -146,5 +184,33 @@ namespace Barotrauma.Steam instance.server = null; instance = null; } + + public static UInt64 SteamIDStringToUInt64(string str) + { + if (string.IsNullOrWhiteSpace(str)) { return 0; } + UInt64 retVal; + if (UInt64.TryParse(str, out retVal) && retVal >(1<<52)) { return retVal; } + if (str.ToUpper().IndexOf("STEAM_") != 0) { return 0; } + string[] split = str.Substring(6).Split(':'); + if (split.Length != 3) { return 0; } + + UInt64 universe = 0; UInt64 y = 0; UInt64 accountNumber = 0; + if (!UInt64.TryParse(split[0], out universe)) { return 0; } + if (!UInt64.TryParse(split[1], out y)) { return 0; } + if (!UInt64.TryParse(split[2], out accountNumber)) { return 0; } + + UInt64 accountInstance = 1; UInt64 accountType = 1; + + return (universe << 56) | (accountType << 52) | (accountInstance << 32) | (accountNumber << 1) | y; + } + + public static string SteamIDUInt64ToString(UInt64 uint64) + { + UInt64 y = uint64 & 0x1; + UInt64 accountNumber = (uint64 >> 1) & 0x7fffffff; + UInt64 universe = (uint64 >> 56) & 0xff; + + return "STEAM_" + universe.ToString() + ":" + y.ToString() + ":" + accountNumber.ToString(); + } } } diff --git a/Barotrauma/BarotraumaShared/Source/Networking/Voip/VoipConfig.cs b/Barotrauma/BarotraumaShared/Source/Networking/Voip/VoipConfig.cs index 69b4a4ff2..0c76824fe 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/Voip/VoipConfig.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/Voip/VoipConfig.cs @@ -1,5 +1,4 @@ -using Lidgren.Network; -using System; +using System; using System.Collections.Generic; using System.Text; diff --git a/Barotrauma/BarotraumaShared/Source/Networking/Voip/VoipQueue.cs b/Barotrauma/BarotraumaShared/Source/Networking/Voip/VoipQueue.cs index d7a326702..5eddf719b 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/Voip/VoipQueue.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/Voip/VoipQueue.cs @@ -1,5 +1,4 @@ -using Lidgren.Network; -using System; +using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework; @@ -113,7 +112,7 @@ namespace Barotrauma.Networking outBuf = null; } - public virtual void Write(NetBuffer msg) + public virtual void Write(IWriteMessage msg) { if (!CanSend) throw new Exception("Called Write on a VoipQueue not set up for sending"); @@ -127,7 +126,7 @@ namespace Barotrauma.Networking } } - public virtual bool Read(NetBuffer msg) + public virtual bool Read(IReadMessage msg) { if (!CanReceive) throw new Exception("Called Read on a VoipQueue not set up for receiving"); @@ -138,7 +137,7 @@ namespace Barotrauma.Networking for (int i = 0; i < BUFFER_COUNT; i++) { bufferLengths[i] = msg.ReadByte(); - msg.ReadBytes(buffers[i], 0, bufferLengths[i]); + buffers[i] = msg.ReadBytes(bufferLengths[i]); } newestBufferInd = BUFFER_COUNT - 1; LatestBufferID = incLatestBufferID; @@ -150,7 +149,7 @@ namespace Barotrauma.Networking for (int i = 0; i < BUFFER_COUNT; i++) { byte len = msg.ReadByte(); - msg.Position += len * 8; + msg.BitPosition += len * 8; } return false; } diff --git a/Barotrauma/BarotraumaShared/Source/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/Source/Physics/PhysicsBody.cs index 0e293408b..118717f43 100644 --- a/Barotrauma/BarotraumaShared/Source/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/Source/Physics/PhysicsBody.cs @@ -2,7 +2,6 @@ using FarseerPhysics; using FarseerPhysics.Dynamics; using FarseerPhysics.Factories; -using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; diff --git a/Barotrauma/BarotraumaShared/Source/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/Source/Serialization/SerializableProperty.cs index 9771f33fb..6865d2329 100644 --- a/Barotrauma/BarotraumaShared/Source/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/Source/Serialization/SerializableProperty.cs @@ -12,7 +12,7 @@ using System.Xml.Linq; namespace Barotrauma { [AttributeUsage(AttributeTargets.Property)] - public class Editable : Attribute + class Editable : Attribute { public int MaxLength; public int DecimalCount = 1; @@ -45,7 +45,7 @@ namespace Barotrauma } [AttributeUsage(AttributeTargets.Property)] - public class InGameEditable : Editable + class InGameEditable : Editable { } @@ -166,7 +166,6 @@ namespace Barotrauma try { - switch (typeName) { case "bool": @@ -175,20 +174,26 @@ namespace Barotrauma propertyInfo.SetValue(parentObject, boolValue, null); break; case "int": - int intVal; - if (int.TryParse(value, out intVal)) + if (int.TryParse(value, out int intVal)) { if (TrySetValueWithoutReflection(parentObject, intVal)) { return true; } propertyInfo.SetValue(parentObject, intVal, null); } + else + { + return false; + } break; case "float": - float floatVal; - if (float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out floatVal)) + if (float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out float floatVal)) { if (TrySetValueWithoutReflection(parentObject, floatVal)) { return true; } propertyInfo.SetValue(parentObject, floatVal, null); } + else + { + return false; + } break; case "string": propertyInfo.SetValue(parentObject, value, null); @@ -420,6 +425,9 @@ namespace Barotrauma case "Charge": if (parentObject is PowerContainer powerContainer) { return powerContainer.Charge; } break; + case "Overload": + if (parentObject is PowerTransfer powerTransfer) { return powerTransfer.Overload; } + break; case "AvailableFuel": { if (parentObject is Reactor reactor) { return reactor.AvailableFuel; } } break; @@ -662,5 +670,33 @@ namespace Barotrauma element.SetAttributeValue(property.NameToLowerInvariant, stringValue); } } + + /// + /// Upgrade the properties of an entity saved with an older version of the game. Properties that should be upgraded are defined using "Upgrade" elements in the config file. + /// for example, would force the scale of the entity to 0.5 if it was saved with a version prior to 0.9.2.0. + /// + /// The entity to upgrade + /// The XML element to get the upgrade instructions from (e.g. the config of an item prefab) + /// The game version the entity was saved with + public static void UpgradeGameVersion(ISerializableEntity entity, XElement configElement, Version savedVersion) + { + foreach (XElement subElement in configElement.Elements()) + { + if (subElement.Name.ToString().ToLowerInvariant() != "upgrade") { continue; } + var upgradeVersion = new Version(subElement.GetAttributeString("gameversion", "0.0.0.0")); + if (savedVersion < upgradeVersion) + { + foreach (XAttribute attribute in subElement.Attributes()) + { + string attributeName = attribute.Name.ToString().ToLowerInvariant(); + if (attributeName == "gameversion") { continue; } + if (entity.SerializableProperties.TryGetValue(attributeName, out SerializableProperty property)) + { + property.TrySetValue(entity, attribute.Value); + } + } + } + } + } } } diff --git a/Barotrauma/BarotraumaShared/Source/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/Source/Serialization/XMLExtensions.cs index 5f50ee246..768018931 100644 --- a/Barotrauma/BarotraumaShared/Source/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/Source/Serialization/XMLExtensions.cs @@ -289,6 +289,30 @@ namespace Barotrauma return intValue; } + public static ushort[] GetAttributeUshortArray(this XElement element, string name, ushort[] defaultValue) + { + if (element?.Attribute(name) == null) return defaultValue; + + string stringValue = element.Attribute(name).Value; + if (string.IsNullOrEmpty(stringValue)) return defaultValue; + + string[] splitValue = stringValue.Split(','); + ushort[] ushortValue = new ushort[splitValue.Length]; + for (int i = 0; i < splitValue.Length; i++) + { + try + { + ushort val = ushort.Parse(splitValue[i]); + ushortValue[i] = val; + } + catch (Exception e) + { + DebugConsole.ThrowError("Error in " + element + "! ", e); + } + } + + return ushortValue; + } public static bool GetAttributeBool(this XElement element, string name, bool defaultValue) { diff --git a/Barotrauma/BarotraumaShared/Source/SteamAchievementManager.cs b/Barotrauma/BarotraumaShared/Source/SteamAchievementManager.cs index ac79701f8..c89a97190 100644 --- a/Barotrauma/BarotraumaShared/Source/SteamAchievementManager.cs +++ b/Barotrauma/BarotraumaShared/Source/SteamAchievementManager.cs @@ -259,23 +259,22 @@ namespace Barotrauma #if SERVER if (GameMain.Server?.TraitorManager != null) { - foreach (Traitor traitor in GameMain.Server.TraitorManager.TraitorList) + if (GameMain.Server.TraitorManager.IsTraitor(character)) { - if (traitor.TargetCharacter == character) - { - //killed the target as a traitor - UnlockAchievement(traitor.Character, "traitorwin"); - } - else if (traitor.Character == character) - { - //someone killed a traitor - UnlockAchievement(causeOfDeath.Killer, "killtraitor"); - } + UnlockAchievement(causeOfDeath.Killer, "killtraitor"); } } #endif } + public static void OnTraitorWin(Character character) + { +#if CLIENT + if (GameMain.Client != null || GameMain.GameSession == null) return; +#endif + UnlockAchievement(character, "traitorwin"); + } + public static void OnRoundEnded(GameSession gameSession) { //made it to the destination @@ -303,13 +302,11 @@ namespace Barotrauma } } -#if CLIENT - if (GameMain.Client != null) { return; } -#endif + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (gameSession.Mission != null) { - if (gameSession.Mission is CombatMission combatMission) + if (gameSession.Mission is CombatMission combatMission && GameMain.GameSession.WinningTeam.HasValue) { //all characters that are alive and in the winning team get an achievement UnlockAchievement(gameSession.Mission.Prefab.AchievementIdentifier + (int)GameMain.GameSession.WinningTeam, true, diff --git a/Barotrauma/BarotraumaShared/Source/TextManager.cs b/Barotrauma/BarotraumaShared/Source/TextManager.cs index beacafa37..743d9f8d6 100644 --- a/Barotrauma/BarotraumaShared/Source/TextManager.cs +++ b/Barotrauma/BarotraumaShared/Source/TextManager.cs @@ -69,7 +69,7 @@ namespace Barotrauma } return language; } - + public static void LoadTextPacks(IEnumerable selectedContentPackages) { availableLanguages.Clear(); @@ -311,10 +311,83 @@ namespace Barotrauma } } - return string.Format(text, args); + return string.Format(text, args); } + public static string FormatServerMessage(string textId) + { + return $"{textId}~"; + } + + public static string FormatServerMessage(string message, IEnumerable keys, IEnumerable values) + { + if (keys == null || values == null || !keys.Any() || !values.Any()) + { + return FormatServerMessage(message); + } + var startIndex = message.LastIndexOf('/') + 1; + var endIndex = message.IndexOf('~', startIndex); + if (endIndex == -1) + { + endIndex = message.Length - 1; + } + var textId = message.Substring(startIndex, endIndex - startIndex + 1); + var keysWithValues = keys.Zip(values, (key, value) => new { Key = key, Value = value }); + var prefixEntries = keysWithValues.Select((kv, index) => + { + if (kv.Value.IndexOfAny(new char[] { '~', '/' }) != -1) + { + var kvStartIndex = kv.Value.LastIndexOf('/') + 1; + return kv.Value.Substring(0, kvStartIndex) + $"[{textId}.{index}]={kv.Value.Substring(kvStartIndex)}"; + } + else + { + return null; + } + }).Where(e => e != null).ToArray(); + return string.Join("", + (prefixEntries.Length > 0 ? string.Join("/", prefixEntries) + "/" : ""), + message, + string.Join("", keysWithValues.Select((kv, index) => kv.Value.IndexOfAny(new char[] { '~', '/' }) != -1 ? $"~{kv.Key}=[{textId}.{index}]" : $"~{kv.Key}={kv.Value}").ToArray()) + ); + } + + static readonly string[] genderPronounVariables = new string[] { + "[genderpronoun]", + "[genderpronounpossessive]", + "[genderpronounreflexive]", + "[Genderpronoun]", + "[Genderpronounpossessive]", + "[Genderpronounreflexive]" + }; + + static readonly string[] genderPronounMaleValues = new string[] { + "PronounMaleLowercase", + "PronounPossessiveMaleLowercase", + "PronounReflexiveMaleLowercase", + "PronounMale", + "PronounPossessiveMale", + "PronounReflexiveMale" + }; + + static readonly string[] genderPronounFemaleValues = new string[] { + "PronounFemaleLowercase", + "PronounPossessiveFemaleLowercase", + "PronounReflexiveFemaleLowercase", + "PronounMale", + "PronounPossessiveFemale", + "PronounReflexiveFemale" + }; + + public static string FormatServerMessageWithGenderPronouns(Gender gender, string message, IEnumerable keys, IEnumerable values) + { + return FormatServerMessage(message, keys.Concat(genderPronounVariables), values.Concat(gender == Gender.Male ? genderPronounMaleValues : genderPronounFemaleValues)); + } + + static readonly Regex reReplacedMessage = new Regex(@"^(?[\[\].A-Za-z0-9_]+?)=(?.*)$", RegexOptions.Compiled); + // Format: ServerMessage.Identifier1/ServerMessage.Indentifier2~[variable1]=value~[variable2]=value + // Also: replacement=ServerMessage.Identifier1~[variable1]=value/ServerMessage.Identifier2~[variable2]=replacement public static string GetServerMessage(string serverMessage) { if (!textPacks.ContainsKey(Language)) @@ -328,6 +401,7 @@ namespace Barotrauma } string[] messages = serverMessage.Split('/'); + var replacedMessages = new Dictionary(); bool translationsFound = false; @@ -335,8 +409,17 @@ namespace Barotrauma { for (int i = 0; i < messages.Length; i++) { - if (!IsServerMessageWithVariables(messages[i])) // No variables, try to translate + if (messages[i].EndsWith("~", StringComparison.Ordinal)) { + messages[i] = messages[i].Substring(0, messages[i].Length - 1); + } + if (!IsServerMessageWithVariables(messages[i]) && !messages[i].Contains('=')) // No variables, try to translate + { + foreach (var replacedMessage in replacedMessages) + { + messages[i] = messages[i].Replace(replacedMessage.Key, replacedMessage.Value); + } + if (messages[i].Contains(" ")) continue; // Spaces found, do not translate string msg = Get(messages[i], true); if (msg != null) // If a translation was found, otherwise use the original @@ -347,7 +430,22 @@ namespace Barotrauma } else { + var match = reReplacedMessage.Match(messages[i]); + string messageVariable = null; + if (match.Success) + { + messageVariable = match.Groups["variable"].ToString(); + messages[i] = match.Groups["message"].ToString(); + } + + foreach (var replacedMessage in replacedMessages) + { + messages[i] = messages[i].Replace(replacedMessage.Key, replacedMessage.Value); + } + + string[] messageWithVariables = messages[i].Split('~'); + string msg = Get(messageWithVariables[0], true); if (msg != null) // If a translation was found, otherwise use the original @@ -355,7 +453,7 @@ namespace Barotrauma messages[i] = msg; translationsFound = true; } - else + else if (messageVariable == null) { continue; // No translation found, probably caused by player input -> skip variable handling } @@ -366,6 +464,12 @@ namespace Barotrauma string[] variableAndValue = messageWithVariables[j].Split('='); messages[i] = messages[i].Replace(variableAndValue[0], variableAndValue[1]); } + + if (messageVariable != null) + { + replacedMessages[messageVariable] = messages[i]; + messages[i] = null; + } } } @@ -374,7 +478,10 @@ namespace Barotrauma string translatedServerMessage = string.Empty; for (int i = 0; i < messages.Length; i++) { - translatedServerMessage += messages[i]; + if (messages[i] != null) + { + translatedServerMessage += messages[i]; + } } return translatedServerMessage; } @@ -427,7 +534,7 @@ namespace Barotrauma } return string.Join(separator, texts); } - + public static string EnsureUTF8(string text) { byte[] bytes = Encoding.Default.GetBytes(text); @@ -494,24 +601,24 @@ namespace Barotrauma { if (gender == Gender.Male) { - return text.Replace("[genderpronoun]", Get("PronounMale").ToLower()) - .Replace("[genderpronounpossessive]", Get("PronounPossessiveMale").ToLower()) - .Replace("[genderpronounreflexive]", Get("PronounReflexiveMale").ToLower()) - .Replace("[Genderpronoun]", Capitalize(Get("PronounMale"))) - .Replace("[Genderpronounpossessive]", Capitalize(Get("PronounPossessiveMale"))) - .Replace("[Genderpronounreflexive]", Capitalize(Get("PronounReflexiveMale"))); + return text.Replace("[genderpronoun]", Get("PronounMaleLowercase")) + .Replace("[genderpronounpossessive]", Get("PronounPossessiveMaleLowercase")) + .Replace("[genderpronounreflexive]", Get("PronounReflexiveMaleLowercase")) + .Replace("[Genderpronoun]", Get("PronounMale")) + .Replace("[Genderpronounpossessive]", Get("PronounPossessiveMale")) + .Replace("[Genderpronounreflexive]", Get("PronounReflexiveMale")); } else { - return text.Replace("[genderpronoun]", Get("PronounFemale").ToLower()) - .Replace("[genderpronounpossessive]", Get("PronounPossessiveFemale").ToLower()) - .Replace("[genderpronounreflexive]", Get("PronounReflexiveFemale").ToLower()) - .Replace("[Genderpronoun]", Capitalize(Get("PronounFemale"))) - .Replace("[Genderpronounpossessive]", Capitalize(Get("PronounPossessiveFemale"))) - .Replace("[Genderpronounreflexive]", Capitalize(Get("PronounReflexiveFemale"))); + return text.Replace("[genderpronoun]", Get("PronounFemaleLowercase")) + .Replace("[genderpronounpossessive]", Get("PronounPossessiveFemaleLowercase")) + .Replace("[genderpronounreflexive]", Get("PronounReflexiveFemaleLowerCase")) + .Replace("[Genderpronoun]", Get("PronounFemale")) + .Replace("[Genderpronounpossessive]", Get("PronounPossessiveFemale")) + .Replace("[Genderpronounreflexive]", Get("PronounReflexiveFemale")); } } - + static Regex isCJK = new Regex( @"\p{IsHangulJamo}|" + @"\p{IsCJKRadicalsSupplement}|" + diff --git a/Barotrauma/BarotraumaShared/Source/TextPack.cs b/Barotrauma/BarotraumaShared/Source/TextPack.cs index 565b752da..f2373007e 100644 --- a/Barotrauma/BarotraumaShared/Source/TextPack.cs +++ b/Barotrauma/BarotraumaShared/Source/TextPack.cs @@ -50,6 +50,10 @@ namespace Barotrauma public string Get(string textTag) { + if (string.IsNullOrEmpty(textTag)) + { + return null; + } if (!texts.TryGetValue(textTag.ToLowerInvariant(), out List textList) || !textList.Any()) { return null; diff --git a/Barotrauma/BarotraumaShared/Source/Utils/SaveUtil.cs b/Barotrauma/BarotraumaShared/Source/Utils/SaveUtil.cs index 23c9465fa..2e473befc 100644 --- a/Barotrauma/BarotraumaShared/Source/Utils/SaveUtil.cs +++ b/Barotrauma/BarotraumaShared/Source/Utils/SaveUtil.cs @@ -404,7 +404,7 @@ namespace Barotrauma } } - public static void CopyFolder(string sourceDirName, string destDirName, bool copySubDirs) + public static void CopyFolder(string sourceDirName, string destDirName, bool copySubDirs, bool overwriteExisting = false) { // Get the subdirectories for the specified directory. DirectoryInfo dir = new DirectoryInfo(sourceDirName); @@ -428,6 +428,10 @@ namespace Barotrauma foreach (FileInfo file in files) { string temppath = Path.Combine(destDirName, file.Name); + if (overwriteExisting && File.Exists(temppath)) + { + File.Delete(temppath); + } file.CopyTo(temppath, false); } diff --git a/Barotrauma/BarotraumaShared/Source/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/Source/Utils/ToolBox.cs index 0844ee21b..0eaa47e1c 100644 --- a/Barotrauma/BarotraumaShared/Source/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/Source/Utils/ToolBox.cs @@ -1,5 +1,4 @@ -using Lidgren.Network; -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -7,6 +6,7 @@ using System.Security.Cryptography; using System.Reflection; using System.Text; using Microsoft.Xna.Framework; +using Barotrauma.Networking; namespace Barotrauma { @@ -313,13 +313,16 @@ namespace Barotrauma /// /// Reads a number of bits from the buffer and inserts them to a new NetBuffer instance /// - public static NetBuffer ExtractBits(this NetBuffer originalBuffer, int numberOfBits) + public static IReadMessage ExtractBits(this IReadMessage originalBuffer, int numberOfBits) { - var buffer = new NetBuffer(); - byte[] data = new byte[(int)Math.Ceiling(numberOfBits / (double)8)]; - - originalBuffer.ReadBits(data, 0, numberOfBits); - buffer.Write(data); + var buffer = new ReadWriteMessage(); + + for (int i=0;i \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/Client/Auth.cs b/Libraries/Facepunch.Steamworks/Client/Auth.cs index d33ff40bd..9341ac467 100644 --- a/Libraries/Facepunch.Steamworks/Client/Auth.cs +++ b/Libraries/Facepunch.Steamworks/Client/Auth.cs @@ -14,17 +14,60 @@ namespace Facepunch.Steamworks get { if ( _auth == null ) - _auth = new Auth{ client = this }; + _auth = new Auth(this); return _auth; } } } + /// + /// Steam authentication statuses + /// + public enum ClientAuthStatus : int + { + OK = 0, + UserNotConnectedToSteam = 1, + NoLicenseOrExpired = 2, + VACBanned = 3, + LoggedInElseWhere = 4, + VACCheckTimedOut = 5, + AuthTicketCanceled = 6, + AuthTicketInvalidAlreadyUsed = 7, + AuthTicketInvalid = 8, + PublisherIssuedBan = 9, + } + + public enum ClientStartAuthSessionResult : int + { + OK = 0, + InvalidTicket = 1, + DuplicateRequest = 2, + InvalidVersion = 3, + GameMismatch = 4, + ExpiredTicket = 5, + ServerNotConnectedToSteam = 6, + } + public class Auth { + public Auth(Client c) + { + client = c; + + client.RegisterCallback(OnAuthTicketValidate); + } + + void OnAuthTicketValidate(SteamNative.ValidateAuthTicketResponse_t data) + { + if (OnAuthChange != null) + OnAuthChange(data.SteamID, data.OwnerSteamID, (ClientAuthStatus)data.AuthSessionResponse); + } + internal Client client; + public Action OnAuthChange; + public class Ticket : IDisposable { internal Client client; @@ -77,6 +120,27 @@ namespace Facepunch.Steamworks } } + /// + /// Start authorizing a ticket. This user isn't authorized yet. Wait for a call to OnAuthChange. + /// + public unsafe ClientStartAuthSessionResult StartSession(byte[] data, ulong steamid) + { + fixed (byte* p = data) + { + var result = client.native.user.BeginAuthSession((IntPtr)p, data.Length, steamid); + + return (ClientStartAuthSessionResult)result; + } + } + + /// + /// Forget this guy. They're no longer in the game. + /// + public void EndSession(ulong steamid) + { + client.native.user.EndAuthSession(steamid); + } + } } diff --git a/Libraries/Facepunch.Steamworks/Client/Lobby.LobbyData.cs b/Libraries/Facepunch.Steamworks/Client/Lobby.LobbyData.cs index 63e4e8498..c45df47db 100644 --- a/Libraries/Facepunch.Steamworks/Client/Lobby.LobbyData.cs +++ b/Libraries/Facepunch.Steamworks/Client/Lobby.LobbyData.cs @@ -60,7 +60,12 @@ namespace Facepunch.Steamworks if ( data.ContainsKey( k ) ) { if ( data[k] == v ) { return true; } - if ( client.native.matchmaking.SetLobbyData( lobby, k, v ) ) + bool setKey = true; + if (lobby == client.Lobby.CurrentLobby && client.Lobby.Owner == client.SteamId) + { + setKey = client.native.matchmaking.SetLobbyData(lobby, k, v); + } + if ( setKey ) { data[k] = v; return true; @@ -68,7 +73,12 @@ namespace Facepunch.Steamworks } else { - if ( client.native.matchmaking.SetLobbyData( lobby, k, v ) ) + bool setKey = true; + if (lobby == client.Lobby.CurrentLobby && client.Lobby.Owner == client.SteamId) + { + setKey = client.native.matchmaking.SetLobbyData(lobby, k, v); + } + if ( setKey ) { data.Add( k, v ); return true; diff --git a/Libraries/Facepunch.Steamworks/Client/Lobby.cs b/Libraries/Facepunch.Steamworks/Client/Lobby.cs index fb2c1633b..bae3249df 100644 --- a/Libraries/Facepunch.Steamworks/Client/Lobby.cs +++ b/Libraries/Facepunch.Steamworks/Client/Lobby.cs @@ -169,19 +169,29 @@ namespace Facepunch.Steamworks /// /// Updates the LobbyData property to have the data for the current lobby, if any /// + private bool suppressUpdateLobbyData = false; internal void UpdateLobbyData() { - int dataCount = client.native.matchmaking.GetLobbyDataCount( CurrentLobby ); - CurrentLobbyData = new LobbyData( client, CurrentLobby ); - for ( int i = 0; i < dataCount; i++ ) + if (suppressUpdateLobbyData) { return; } + try { - if ( client.native.matchmaking.GetLobbyDataByIndex( CurrentLobby, i, out string key, out string value ) ) + suppressUpdateLobbyData = true; + int dataCount = client.native.matchmaking.GetLobbyDataCount(CurrentLobby); + CurrentLobbyData = new LobbyData(client, CurrentLobby); + for (int i = 0; i < dataCount; i++) { - CurrentLobbyData.SetData( key, value ); + if (client.native.matchmaking.GetLobbyDataByIndex(CurrentLobby, i, out string key, out string value)) + { + CurrentLobbyData.SetData(key, value); + } } - } - if ( OnLobbyDataUpdated != null ) { OnLobbyDataUpdated(); } + if (OnLobbyDataUpdated != null) { OnLobbyDataUpdated(); } + } + finally + { + suppressUpdateLobbyData = false; + } } /// diff --git a/Libraries/Facepunch.Steamworks/Client/LobbyList.cs b/Libraries/Facepunch.Steamworks/Client/LobbyList.cs index 447097326..0b4d1258f 100644 --- a/Libraries/Facepunch.Steamworks/Client/LobbyList.cs +++ b/Libraries/Facepunch.Steamworks/Client/LobbyList.cs @@ -76,7 +76,7 @@ namespace Facepunch.Steamworks } - + bool registeredLobbyDataUpdated = false; void OnLobbyList(LobbyMatchList_t callback, bool error) { if (error) return; @@ -89,6 +89,9 @@ namespace Facepunch.Steamworks { //add the lobby to the list of requests ulong lobby = client.native.matchmaking.GetLobbyByIndex(i); + + if (requests.Contains(lobby)) { continue; } + requests.Add(lobby); //cast to a LobbyList.Lobby @@ -103,7 +106,11 @@ namespace Facepunch.Steamworks { //else we need to get the info for the missing lobby client.native.matchmaking.RequestLobbyData(lobby); - client.RegisterCallback( OnLobbyDataUpdated ); + if (!registeredLobbyDataUpdated) + { + client.RegisterCallback(OnLobbyDataUpdated); + registeredLobbyDataUpdated = true; + } } } @@ -115,7 +122,7 @@ namespace Facepunch.Steamworks void checkFinished() { - if (Lobbies.Count == requests.Count) + if (Lobbies.Count >= requests.Count) { Finished = true; return; @@ -128,11 +135,12 @@ namespace Facepunch.Steamworks if (callback.Success == 1) //1 if success, 0 if failure { //find the lobby that has been updated - Lobby lobby = Lobbies.Find(x => x.LobbyID == callback.SteamIDLobby); + Lobby lobby = Lobbies.Find(x => x != null && x.LobbyID == callback.SteamIDLobby); //if this lobby isn't yet in the list of lobbies, we know that we should add it if (lobby == null) { + lobby = Lobby.FromSteam(client, callback.SteamIDLobby); Lobbies.Add(lobby); checkFinished(); } @@ -140,8 +148,6 @@ namespace Facepunch.Steamworks //otherwise lobby data in general was updated and you should listen to see what changed if (OnLobbiesUpdated != null) { OnLobbiesUpdated(); } } - - } public Action OnLobbiesUpdated; diff --git a/Libraries/Facepunch.Steamworks/Client/ServerList.Server.cs b/Libraries/Facepunch.Steamworks/Client/ServerList.Server.cs index 40dfdc653..2dbc6e849 100644 --- a/Libraries/Facepunch.Steamworks/Client/ServerList.Server.cs +++ b/Libraries/Facepunch.Steamworks/Client/ServerList.Server.cs @@ -46,16 +46,36 @@ namespace Facepunch.Steamworks internal static Server FromSteam( Client client, SteamNative.gameserveritem_t item ) { + int serverNameLength = item.ServerName.Length; + for (int i = 0; i < item.ServerName.Length; i++) + { + if (item.ServerName[i] == '\0') + { + serverNameLength = i; + break; + } + } + + int mapLength = item.Map.Length; + for (int i = 0; i < item.Map.Length; i++) + { + if (item.Map[i] == '\0') + { + mapLength = i; + break; + } + } + return new Server() { Client = client, Address = Utility.Int32ToIp( item.NetAdr.IP ), ConnectionPort = item.NetAdr.ConnectionPort, QueryPort = item.NetAdr.QueryPort, - Name = Encoding.UTF8.GetString(item.ServerName), + Name = Encoding.UTF8.GetString(item.ServerName, 0, serverNameLength), Ping = item.Ping, GameDir = item.GameDir, - Map = Encoding.UTF8.GetString(item.Map), + Map = Encoding.UTF8.GetString(item.Map, 0, mapLength), Description = item.GameDescription, AppId = item.AppID, Players = item.Players, diff --git a/Libraries/Facepunch.Steamworks/Interfaces/Networking.cs b/Libraries/Facepunch.Steamworks/Interfaces/Networking.cs index ee8bca292..6d5b2ffe5 100644 --- a/Libraries/Facepunch.Steamworks/Interfaces/Networking.cs +++ b/Libraries/Facepunch.Steamworks/Interfaces/Networking.cs @@ -37,7 +37,10 @@ namespace Facepunch.Steamworks OnIncomingConnection = null; OnConnectionFailed = null; OnP2PData = null; - ListenChannels.Clear(); + lock (ListenChannels) + { + ListenChannels.Clear(); + } } @@ -56,27 +59,34 @@ namespace Facepunch.Steamworks UpdateTimer.Reset(); UpdateTimer.Start(); - foreach ( var channel in ListenChannels ) + lock (ListenChannels) { - while ( ReadP2PPacket( channel ) ) + for (int i = 0; i < ListenChannels.Count; i++) { - // Nothing Here. + while (ReadP2PPacket(ListenChannels[i])) + { + //handle listen channel being closed by OnP2PData callback + if (i >= ListenChannels.Count) break; + } } } } /// /// Enable or disable listening on a specific channel. - /// If you donp't enable the channel we won't listen to it, + /// If you don't enable the channel we won't listen to it, /// so you won't be able to receive messages on it. /// public void SetListenChannel( int ChannelId, bool Listen ) { - ListenChannels.RemoveAll( x => x == ChannelId ); - - if ( Listen ) + lock (ListenChannels) { - ListenChannels.Add( ChannelId ); + ListenChannels.RemoveAll(x => x == ChannelId); + + if (Listen) + { + ListenChannels.Add(ChannelId); + } } } diff --git a/Libraries/Facepunch.Steamworks/Interfaces/Workshop.Item.cs b/Libraries/Facepunch.Steamworks/Interfaces/Workshop.Item.cs index 718fce135..50d730800 100644 --- a/Libraries/Facepunch.Steamworks/Interfaces/Workshop.Item.cs +++ b/Libraries/Facepunch.Steamworks/Interfaces/Workshop.Item.cs @@ -52,9 +52,19 @@ namespace Facepunch.Steamworks return item; } - public bool Download( bool highPriority = true, Action onInstalled = null ) + public bool Download(bool highPriority = true, Action onInstalled = null) { - if ( Installed ) return true; + return Download(highPriority, false, onInstalled); + } + + public bool ForceDownload(bool highPriority = true, Action onInstalled = null) + { + return Download(highPriority, true, onInstalled); + } + + private bool Download( bool highPriority = true, bool ignoreAlreadyInstalled = false, Action onInstalled = null ) + { + if ( !ignoreAlreadyInstalled && Installed ) return true; if ( Downloading ) return true; if ( !workshop.ugc.DownloadItem( Id, highPriority ) ) diff --git a/Libraries/Facepunch.Steamworks/Server.cs b/Libraries/Facepunch.Steamworks/Server.cs index 13a01c804..15aea5080 100644 --- a/Libraries/Facepunch.Steamworks/Server.cs +++ b/Libraries/Facepunch.Steamworks/Server.cs @@ -112,6 +112,17 @@ namespace Facepunch.Steamworks base.Update(); } + /// + /// Gets the server owner's SteamID. + /// + public ulong SteamId + { + get + { + return native.gameServer.GetSteamID(); + } + } + /// /// Sets whether this should be marked as a dedicated server. /// If not, it is assumed to be a listen server. diff --git a/Libraries/Facepunch.Steamworks/Server/Auth.cs b/Libraries/Facepunch.Steamworks/Server/Auth.cs index f94cddc33..9270c0a6a 100644 --- a/Libraries/Facepunch.Steamworks/Server/Auth.cs +++ b/Libraries/Facepunch.Steamworks/Server/Auth.cs @@ -15,7 +15,7 @@ namespace Facepunch.Steamworks public Action OnAuthChange; /// - /// Steam authetication statuses + /// Steam authentication statuses /// public enum Status : int { @@ -31,6 +31,17 @@ namespace Facepunch.Steamworks PublisherIssuedBan = 9, } + public enum StartAuthSessionResult : int + { + OK = 0, + InvalidTicket = 1, + DuplicateRequest = 2, + InvalidVersion = 3, + GameMismatch = 4, + ExpiredTicket = 5, + ServerNotConnectedToSteam = 6, + } + internal ServerAuth( Server s ) { server = s; @@ -47,16 +58,13 @@ namespace Facepunch.Steamworks /// /// Start authorizing a ticket. This user isn't authorized yet. Wait for a call to OnAuthChange. /// - public unsafe bool StartSession( byte[] data, ulong steamid ) + public unsafe StartAuthSessionResult StartSession( byte[] data, ulong steamid ) { fixed ( byte* p = data ) { var result = server.native.gameServer.BeginAuthSession( (IntPtr)p, data.Length, steamid ); - if ( result == SteamNative.BeginAuthSessionResult.OK ) - return true; - - return false; + return (StartAuthSessionResult)result; } } diff --git a/Libraries/MonoGame.Framework/DesktopGL/MonoGame.Framework.dll b/Libraries/MonoGame.Framework/DesktopGL/MonoGame.Framework.dll index 2088fd550..39c64ff3a 100644 Binary files a/Libraries/MonoGame.Framework/DesktopGL/MonoGame.Framework.dll and b/Libraries/MonoGame.Framework/DesktopGL/MonoGame.Framework.dll differ diff --git a/Libraries/MonoGame.Framework/DesktopGL/MonoGame.Framework.xml b/Libraries/MonoGame.Framework/DesktopGL/MonoGame.Framework.xml index 98c00467a..83d8af8c8 100644 --- a/Libraries/MonoGame.Framework/DesktopGL/MonoGame.Framework.xml +++ b/Libraries/MonoGame.Framework/DesktopGL/MonoGame.Framework.xml @@ -16663,669 +16663,5 @@ - - - A thread-safe, read-only, buffering stream wrapper. - - - - - A single data packet from a logical Vorbis stream. - - - - - Defines flags to apply to the current packet - - - - - Packet is first since reader had to resync with stream. - - - - - Packet is the last in the logical stream. - - - - - Packet does not have all its data available. - - - - - Packet has a granule count defined. - - - - - Flag for use by inheritors. - - - - - Flag for use by inheritors. - - - - - Flag for use by inheritors. - - - - - Flag for use by inheritors. - - - - - Gets the value of the specified flag. - - - - - Sets the value of the specified flag. - - - - - Creates a new instance with the specified length. - - The length of the packet. - - - - Reads the next byte of the packet. - - The next byte if available, otherwise -1. - - - - Indicates that the packet has been read and its data is no longer needed. - - - - - Attempts to read the specified number of bits from the packet, but may return fewer. Does not advance the position counter. - - The number of bits to attempt to read. - The number of bits actually read. - The value of the bits read. - is not between 0 and 64. - - - - Advances the position counter by the specified number of bits. - - The number of bits to advance. - - - - Resets the bit reader. - - - - - Gets whether the packet was found after a stream resync. - - - - - Gets the position of the last granule in the packet. - - - - - Gets the position of the last granule in the page the packet is in. - - - - - Gets the length of the packet. - - - - - Gets whether the packet is the last one in the logical stream. - - - - - Gets the number of bits read from the packet. - - - - - Gets the number of granules in the packet. If null, the packet has not been decoded yet. - - - - - Reads the specified number of bits from the packet and advances the position counter. - - The number of bits to read. - The value of the bits read. - The number of bits specified is not between 0 and 64. - - - - Reads the next byte from the packet. Does not advance the position counter. - - The byte read from the packet. - - - - Reads the next byte from the packet and advances the position counter. - - The byte read from the packet. - - - - Reads the specified number of bytes from the packet and advances the position counter. - - The number of bytes to read. - A byte array holding the data read. - - - - Reads the specified number of bytes from the packet into the buffer specified and advances the position counter. - - The buffer to read into. - The index into the buffer to start placing the read data. - The number of bytes to read. - The number of bytes read. - is less than 0 or + is past the end of . - - - - Reads the next bit from the packet and advances the position counter. - - The value of the bit read. - - - - Retrieves the next 16 bits from the packet as a and advances the position counter. - - The value of the next 16 bits. - - - - Retrieves the next 32 bits from the packet as a and advances the position counter. - - The value of the next 32 bits. - - - - Retrieves the next 64 bits from the packet as a and advances the position counter. - - The value of the next 64 bits. - - - - Retrieves the next 16 bits from the packet as a and advances the position counter. - - The value of the next 16 bits. - - - - Retrieves the next 32 bits from the packet as a and advances the position counter. - - The value of the next 32 bits. - - - - Retrieves the next 64 bits from the packet as a and advances the position counter. - - The value of the next 64 bits. - - - - Advances the position counter by the specified number of bytes. - - The number of bytes to advance. - - - - Provides a interface for a Vorbis logical stream container. - - - - - Gets the list of stream serials found in the container so far. - - - - - Gets whether the container supports seeking. - - - - - Gets the number of bits in the container that are not associated with a logical stream. - - - - - Gets the number of pages that have been read in the container. - - - - - Event raised when a new logical stream is found in the container. - - - - - Initializes the container and finds the first stream. - - True if a valid logical stream is found, otherwise False. - - - - Finds the next new stream in the container. - - True if a new stream was found, otherwise False. - is False. - - - - Retrieves the total number of pages in the container. - - The total number of pages. - is False. - - - - Provides packets on-demand for the Vorbis stream decoder. - - - - - Gets the serial number associated with this stream. - - - - - Gets whether seeking is supported on this stream. - - - - - Gets the number of bits of overhead in this stream's container. - - - - - Retrieves the total number of pages (or frames) this stream uses. - - The page count. - is False. - - - - Retrieves the next packet in the stream. - - The next packet in the stream or null if no more packets. - - - - Retrieves the next packet in the stream but does not advance to the following packet. - - The next packet in the stream or null if no more packets. - - - - Retrieves the packet specified from the stream. - - The index of the packet to retrieve. - The specified packet. - is less than 0 or past the end of the stream. - is False. - - - - Retrieves the total number of granules in this Vorbis stream. - - The number of samples - is False. - - - - Finds the packet index to the granule position specified in the current stream. - - The granule position to seek to. - A callback method that takes the current and previous packets and returns the number of granules in the current packet. - The index of the packet that includes the specified granule position or -1 if none found. - is less than 0 or is after the last granule. - - - - Sets the next packet to be returned, applying a pre-roll as necessary. - - The packet to key from. - The number of packets to return before the indicated packet. - - - - Occurs when the stream is about to change parameters. - - - - - Gets the counters for latency and bitrate calculations, as well as overall bit counts - - - - - Gets the calculated bit rate of audio stream data for the everything decoded so far - - - - - Gets the calculated bit rate for the last ~1 second of audio - - - - - Gets the calculated latency per page - - - - - Gets the calculated latency per packet - - - - - Gets the calculated latency per second of output - - - - - Gets the number of bits read that do not contribute to the output audio - - - - - Gets the number of bits read that contribute to the output audio - - - - - Gets the number of pages read so far in the current stream - - - - - Gets the total number of pages in the current stream - - - - - Gets whether the stream has been clipped since the last reset - - - - - Event data for when a new logical stream is found in a container. - - - - - Creates a new instance of with the specified . - - An instance. - - - - Gets new the instance. - - - - - Gets or sets whether to ignore the logical stream associated with the packet provider. - - - - - Event data for when a logical stream has a parameter change. - - - - - Creates a new instance of . - - The first packet after the parameter change. - - - - Gets the first packet after the parameter change. This would typically be the parameters packet. - - - - - Gets or Sets whether to limit reads to the smallest size possible. - - - - - Gets or Sets the maximum size of the buffer. This is not a hard limit. - - - - - Gets the offset of the start of the buffered data. Reads to offsets before this are likely to require a seek. - - - - - Gets the number of bytes currently buffered. - - - - - Gets the number of bytes the buffer can hold. - - - - - Reads the number of bytes specified into the buffer given, starting with the offset indicated. - - The offset into the stream to start reading. - The buffer to read to. - The index into the buffer to start writing to. - The number of bytes to read. - The number of bytes read. - - - - Tells the buffer that it no longer needs to maintain any bytes before the indicated offset. - - The offset to discard through. - - - - Gets the number of channels in the current selected Vorbis stream - - - - - Gets the sample rate of the current selected Vorbis stream - - - - - Gets the encoder's upper bitrate of the current selected Vorbis stream - - - - - Gets the encoder's nominal bitrate of the current selected Vorbis stream - - - - - Gets the encoder's lower bitrate of the current selected Vorbis stream - - - - - Gets the encoder's vendor string for the current selected Vorbis stream - - - - - Gets the comments in the current selected Vorbis stream - - - - - Gets whether the previous short sample count was due to a parameter change in the stream. - - - - - Gets the number of bits read that are related to framing and transport alone - - - - - Gets or sets whether to automatically apply clipping to samples returned by . - - - - - Gets stats from each decoder stream available - - - - - Gets the currently-selected stream's index - - - - - Reads decoded samples from the current logical stream - - The buffer to write the samples to - The offset into the buffer to write the samples to - The number of samples to write - The number of samples written - - - - Clears the parameter change flag so further samples can be requested. - - - - - Returns the number of logical streams found so far in the physical container - - - - - Searches for the next stream in a concatenated file - - True if a new stream was found, otherwise false. - - - - Switches to an alternate logical stream. - - The logical stream index to switch to - True if the properties of the logical stream differ from those of the one previously being decoded. Otherwise, False. - - - - Gets or Sets the current timestamp of the decoder. Is the timestamp before the next sample to be decoded - - - - - Gets or Sets the current position of the next sample to be decoded. - - - - - Gets the total length of the current logical stream - - - - - Provides an implementation for basic Ogg files. - - - - - Gets the list of stream serials found in the container so far. - - - - - Event raised when a new logical stream is found in the container. - - - - - Creates a new instance with the specified file. - - The full path to the file. - - - - Creates a new instance with the specified stream. Optionally sets to close the stream when disposed. - - The stream to read. - True to close the stream when is called, otherwise False. - - - - Initializes the container and finds the first stream. - - True if a valid logical stream is found, otherwise False. - - - - Disposes this instance. - - - - - Gets the instance for the specified stream serial. - - The stream serial to look for. - An instance. - The specified stream serial was not found. - - - - Finds the next new stream in the container. - - True if a new stream was found, otherwise False. - is False. - - - - Gets the number of pages that have been read in the container. - - - - - Retrieves the total number of pages in the container. - - The total number of pages. - is False. - - - - Gets whether the container supports seeking. - - - - - Gets the number of bits in the container that are not associated with a logical stream. - - diff --git a/Libraries/MonoGame.Framework/DesktopGL/libSDL2-2.0.0.dylib b/Libraries/MonoGame.Framework/DesktopGL/libSDL2-2.0.0.dylib new file mode 100644 index 000000000..221b67812 Binary files /dev/null and b/Libraries/MonoGame.Framework/DesktopGL/libSDL2-2.0.0.dylib differ diff --git a/Libraries/MonoGame.Framework/DesktopGL/libopenal.1.dylib b/Libraries/MonoGame.Framework/DesktopGL/libopenal.1.dylib new file mode 100644 index 000000000..883af9540 Binary files /dev/null and b/Libraries/MonoGame.Framework/DesktopGL/libopenal.1.dylib differ diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Input/KeyboardUtil.SDL.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Input/KeyboardUtil.SDL.cs index 3d311a24b..c5baac2cd 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Input/KeyboardUtil.SDL.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Input/KeyboardUtil.SDL.cs @@ -161,7 +161,7 @@ namespace Microsoft.Xna.Framework.Input return Keys.None; } - public static char ApplyModifiers(char chr, Sdl.Keyboard.Keymod mods) + public static int ApplyModifiers(int chr, Sdl.Keyboard.Keymod mods) { //TODO: this is not by any means comprehensive if ((mods & Sdl.Keyboard.Keymod.Ctrl) == Sdl.Keyboard.Keymod.None) return chr; @@ -169,22 +169,22 @@ namespace Microsoft.Xna.Framework.Input { case 'a': case 'A': - return (char)0x1; + return 0x1; case 'r': case 'R': - return (char)0x12; + return 0x12; case 'z': case 'Z': - return (char)0x1A; + return 0x1A; case 'x': case 'X': - return (char)0x18; + return 0x18; case 'c': case 'C': - return (char)0x3; + return 0x3; case 'v': case 'V': - return (char)0x16; + return 0x16; } return chr; } diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Linux.csproj b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Linux.csproj index a53de9258..a095321b9 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Linux.csproj +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Linux.csproj @@ -31,9 +31,9 @@ true none - bin\Linux\AnyCPU\Release + ..\..\DesktopGL\ obj\Linux\AnyCPU\Release - bin\Linux\AnyCPU\Release\MonoGame.Framework.xml + ..\..\DesktopGL\MonoGame.Framework.xml OPENGL;OPENAL;XNADESIGNPROVIDED;TRACE;LINUX;DESKTOPGL;SUPPORTS_EFX prompt 4 @@ -537,106 +537,6 @@ _XNADesignProvided - - Utilities\NVorbis\BufferedReadStream.cs - NVorbis - - - Utilities\NVorbis\DataPacket.cs - NVorbis - - - Utilities\NVorbis\Huffman.cs - NVorbis - - - Utilities\NVorbis\IContainerReader.cs - NVorbis - - - Utilities\NVorbis\IPacketProvider.cs - NVorbis - - - Utilities\NVorbis\IVorbisStreamStatus.cs - NVorbis - - - Utilities\NVorbis\Mdct.cs - NVorbis - - - Utilities\NVorbis\NewStreamEventArgs.cs - NVorbis - - - Utilities\NVorbis\ParameterChangeEventArgs.cs - NVorbis - - - Utilities\NVorbis\RingBuffer.cs - NVorbis - - - Utilities\NVorbis\StreamReadBuffer.cs - NVorbis - - - Utilities\NVorbis\Utils.cs - NVorbis - - - Utilities\NVorbis\VorbisCodebook.cs - NVorbis - - - Utilities\NVorbis\VorbisFloor.cs - NVorbis - - - Utilities\NVorbis\VorbisMapping.cs - NVorbis - - - Utilities\NVorbis\VorbisMode.cs - NVorbis - - - Utilities\NVorbis\VorbisReader.cs - NVorbis - - - Utilities\NVorbis\VorbisResidue.cs - NVorbis - - - Utilities\NVorbis\VorbisStreamDecoder.cs - NVorbis - - - Utilities\NVorbis\VorbisTime.cs - NVorbis - - - Utilities\NVorbis\Ogg\OggContainerReader.cs - NVorbis - - - Utilities\NVorbis\Ogg\OggCrc.cs - NVorbis - - - Utilities\NVorbis\Ogg\OggPacket.cs - NVorbis - - - Utilities\NVorbis\Ogg\OggPacketReader.cs - NVorbis - - - Utilities\NVorbis\Ogg\OggPageFlags.cs - NVorbis - diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Windows.csproj b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Windows.csproj index 72598aef9..c9a2d2415 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Windows.csproj +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Windows.csproj @@ -20,9 +20,9 @@ false full true - bin\Windows\AnyCPU\Debug + ..\..\Windows\ obj\Windows\AnyCPU\Debug - bin\Windows\AnyCPU\Debug\MonoGame.Framework.xml + ..\..\Windows\MonoGame.Framework.xml DEBUG;XNADESIGNPROVIDED;TRACE;WINDOWS;DIRECTX;WINDOWS_MEDIA_SESSION prompt 4 diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGamePlatform.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGamePlatform.cs index bcb7d0824..cc9f42204 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGamePlatform.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGamePlatform.cs @@ -132,9 +132,23 @@ namespace Microsoft.Xna.Framework if (!_keys.Contains(key)) _keys.Add(key); - char character = KeyboardUtil.ApplyModifiers((char)ev.Key.Keysym.Sym, ev.Key.Keysym.Mod); - if (char.IsControl(character)) + + //TODO: rethink all of this + char character = (char)KeyboardUtil.ApplyModifiers(ev.Key.Keysym.Sym, ev.Key.Keysym.Mod); + + if ((int)((char)ev.Key.Keysym.Sym) != ev.Key.Keysym.Sym) + { + character = '\0'; + } + + if (char.IsControl(character) || + key == Keys.Left || + key == Keys.Right || + key == Keys.Up || + key == Keys.Down) + { _view.CallTextInput(character, key); + } } else if (ev.Type == Sdl.EventType.KeyUp) { diff --git a/Libraries/MonoGame.Framework/Windows/MonoGame.Framework.dll b/Libraries/MonoGame.Framework/Windows/MonoGame.Framework.dll index 6376c2f6f..52de5b72a 100644 Binary files a/Libraries/MonoGame.Framework/Windows/MonoGame.Framework.dll and b/Libraries/MonoGame.Framework/Windows/MonoGame.Framework.dll differ diff --git a/Libraries/MonoGame.Framework/Windows/MonoGame.Framework.pdb b/Libraries/MonoGame.Framework/Windows/MonoGame.Framework.pdb index 8634c71d6..e35e05b62 100644 Binary files a/Libraries/MonoGame.Framework/Windows/MonoGame.Framework.pdb and b/Libraries/MonoGame.Framework/Windows/MonoGame.Framework.pdb differ