diff --git a/Barotrauma/BarotraumaClient/ClientCode.projitems b/Barotrauma/BarotraumaClient/ClientCode.projitems index 3864fe96e..a5cc964a4 100644 --- a/Barotrauma/BarotraumaClient/ClientCode.projitems +++ b/Barotrauma/BarotraumaClient/ClientCode.projitems @@ -214,6 +214,9 @@ + + Never + diff --git a/Barotrauma/BarotraumaClient/Content/Content.mgcb b/Barotrauma/BarotraumaClient/Content/Content.mgcb deleted file mode 100644 index b23f45db0..000000000 --- a/Barotrauma/BarotraumaClient/Content/Content.mgcb +++ /dev/null @@ -1,51 +0,0 @@ - -#----------------------------- Global Properties ----------------------------# - -/outputDir:bin/$(Platform) -/intermediateDir:obj/$(Platform) -/platform:Windows -/config: -/profile:Reach -/compress:False - -#-------------------------------- References --------------------------------# - - -#---------------------------------- Content ---------------------------------# - -#begin Effects/blurshader.fx -/importer:EffectImporter -/processor:EffectProcessor -/processorParam:DebugMode=Auto -/build:Effects/blurshader.fx - -#begin Effects/damageshader.fx -/importer:EffectImporter -/processor:EffectProcessor -/processorParam:DebugMode=Auto -/build:Effects/damageshader.fx - -#begin Effects/deformshader.fx -/importer:EffectImporter -/processor:EffectProcessor -/processorParam:DebugMode=Auto -/build:Effects/deformshader.fx - -#begin Effects/losshader.fx -/importer:EffectImporter -/processor:EffectProcessor -/processorParam:DebugMode=Auto -/build:Effects/losshader.fx - -#begin Effects/postprocess.fx -/importer:EffectImporter -/processor:EffectProcessor -/processorParam:DebugMode=Auto -/build:Effects/postprocess.fx - -#begin Effects/watershader.fx -/importer:EffectImporter -/processor:EffectProcessor -/processorParam:DebugMode=Auto -/build:Effects/watershader.fx - diff --git a/Barotrauma/BarotraumaClient/Content/Content_opengl.mgcb b/Barotrauma/BarotraumaClient/Content/Content_opengl.mgcb deleted file mode 100644 index d89dd6e4e..000000000 --- a/Barotrauma/BarotraumaClient/Content/Content_opengl.mgcb +++ /dev/null @@ -1,51 +0,0 @@ - -#----------------------------- Global Properties ----------------------------# - -/outputDir:bin/$(Platform) -/intermediateDir:obj/$(Platform) -/platform:DesktopGL -/config: -/profile:Reach -/compress:False - -#-------------------------------- References --------------------------------# - - -#---------------------------------- Content ---------------------------------# - -#begin Effects/blurshader_opengl.fx -/importer:EffectImporter -/processor:EffectProcessor -/processorParam:DebugMode=Auto -/build:Effects/blurshader_opengl.fx - -#begin Effects/damageshader_opengl.fx -/importer:EffectImporter -/processor:EffectProcessor -/processorParam:DebugMode=Auto -/build:Effects/damageshader_opengl.fx - -#begin Effects/deformshader_opengl.fx -/importer:EffectImporter -/processor:EffectProcessor -/processorParam:DebugMode=Auto -/build:Effects/deformshader_opengl.fx - -#begin Effects/losshader_opengl.fx -/importer:EffectImporter -/processor:EffectProcessor -/processorParam:DebugMode=Auto -/build:Effects/losshader_opengl.fx - -#begin Effects/postprocess_opengl.fx -/importer:EffectImporter -/processor:EffectProcessor -/processorParam:DebugMode=Auto -/build:Effects/postprocess_opengl.fx - -#begin Effects/watershader_opengl.fx -/importer:EffectImporter -/processor:EffectProcessor -/processorParam:DebugMode=Auto -/build:Effects/watershader_opengl.fx - diff --git a/Barotrauma/BarotraumaClient/Content/Effects/deformshader.xnb b/Barotrauma/BarotraumaClient/Content/Effects/deformshader.xnb index d40fbb220..35b0ae81b 100644 Binary files a/Barotrauma/BarotraumaClient/Content/Effects/deformshader.xnb and b/Barotrauma/BarotraumaClient/Content/Effects/deformshader.xnb differ diff --git a/Barotrauma/BarotraumaClient/Content/Effects/deformshader_opengl.xnb b/Barotrauma/BarotraumaClient/Content/Effects/deformshader_opengl.xnb index 8d926c6ea..15bed2a78 100644 Binary files a/Barotrauma/BarotraumaClient/Content/Effects/deformshader_opengl.xnb and b/Barotrauma/BarotraumaClient/Content/Effects/deformshader_opengl.xnb differ diff --git a/Barotrauma/BarotraumaClient/Content/Effects/solidcolor.xnb b/Barotrauma/BarotraumaClient/Content/Effects/solidcolor.xnb new file mode 100644 index 000000000..8d2819b44 Binary files /dev/null and b/Barotrauma/BarotraumaClient/Content/Effects/solidcolor.xnb differ diff --git a/Barotrauma/BarotraumaClient/Content/Effects/solidcolor_opengl.xnb b/Barotrauma/BarotraumaClient/Content/Effects/solidcolor_opengl.xnb new file mode 100644 index 000000000..efbff6947 Binary files /dev/null and b/Barotrauma/BarotraumaClient/Content/Effects/solidcolor_opengl.xnb differ diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index fe0c40067..5bfa817fa 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -152,7 +152,14 @@ - + + + + + + + + PreserveNewest diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index a12706abe..5fbe31943 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -153,6 +153,13 @@ + + + + + + + PreserveNewest diff --git a/Barotrauma/BarotraumaClient/Content/Effects/blurshader.fx b/Barotrauma/BarotraumaClient/Shaders/blurshader.fx similarity index 100% rename from Barotrauma/BarotraumaClient/Content/Effects/blurshader.fx rename to Barotrauma/BarotraumaClient/Shaders/blurshader.fx diff --git a/Barotrauma/BarotraumaClient/Content/Effects/blurshader_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/blurshader_opengl.fx similarity index 100% rename from Barotrauma/BarotraumaClient/Content/Effects/blurshader_opengl.fx rename to Barotrauma/BarotraumaClient/Shaders/blurshader_opengl.fx diff --git a/Barotrauma/BarotraumaClient/Content/Effects/damageshader.fx b/Barotrauma/BarotraumaClient/Shaders/damageshader.fx similarity index 100% rename from Barotrauma/BarotraumaClient/Content/Effects/damageshader.fx rename to Barotrauma/BarotraumaClient/Shaders/damageshader.fx diff --git a/Barotrauma/BarotraumaClient/Content/Effects/damageshader_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/damageshader_opengl.fx similarity index 100% rename from Barotrauma/BarotraumaClient/Content/Effects/damageshader_opengl.fx rename to Barotrauma/BarotraumaClient/Shaders/damageshader_opengl.fx diff --git a/Barotrauma/BarotraumaClient/Content/Effects/deformshader.fx b/Barotrauma/BarotraumaClient/Shaders/deformshader.fx similarity index 87% rename from Barotrauma/BarotraumaClient/Content/Effects/deformshader.fx rename to Barotrauma/BarotraumaClient/Shaders/deformshader.fx index bf621efe0..dcf54addd 100644 --- a/Barotrauma/BarotraumaClient/Content/Effects/deformshader.fx +++ b/Barotrauma/BarotraumaClient/Shaders/deformshader.fx @@ -14,6 +14,7 @@ float2 uvTopLeft; float2 uvBottomRight; float4 tintColor; +float4 solidColor; struct VertexShaderInput { @@ -74,6 +75,11 @@ float4 mainPS(VertexShaderOutput input) : COLOR return xTexture.Sample(TextureSampler, input.TexCoords) * input.Color; } +float4 solidColorPS(VertexShaderOutput input) : COLOR +{ + return solidColor * xTexture.Sample(TextureSampler, input.TexCoords).a; +} + technique DeformShader { pass Pass1 @@ -81,4 +87,13 @@ technique DeformShader VertexShader = compile vs_4_0_level_9_1 mainVS(); PixelShader = compile ps_4_0_level_9_1 mainPS(); } +} + +technique DeformShaderSolidColor +{ + pass Pass1 + { + VertexShader = compile vs_4_0_level_9_1 mainVS(); + PixelShader = compile ps_4_0_level_9_1 solidColorPS(); + } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/Content/Effects/deformshader_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/deformshader_opengl.fx similarity index 87% rename from Barotrauma/BarotraumaClient/Content/Effects/deformshader_opengl.fx rename to Barotrauma/BarotraumaClient/Shaders/deformshader_opengl.fx index fbf640ede..c77904ffe 100644 --- a/Barotrauma/BarotraumaClient/Content/Effects/deformshader_opengl.fx +++ b/Barotrauma/BarotraumaClient/Shaders/deformshader_opengl.fx @@ -14,6 +14,7 @@ float2 uvTopLeft; float2 uvBottomRight; float4 tintColor; +float4 solidColor; struct VertexShaderInput { @@ -74,6 +75,11 @@ float4 mainPS(VertexShaderOutput input) : COLOR return xTexture.Sample(TextureSampler, input.TexCoords) * input.Color; } +float4 solidColorPS(VertexShaderOutput input) : COLOR +{ + return solidColor * xTexture.Sample(TextureSampler, input.TexCoords).a; +} + technique DeformShader { pass Pass1 @@ -81,4 +87,13 @@ technique DeformShader VertexShader = compile vs_3_0 mainVS(); PixelShader = compile ps_3_0 mainPS(); } +} + +technique DeformShaderSolidColor +{ + pass Pass1 + { + VertexShader = compile vs_3_0 mainVS(); + PixelShader = compile ps_3_0 solidColorPS(); + } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/Content/Effects/losshader.fx b/Barotrauma/BarotraumaClient/Shaders/losshader.fx similarity index 100% rename from Barotrauma/BarotraumaClient/Content/Effects/losshader.fx rename to Barotrauma/BarotraumaClient/Shaders/losshader.fx diff --git a/Barotrauma/BarotraumaClient/Content/Effects/losshader_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/losshader_opengl.fx similarity index 100% rename from Barotrauma/BarotraumaClient/Content/Effects/losshader_opengl.fx rename to Barotrauma/BarotraumaClient/Shaders/losshader_opengl.fx diff --git a/Barotrauma/BarotraumaClient/Content/Effects/postprocess.fx b/Barotrauma/BarotraumaClient/Shaders/postprocess.fx similarity index 100% rename from Barotrauma/BarotraumaClient/Content/Effects/postprocess.fx rename to Barotrauma/BarotraumaClient/Shaders/postprocess.fx diff --git a/Barotrauma/BarotraumaClient/Content/Effects/postprocess_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/postprocess_opengl.fx similarity index 100% rename from Barotrauma/BarotraumaClient/Content/Effects/postprocess_opengl.fx rename to Barotrauma/BarotraumaClient/Shaders/postprocess_opengl.fx diff --git a/Barotrauma/BarotraumaClient/Content/Effects/watershader.fx b/Barotrauma/BarotraumaClient/Shaders/watershader.fx similarity index 100% rename from Barotrauma/BarotraumaClient/Content/Effects/watershader.fx rename to Barotrauma/BarotraumaClient/Shaders/watershader.fx diff --git a/Barotrauma/BarotraumaClient/Content/Effects/watershader_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/watershader_opengl.fx similarity index 100% rename from Barotrauma/BarotraumaClient/Content/Effects/watershader_opengl.fx rename to Barotrauma/BarotraumaClient/Shaders/watershader_opengl.fx diff --git a/Barotrauma/BarotraumaClient/Source/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaClient/Source/Characters/AI/HumanAIController.cs index 9f108885f..b588b93a4 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/AI/HumanAIController.cs @@ -87,52 +87,5 @@ namespace Barotrauma // GUI.DrawLine(spriteBatch, pos, new Vector2(Character.CursorWorldPosition.X, -Character.CursorWorldPosition.Y), Color.Yellow, width: 4); //} } - - //TODO: move this to the shared project, otherwise bots won't be able to report things in multiplayer - partial void ReportProblems() - { - if (GameMain.Client != null) return; - - Order newOrder = null; - if (Character.CurrentHull != null) - { - if (Character.CurrentHull.FireSources.Count > 0) - { - var orderPrefab = Order.PrefabList.Find(o => o.AITag == "reportfire"); - newOrder = new Order(orderPrefab, Character.CurrentHull, null); - } - - if (Character.CurrentHull.ConnectedGaps.Any(g => !g.IsRoomToRoom && g.ConnectedDoor == null && g.Open > 0.0f)) - { - var orderPrefab = Order.PrefabList.Find(o => o.AITag == "reportbreach"); - newOrder = new Order(orderPrefab, Character.CurrentHull, null); - } - - foreach (Character c in Character.CharacterList) - { - if (c.CurrentHull == Character.CurrentHull && !c.IsDead && - (c.AIController is EnemyAIController || c.TeamID != Character.TeamID)) - { - var orderPrefab = Order.PrefabList.Find(o => o.AITag == "reportintruders"); - newOrder = new Order(orderPrefab, Character.CurrentHull, null); - } - } - } - - if (Character.CurrentHull != null && (Character.Bleeding > 1.0f || Character.Vitality < Character.MaxVitality * 0.1f)) - { - var orderPrefab = Order.PrefabList.Find(o => o.AITag == "requestfirstaid"); - newOrder = new Order(orderPrefab, Character.CurrentHull, null); - } - - if (newOrder != null) - { - if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime)) - { - Character.Speak( - newOrder.GetChatMessage("", Character.CurrentHull?.RoomName, givingOrderToSelf: false), ChatMessageType.Order); - } - } - } } } diff --git a/Barotrauma/BarotraumaClient/Source/Characters/Character.cs b/Barotrauma/BarotraumaClient/Source/Characters/Character.cs index 390c10936..615423b53 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/Character.cs @@ -338,7 +338,7 @@ namespace Barotrauma var entityList = Submarine.VisibleEntities ?? Item.ItemList; Item closestItem = null; - float closestItemDistance = aimAssistAmount; + float closestItemDistance = Math.Max(aimAssistAmount, 2.0f); foreach (MapEntity entity in entityList) { if (!(entity is Item item)) diff --git a/Barotrauma/BarotraumaClient/Source/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/Source/Characters/CharacterHUD.cs index 7c5967485..86c88a3e0 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/CharacterHUD.cs @@ -1,4 +1,5 @@ using Barotrauma.Items.Components; +using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; @@ -136,10 +137,14 @@ namespace Barotrauma brokenItemsCheckTimer = 1.0f; foreach (Item item in Item.ItemList) { - if (item.CurrentHull == character.CurrentHull && item.Repairables.Any(r => item.Condition < r.ShowRepairUIThreshold)) + if (!item.Repairables.Any(r => item.Condition < r.ShowRepairUIThreshold)) { continue; } + if (!Submarine.VisibleEntities.Contains(item)) { continue; } + + Vector2 diff = item.WorldPosition - character.WorldPosition; + if (Submarine.CheckVisibility(character.SimPosition, character.SimPosition + ConvertUnits.ToSimUnits(diff)) == null) { - brokenItems.Add(item); - } + brokenItems.Add(item); + } } } } diff --git a/Barotrauma/BarotraumaClient/Source/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/Source/Characters/CharacterNetworking.cs index 983ffb1a5..4aac62ef9 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/CharacterNetworking.cs @@ -123,10 +123,7 @@ namespace Barotrauma for (int i = 0; i < inputCount; i++) { msg.WriteRangedInteger(0, (int)InputNetFlags.MaxVal, (int)memInput[i].states); - if (memInput[i].states.HasFlag(InputNetFlags.Aim)) - { - msg.Write(memInput[i].intAim); - } + msg.Write(memInput[i].intAim); if (memInput[i].states.HasFlag(InputNetFlags.Select) || memInput[i].states.HasFlag(InputNetFlags.Use) || memInput[i].states.HasFlag(InputNetFlags.Health) || diff --git a/Barotrauma/BarotraumaClient/Source/DebugConsole.cs b/Barotrauma/BarotraumaClient/Source/DebugConsole.cs index 81342069c..6345fd8d7 100644 --- a/Barotrauma/BarotraumaClient/Source/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/Source/DebugConsole.cs @@ -436,13 +436,15 @@ namespace Barotrauma AssignOnExecute("ambientlight", (string[] args) => { - if (Level.Loaded == null) - { - ThrowError("Could not set ambient light color (no level loaded)."); - return; - } Color color = XMLExtensions.ParseColor(string.Join("", args)); - Level.Loaded.GenerationParams.AmbientLightColor = color; + if (Level.Loaded != null) + { + Level.Loaded.GenerationParams.AmbientLightColor = color; + } + else + { + GameMain.LightManager.AmbientLight = color; + } NewMessage("Set ambient light color to " + color + ".", Color.White); }); AssignRelayToServer("ambientlight", false); @@ -1050,6 +1052,195 @@ namespace Barotrauma TextManager.WriteToCSV(); NPCConversation.WriteToCSV(); })); + + commands.Add(new Command("csvtoxml", "csvtoxml [language] -> Converts .csv localization files in Content/NPCConversations & Content/Texts to .xml for use in-game.", (string[] args) => + { + if (args.Length == 0) return; + LocalizationCSVtoXML.Convert(args[0]); + })); +#endif + + commands.Add(new Command("cleanbuild", "", (string[] args) => + { + GameMain.Config.MusicVolume = 0.5f; + GameMain.Config.SoundVolume = 0.5f; + NewMessage("Music and sound volume set to 0.5", Color.Green); + + commands.Add(new Command("camerasettings", "camerasettings [defaultzoom] [zoomsmoothness] [movesmoothness] [minzoom] [maxzoom]: debug command for testing camera settings. The values default to 1.1, 8.0, 8.0, 0.1 and 2.0.", (string[] args) => + { + float defaultZoom = Screen.Selected.Cam.DefaultZoom; + if (args.Length > 0) float.TryParse(args[0], NumberStyles.Number, CultureInfo.InvariantCulture, out defaultZoom); + + float zoomSmoothness = Screen.Selected.Cam.ZoomSmoothness; + if (args.Length > 1) float.TryParse(args[1], NumberStyles.Number, CultureInfo.InvariantCulture, out zoomSmoothness); + float moveSmoothness = Screen.Selected.Cam.MoveSmoothness; + if (args.Length > 2) float.TryParse(args[2], NumberStyles.Number, CultureInfo.InvariantCulture, out moveSmoothness); + + float minZoom = Screen.Selected.Cam.MinZoom; + if (args.Length > 3) float.TryParse(args[3], NumberStyles.Number, CultureInfo.InvariantCulture, out minZoom); + float maxZoom = Screen.Selected.Cam.MaxZoom; + if (args.Length > 4) float.TryParse(args[4], NumberStyles.Number, CultureInfo.InvariantCulture, out maxZoom); + + Screen.Selected.Cam.DefaultZoom = defaultZoom; + Screen.Selected.Cam.ZoomSmoothness = zoomSmoothness; + Screen.Selected.Cam.MoveSmoothness = moveSmoothness; + Screen.Selected.Cam.MinZoom = minZoom; + Screen.Selected.Cam.MaxZoom = maxZoom; + })); + + commands.Add(new Command("waterparams", "waterparams [distortionscalex] [distortionscaley] [distortionstrengthx] [distortionstrengthy] [bluramount]: default 0.5 0.5 0.5 0.5 1", (string[] args) => + { + float distortScaleX = 0.5f, distortScaleY = 0.5f; + float distortStrengthX = 0.5f, distortStrengthY = 0.5f; + float blurAmount = 0.0f; + if (args.Length > 0) float.TryParse(args[0], NumberStyles.Number, CultureInfo.InvariantCulture, out distortScaleX); + if (args.Length > 1) float.TryParse(args[1], NumberStyles.Number, CultureInfo.InvariantCulture, out distortScaleY); + if (args.Length > 2) float.TryParse(args[2], NumberStyles.Number, CultureInfo.InvariantCulture, out distortStrengthX); + if (args.Length > 3) float.TryParse(args[3], NumberStyles.Number, CultureInfo.InvariantCulture, out distortStrengthY); + if (args.Length > 4) float.TryParse(args[4], NumberStyles.Number, CultureInfo.InvariantCulture, out blurAmount); + WaterRenderer.DistortionScale = new Vector2(distortScaleX, distortScaleY); + WaterRenderer.DistortionStrength = new Vector2(distortStrengthX, distortStrengthY); + WaterRenderer.BlurAmount = blurAmount; + })); + + + commands.Add(new Command("refreshrect", "Updates the dimensions of the selected items to match the ones defined in the prefab. Applied only in the subeditor.", (string[] args) => + { + //TODO: maybe do this automatically during loading when possible? + if (Screen.Selected == GameMain.SubEditorScreen) + { + if (!MapEntity.SelectedAny) + { + ThrowError("You have to select item(s) first!"); + } + else + { + foreach (var mapEntity in MapEntity.SelectedList) + { + if (mapEntity is Item item) + { + item.Rect = new Rectangle(item.Rect.X, item.Rect.Y, + (int)(item.Prefab.sprite.size.X * item.Prefab.Scale), + (int)(item.Prefab.sprite.size.Y * item.Prefab.Scale)); + } + else if (mapEntity is Structure structure) + { + if (!structure.ResizeHorizontal) + { + structure.Rect = new Rectangle(structure.Rect.X, structure.Rect.Y, + (int)structure.Prefab.ScaledSize.X, + structure.Rect.Height); + } + if (!structure.ResizeVertical) + { + structure.Rect = new Rectangle(structure.Rect.X, structure.Rect.Y, + structure.Rect.Width, + (int)structure.Prefab.ScaledSize.Y); + } + } + } + } + } + }, isCheat: false)); +#endif + + GameMain.Config.SaveNewPlayerConfig(); + + commands.Add(new Command("loadtexts", "loadtexts [sourcefile] [destinationfile]: Loads all lines of text from a given .txt file and inserts them sequientially into the elements of an xml file. If the file paths are omitted, EnglishVanilla.txt and EnglishVanilla.xml are used.", (string[] args) => + { + string sourcePath = args.Length > 0 ? args[0] : "Content/Texts/EnglishVanilla.txt"; + string destinationPath = args.Length > 1 ? args[1] : "Content/Texts/EnglishVanilla.xml"; + + string[] lines; + try + { + lines = File.ReadAllLines(sourcePath); + } + catch (Exception e) + { + ThrowError("Reading the file \"" + sourcePath + "\" failed.", e); + return; + } + var doc = XMLExtensions.TryLoadXml(destinationPath); + int i = 0; + foreach (XElement element in doc.Root.Elements()) + { + if (i >= lines.Length) + { + ThrowError("Error while loading texts to the xml file. The xml has more elements than the number of lines in the text file."); + return; + } + element.Value = lines[i]; + i++; + } + doc.Save(destinationPath); + }, + () => + { + var files = TextManager.GetTextFiles().Select(f => f.Replace("\\", "/")); + return new string[][] + { + files.Where(f => Path.GetExtension(f)==".txt").ToArray(), + files.Where(f => Path.GetExtension(f)==".xml").ToArray() + }; + })); + + commands.Add(new Command("updatetextfile", "updatetextfile [sourcefile] [destinationfile]: Inserts all the xml elements that are only present in the source file into the destination file. Can be used to update outdated translation files more easily.", (string[] args) => + { + if (args.Length < 2) return; + string sourcePath = args[0]; + string destinationPath = args[1]; + + var sourceDoc = XMLExtensions.TryLoadXml(sourcePath); + var destinationDoc = XMLExtensions.TryLoadXml(destinationPath); + + XElement destinationElement = destinationDoc.Root.Elements().First(); + foreach (XElement element in sourceDoc.Root.Elements()) + { + if (destinationDoc.Root.Element(element.Name) == null) + { + element.Value = "!!!!!!!!!!!!!" + element.Value; + destinationElement.AddAfterSelf(element); + } + XNode nextNode = destinationElement.NextNode; + while ((!(nextNode is XElement) || nextNode == element) && nextNode != null) nextNode = nextNode.NextNode; + destinationElement = nextNode as XElement; + } + destinationDoc.Save(destinationPath); + }, + () => + { + var files = TextManager.GetTextFiles().Where(f => Path.GetExtension(f) == ".xml").Select(f => f.Replace("\\", "/")).ToArray(); + return new string[][] + { + files, + files + }; + })); + + commands.Add(new Command("dumpentitytexts", "dumpentitytexts [filepath]: gets the names and descriptions of all entity prefabs and writes them into a file along with xml tags that can be used in translation files. If the filepath is omitted, the file is written to Content/Texts/EntityTexts.txt", (string[] args) => + { + string filePath = args.Length > 0 ? args[0] : "Content/Texts/EntityTexts.txt"; + List lines = new List(); + foreach (MapEntityPrefab me in MapEntityPrefab.List) + { + lines.Add("" + me.Name + ""); + lines.Add("" + me.Description + ""); + } + File.WriteAllLines(filePath, lines); + })); +#if DEBUG + commands.Add(new Command("checkduplicates", "Checks the given language for duplicate translation keys and writes to file.", (string[] args) => + { + if (args.Length != 1) return; + TextManager.CheckForDuplicates(args[0]); + })); + + commands.Add(new Command("writetocsv", "Writes the default language (English) to a .csv file.", (string[] args) => + { + TextManager.WriteToCSV(); + NPCConversation.WriteToCSV(); + })); #endif commands.Add(new Command("cleanbuild", "", (string[] args) => diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUI.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUI.cs index 7d0daa696..cd8a792c0 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUI.cs @@ -384,12 +384,12 @@ namespace Barotrauma Cursor.Draw(spriteBatch, PlayerInput.LatestMousePosition); } - public static void DrawBackgroundSprite(SpriteBatch spriteBatch, Sprite backgroundSprite) + public static void DrawBackgroundSprite(SpriteBatch spriteBatch, Sprite backgroundSprite, float blurAmount = 1.0f, float aberrationStrength = 1.0f) { double aberrationT = (Timing.TotalTime * 0.5f); - GameMain.GameScreen.PostProcessEffect.Parameters["blurDistance"].SetValue(0.001f); + GameMain.GameScreen.PostProcessEffect.Parameters["blurDistance"].SetValue(0.001f * aberrationStrength); GameMain.GameScreen.PostProcessEffect.Parameters["chromaticAberrationStrength"].SetValue(new Vector3(-0.025f, -0.01f, -0.05f) * - (float)(PerlinNoise.CalculatePerlin(aberrationT, aberrationT, 0) + 0.5f)); + (float)(PerlinNoise.CalculatePerlin(aberrationT, aberrationT, 0) + 0.5f) * aberrationStrength); GameMain.GameScreen.PostProcessEffect.CurrentTechnique = GameMain.GameScreen.PostProcessEffect.Techniques["BlurChromaticAberration"]; GameMain.GameScreen.PostProcessEffect.CurrentTechnique.Passes[0].Apply(); diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/Source/GameSession/CrewManager.cs index 202dc9965..a5968ce0f 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/CrewManager.cs @@ -107,14 +107,37 @@ namespace Barotrauma }; scrollButtonDown.Children.ForEach(c => c.SpriteEffects = SpriteEffects.FlipVertically); - var characterInfo = new CharacterInfo(subElement); - characterInfos.Add(characterInfo); - foreach (XElement invElement in subElement.Elements()) + if (isSinglePlayer) + { + chatBox = new ChatBox(guiFrame, isSinglePlayer: true) { - if (invElement.Name.ToString().ToLowerInvariant() != "inventory") continue; - characterInfo.InventoryData = invElement; - break; - } + OnEnterMessage = (textbox, text) => + { + if (Character.Controlled == null) { return true; } + + textbox.TextColor = ChatMessage.MessageColor[(int)ChatMessageType.Default]; + + if (!string.IsNullOrWhiteSpace(text)) + { + string msgCommand = ChatMessage.GetChatMessageCommand(text, out string msg); + AddSinglePlayerChatMessage( + Character.Controlled.Info.Name, + msg, + ((msgCommand == "r" || msgCommand == "radio") && ChatMessage.CanUseRadio(Character.Controlled)) ? ChatMessageType.Radio : ChatMessageType.Default, + Character.Controlled); + var headset = GetHeadset(Character.Controlled, true); + if (headset != null && headset.CanTransmit()) + { + headset.TransmitSignal(stepsTaken: 0, signal: msg, source: headset.Item, sender: Character.Controlled, sendToChat: false); + } + } + textbox.Deselect(); + textbox.Text = ""; + return true; + } + }; + + chatBox.InputBox.OnTextChanged += chatBox.TypingChatMessage; } var reports = Order.PrefabList.FindAll(o => o.TargetAllCharacters && o.SymbolSprite != null); @@ -136,6 +159,7 @@ namespace Barotrauma { if (Character.Controlled == null || Character.Controlled.SpeechImpediment >= 100.0f) return false; SetCharacterOrder(null, order, null, Character.Controlled); + HumanAIController.PropagateHullSafety(Character.Controlled, Character.Controlled.CurrentHull); return true; }, UserData = order, @@ -152,12 +176,14 @@ namespace Barotrauma Visible = false }; - var img = new GUIImage(new RectTransform(Vector2.One, btn.RectTransform), order.Prefab.SymbolSprite, scaleToFit: true) + var characterInfo = new CharacterInfo(subElement); + characterInfos.Add(characterInfo); + foreach (XElement invElement in subElement.Elements()) { - Color = order.Color, - HoverColor = Color.Lerp(order.Color, Color.White, 0.5f), - ToolTip = order.Name - }; + if (invElement.Name.ToString().ToLowerInvariant() != "inventory") continue; + characterInfo.InventoryData = invElement; + break; + } } screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/GameSession.cs b/Barotrauma/BarotraumaClient/Source/GameSession/GameSession.cs index fc3e1c8f8..16a57092d 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/GameSession.cs @@ -7,10 +7,6 @@ namespace Barotrauma { private InfoFrameTab selectedTab; private GUIButton infoFrame; - /// - /// Determines whether the hotkey for the info button was held down in the previous frame. - /// - private bool prevInfoKey; private GUIFrame infoFrameContent; @@ -19,12 +15,7 @@ namespace Barotrauma { get { return roundSummary; } } - - partial void InitProjSpecific() - { - prevInfoKey = false; - } - + private bool ToggleInfoFrame() { if (infoFrame == null) @@ -45,8 +36,7 @@ namespace Barotrauma int width = 600, height = 400; infoFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas), style: "GUIBackgroundBlocker"); - - + var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.35f), infoFrame.RectTransform, Anchor.Center) { MinSize = new Point(width,height) }); var paddedFrame = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.9f), innerFrame.RectTransform, Anchor.Center), style:null); @@ -135,10 +125,17 @@ namespace Barotrauma { if (GUI.DisableHUD) return; - if (prevInfoKey != (PlayerInput.KeyDown(InputType.InfoTab) && GUI.KeyboardDispatcher.Subscriber == null)) + if (PlayerInput.KeyDown(InputType.InfoTab) && + (GUI.KeyboardDispatcher.Subscriber == null || GUI.KeyboardDispatcher.Subscriber is GUIListBox)) + { + if (infoFrame == null) + { + ToggleInfoFrame(); + } + } + else if (infoFrame != null) { ToggleInfoFrame(); - prevInfoKey = PlayerInput.KeyDown(InputType.InfoTab); } infoFrame?.UpdateManually(deltaTime); diff --git a/Barotrauma/BarotraumaClient/Source/GameSettings.cs b/Barotrauma/BarotraumaClient/Source/GameSettings.cs index a111ddc03..00085898f 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSettings.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSettings.cs @@ -626,28 +626,6 @@ namespace Barotrauma new GUIMessageBox(TextManager.Get("RestartRequiredLabel"), TextManager.Get("RestartRequiredLanguage")); - return true; - }; - - //spacing - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), generalLayoutGroup.RectTransform), style: null); - - new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), buttonArea.RectTransform, Anchor.BottomLeft), - TextManager.Get("Cancel")) - { - IgnoreLayoutGroups = true, - OnClicked = (x, y) => - { - if (UnsavedSettings) - { - LoadPlayerConfig(); - } - if (Screen.Selected == GameMain.MainMenuScreen) GameMain.MainMenuScreen.ReturnToMainMenu(null, null); - GUI.SettingsMenuOpen = false; - return true; - } - }; - //spacing new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), generalLayoutGroup.RectTransform), style: null); diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/ItemComponent.cs index 94a9eee32..72589ccf5 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/ItemComponent.cs @@ -184,63 +184,6 @@ namespace Barotrauma.Items.Components } } } - - public void ApplyTo(RectTransform target) - { - if (RelativeOffset.HasValue) - { - target.RelativeOffset = RelativeOffset.Value; - } - else if (AbsoluteOffset.HasValue) - { - target.AbsoluteOffset = AbsoluteOffset.Value; - } - if (RelativeSize.HasValue) - { - target.RelativeSize = RelativeSize.Value; - } - else if (AbsoluteSize.HasValue) - { - target.NonScaledSize = AbsoluteSize.Value; - } - if (Anchor.HasValue) - { - target.Anchor = Anchor.Value; - } - if (Pivot.HasValue) - { - target.Pivot = Pivot.Value; - } - else - { - target.Pivot = RectTransform.MatchPivotToAnchor(target.Anchor); - } - target.RecalculateChildren(true, true); - } - } - - public GUIFrame GuiFrame { get; protected set; } - - [Serialize(false, false)] - public bool AllowUIOverlap - { - get; - set; - } - - private ItemComponent linkToUIComponent; - [Serialize("", false)] - public string LinkUIToComponent - { - get; - set; - } - - [Serialize(0, false)] - public int HudPriority - { - get; - private set; } private bool shouldMuffleLooping; diff --git a/Barotrauma/BarotraumaClient/Source/Items/Inventory.cs b/Barotrauma/BarotraumaClient/Source/Items/Inventory.cs index 96adb2e97..44c39061f 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Inventory.cs @@ -598,7 +598,7 @@ namespace Barotrauma if (selectedSlot == null) { - draggingItem.Drop(); + draggingItem.Drop(Character.Controlled); GUI.PlayUISound(GUISoundType.DropItem); } else if (selectedSlot.ParentInventory.Items[selectedSlot.SlotIndex] != draggingItem) @@ -954,7 +954,7 @@ namespace Barotrauma { if (receivedItemIDs[i] == 0 || (Entity.FindEntityByID(receivedItemIDs[i]) as Item != Items[i])) { - if (Items[i] != null) Items[i].Drop(); + if (Items[i] != null) Items[i].Drop(null); System.Diagnostics.Debug.Assert(Items[i] == null); } } diff --git a/Barotrauma/BarotraumaClient/Source/Items/Item.cs b/Barotrauma/BarotraumaClient/Source/Items/Item.cs index 9c7b97434..2f1a0c511 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Item.cs @@ -849,6 +849,11 @@ namespace Barotrauma { if (GameMain.Client == null) { return; } + if (parentInventory != null || body == null || !body.Enabled || Removed) + { + return; + } + Vector2 newVelocity = body.LinearVelocity; Vector2 newPosition = body.SimPosition; float newAngularVelocity = body.AngularVelocity; diff --git a/Barotrauma/BarotraumaClient/Source/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/Source/Networking/GameClient.cs index 1abfdefb6..98b7cd3bc 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/GameClient.cs @@ -202,6 +202,12 @@ namespace Barotrauma.Networking private void ConnectToServer(string hostIP) { + chatBox.InputBox.Enabled = false; + if (GameMain.NetLobbyScreen?.TextBox != null) + { + GameMain.NetLobbyScreen.TextBox.Enabled = false; + } + string[] address = hostIP.Split(':'); if (address.Length == 1) { @@ -259,7 +265,11 @@ namespace Barotrauma.Networking { DebugConsole.ThrowError("Couldn't connect to " + hostIP + ". Error message: " + e.Message); Disconnect(); - + chatBox.InputBox.Enabled = true; + if (GameMain.NetLobbyScreen?.TextBox != null) + { + GameMain.NetLobbyScreen.TextBox.Enabled = true; + } GameMain.ServerListScreen.Select(); return; } @@ -530,6 +540,10 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.Select(); } connected = true; + if (GameMain.NetLobbyScreen?.TextBox != null) + { + GameMain.NetLobbyScreen.TextBox.Enabled = true; + } } yield return CoroutineStatus.Success; diff --git a/Barotrauma/BarotraumaClient/Source/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/Source/Screens/CampaignUI.cs index 258eb60de..12d87cf51 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/CampaignUI.cs @@ -118,7 +118,18 @@ namespace Barotrauma CanBeFocused = false }; - characterList = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.95f), tabs[(int)Tab.Crew].RectTransform, Anchor.Center)) + var crewContent = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), tabs[(int)Tab.Crew].RectTransform, Anchor.Center)) + { + Stretch = true, + RelativeSpacing = 0.02f + }; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), crewContent.RectTransform), "", font: GUI.LargeFont) + { + TextGetter = GetMoney + }; + + characterList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.9f), crewContent.RectTransform)) { OnSelected = SelectCharacter }; diff --git a/Barotrauma/BarotraumaClient/Source/Screens/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/Source/Screens/CharacterEditorScreen.cs index 523b82cc7..ce3420604 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/CharacterEditorScreen.cs @@ -438,6 +438,43 @@ namespace Barotrauma UpdateSourceRect(limb, newRect); } } + UpdateJointCreation(); + if (PlayerInput.KeyHit(Keys.Left)) + { + foreach (var limb in selectedLimbs) + { + var newRect = limb.ActiveSprite.SourceRect; + newRect.X--; + UpdateSourceRect(limb, newRect); + } + } + if (PlayerInput.KeyHit(Keys.Right)) + { + foreach (var limb in selectedLimbs) + { + var newRect = limb.ActiveSprite.SourceRect; + newRect.X++; + UpdateSourceRect(limb, newRect); + } + } + if (PlayerInput.KeyHit(Keys.Down)) + { + foreach (var limb in selectedLimbs) + { + var newRect = limb.ActiveSprite.SourceRect; + newRect.Y++; + UpdateSourceRect(limb, newRect); + } + } + if (PlayerInput.KeyHit(Keys.Up)) + { + foreach (var limb in selectedLimbs) + { + var newRect = limb.ActiveSprite.SourceRect; + newRect.Y--; + UpdateSourceRect(limb, newRect); + } + } } if (!isFreezed) { diff --git a/Barotrauma/BarotraumaClient/Source/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/Source/Screens/MainMenuScreen.cs index ca9b6c880..bb123c052 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/MainMenuScreen.cs @@ -38,9 +38,18 @@ namespace Barotrauma #region Creation public MainMenuScreen(GameMain game) { + new GUIImage(new RectTransform(new Vector2(0.35f, 0.2f), Frame.RectTransform, Anchor.BottomRight) + { RelativeOffset = new Vector2(0.05f, 0.05f), AbsoluteOffset = new Point(-5, -5) }, + style: "TitleText") + { + Color = Color.Black * 0.5f, + CanBeFocused = false + }; + new GUIImage(new RectTransform(new Vector2(0.35f, 0.2f), Frame.RectTransform, Anchor.BottomRight) { RelativeOffset = new Vector2(0.05f, 0.05f) }, + style: "TitleText"); + buttonsParent = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 0.85f), parent: Frame.RectTransform, anchor: Anchor.BottomLeft, pivot: Pivot.BottomLeft) { - RelativeOffset = new Vector2(0, 0), AbsoluteOffset = new Point(50, 0) }) { @@ -66,7 +75,7 @@ namespace Barotrauma var campaignButtons = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.0f), parent: campaignNavigation.RectTransform), style: "MainMenuGUIFrame"); - var campaignList = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.15f), parent: campaignButtons.RectTransform)) + var campaignList = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.2f), parent: campaignButtons.RectTransform)) { Stretch = false, RelativeSpacing = 0.035f @@ -112,7 +121,7 @@ namespace Barotrauma var multiplayerButtons = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.0f), parent: multiplayerNavigation.RectTransform), style: "MainMenuGUIFrame"); - var multiplayerList = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.15f), parent: multiplayerButtons.RectTransform)) + var multiplayerList = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.2f), parent: multiplayerButtons.RectTransform)) { Stretch = false, RelativeSpacing = 0.035f @@ -157,7 +166,7 @@ namespace Barotrauma var customizeButtons = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.0f), parent: customizeNavigation.RectTransform), style: "MainMenuGUIFrame"); - var customizeList = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.15f), parent: customizeButtons.RectTransform)) + var customizeList = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.2f), parent: customizeButtons.RectTransform)) { Stretch = false, RelativeSpacing = 0.035f @@ -673,7 +682,12 @@ namespace Barotrauma backgroundSprite = (LocationType.List.Where(l => l.UseInMainMenu).GetRandom()).GetPortrait(0); } - if (backgroundSprite != null) { GUI.DrawBackgroundSprite(spriteBatch, backgroundSprite); } + if (backgroundSprite != null) + { + GUI.DrawBackgroundSprite(spriteBatch, backgroundSprite, + blurAmount: 0.0f, + aberrationStrength: 0.0f); + } } public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) diff --git a/Barotrauma/BarotraumaClient/Source/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/Source/Screens/SubEditorScreen.cs index 4469d6211..022395eb3 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/SubEditorScreen.cs @@ -625,8 +625,7 @@ namespace Barotrauma { foreach (Item item in itemInventory.Items) { - if (item == null) continue; - item.Drop(); + item?.Drop(null); } } else // If current screen is not subeditor, delete anyway to avoid lingering objects @@ -671,8 +670,7 @@ namespace Barotrauma { foreach (Item item in itemInventory.Items) { - if (item == null) continue; - item.Drop(); + item?.Drop(null); } } else // If current screen is not subeditor, delete anyway to avoid lingering objects @@ -703,8 +701,7 @@ namespace Barotrauma { foreach (Item item in itemInventory.Items) { - if (item == null) continue; - item.Drop(); + item?.Drop(null); } } else // If current screen is not subeditor, delete anyway to avoid lingering objects @@ -1457,7 +1454,7 @@ namespace Barotrauma Item existingWire = dummyCharacter.SelectedItems.FirstOrDefault(i => i != null && i.Prefab == userData as ItemPrefab); if (existingWire != null) { - existingWire.Drop(); + existingWire.Drop(null); existingWire.Remove(); return false; } @@ -1470,7 +1467,7 @@ namespace Barotrauma existingWire = dummyCharacter.Inventory.Items[slotIndex]; if (existingWire != null && existingWire.Prefab != userData as ItemPrefab) { - existingWire.Drop(); + existingWire.Drop(null); existingWire.Remove(); } diff --git a/Barotrauma/BarotraumaClient/Source/Sounds/SoundManager.cs b/Barotrauma/BarotraumaClient/Source/Sounds/SoundManager.cs index e299e79cd..4256482ea 100644 --- a/Barotrauma/BarotraumaClient/Source/Sounds/SoundManager.cs +++ b/Barotrauma/BarotraumaClient/Source/Sounds/SoundManager.cs @@ -109,6 +109,12 @@ namespace Barotrauma.Sounds { get { return loadedSounds.Select(s => s.Filename).Distinct().Count(); } } + public int UniqueLoadedSoundCount + { + get { return loadedSounds.Select(s => s.Filename).Distinct().Count(); } + } + + private Dictionary> categoryModifiers; private Dictionary> categoryModifiers; @@ -418,6 +424,10 @@ namespace Barotrauma.Sounds { categoryModifiers[category].Second = muffle; } + else + { + categoryModifiers[category].Second = muffle; + } } for (int i = 0; i < playingChannels.Length; i++) diff --git a/Barotrauma/BarotraumaClient/Source/Utils/LocalizationCSVtoXML.cs b/Barotrauma/BarotraumaClient/Source/Utils/LocalizationCSVtoXML.cs new file mode 100644 index 000000000..e908621bc --- /dev/null +++ b/Barotrauma/BarotraumaClient/Source/Utils/LocalizationCSVtoXML.cs @@ -0,0 +1,246 @@ +#if DEBUG +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; + +namespace Barotrauma +{ + class LocalizationCSVtoXML + { + private static Regex csvSplit = new Regex("(?:^|,)(\"(?:[^\"])*\"|[^,]*)", RegexOptions.Compiled); // Handling commas inside data fields surrounded by "" + private static List conversationClosingIndent = new List(); + private static char[] separator = new char[1] { ',' }; + + private const string conversationsPath = "Content/NPCConversations"; + private const string infoTextPath = "Content/Texts"; + private const string xmlHeader = ""; + + public static void Convert(string language) + { + List conversationFiles = new List(); + List infoTextFiles = new List(); + + language = language.CapitaliseFirstInvariant(); + + foreach (string filePath in Directory.GetFiles(conversationsPath, "*.csv", SearchOption.AllDirectories)) + { + conversationFiles.Add(filePath); + } + + foreach (string filePath in Directory.GetFiles(infoTextPath, "*.csv", SearchOption.AllDirectories)) + { + infoTextFiles.Add(filePath); + } + + for (int i = 0; i < conversationFiles.Count; i++) + { + List xmlContent = ConvertConversationsToXML(File.ReadAllLines(conversationFiles[i], Encoding.UTF8), language); + string xmlFileFullPath = $"{conversationsPath}/NPCConversations_{language}_NEW.xml"; + File.WriteAllLines(xmlFileFullPath, xmlContent); + DebugConsole.NewMessage("Conversation localization .xml file successfully created at: " + xmlFileFullPath); + } + + for (int i = 0; i < infoTextFiles.Count; i++) + { + List xmlContent = ConvertInfoTextToXML(File.ReadAllLines(infoTextFiles[i], Encoding.UTF8), language); + string xmlFileFullPath = $"{infoTextPath}/{language}Vanilla_NEW.xml"; + File.WriteAllLines(xmlFileFullPath, xmlContent); + DebugConsole.NewMessage("InfoText localization .xml file successfully created at: " + xmlFileFullPath); + } + + if (conversationFiles.Count == 0 && infoTextFiles.Count == 0) + { + DebugConsole.ThrowError("No .csv files found to convert."); + } + } + + private static List ConvertInfoTextToXML(string[] csvContent, string language) + { + List xmlContent = new List(); + xmlContent.Add(xmlHeader); + + xmlContent.Add($""); + xmlContent.Add(string.Empty); + + for (int i = 0; i < csvContent.Length; i++) + { + csvContent[i] = csvContent[i].Trim(separator); + + if (csvContent[i].Length == 0) + { + xmlContent.Add(string.Empty); + } + else + { + string[] split = csvContent[i].Split(separator, 2); + + if (split.Length == 2) + { + split[1] = split[1].Replace("\"", ""); // Replaces quotation marks around data that are added when exporting via excel + xmlContent.Add($"<{split[0]}>{split[1]}"); + } + else if (split[0].Contains(".")) // An empty field + { + xmlContent.Add($"<{split[0]}>"); + } + else // A header + { + xmlContent.Add($""); + } + } + } + + xmlContent.Add(string.Empty); + xmlContent.Add(""); + + return xmlContent; + } + + private static List ConvertConversationsToXML(string[] csvContent, string language) + { + List xmlContent = new List(); + xmlContent.Add(xmlHeader); + + xmlContent.Add($""); + xmlContent.Add(string.Empty); + + for (int i = 0; i < csvContent.Length; i++) + { + string[] split = SplitCSV(csvContent[i]); + + int emptyFields = 0; + + for (int j = 0; j < split.Length; j++) + { + if (split[j] == string.Empty) emptyFields++; + } + + if (emptyFields == split.Length) // Empty line with only commas, indicates the end of the previous conversation + { + HandleClosingElements(xmlContent, 0); + xmlContent.Add(string.Empty); + continue; + } + else if (emptyFields == split.Length - 1 && split[0] != string.Empty) // A header + { + xmlContent.Add($""); + continue; + } + + string speaker = split[1]; + int depthIndex = int.Parse(split[2]); + // 3 = original line + string line = split[4].Replace("\"", ""); + string flags = split[5].Replace("\"", ""); + string allowedJobs = split[6].Replace("\"", ""); + string speakerTags = split[7].Replace("\"", ""); + string minIntensity = split[8].Replace("\"", ""); + string maxIntensity = split[9].Replace("\"", ""); + + string element = + $"{GetIndenting(depthIndex)}" + + $" depthIndex; + } + + if (!nextIsSubConvo) + { + xmlContent.Add(element.TrimEnd() + "/>"); + if (nextDepth < depthIndex) + { + HandleClosingElements(xmlContent, nextDepth); + } + } + else + { + xmlContent.Add(element.TrimEnd() + ">"); + conversationClosingIndent.Add(depthIndex); + } + } + else + { + xmlContent.Add(element.TrimEnd() + "/>"); + } + } + + xmlContent.Add(string.Empty); + xmlContent.Add(""); + + return xmlContent; + } + + private static void HandleClosingElements(List xmlContent, int targetDepth) + { + if (conversationClosingIndent.Count == 0) return; + + for (int k = conversationClosingIndent.Count - 1; k >= 0; k--) + { + int currentIndent = conversationClosingIndent[k]; + if (currentIndent < targetDepth) break; + xmlContent.Add($"{GetIndenting(currentIndent)}"); + conversationClosingIndent.RemoveAt(k); + } + } + + private static string[] SplitCSV(string input) // Splits the .csv with regex, leaving commas inside quotation marks intact + { + List list = new List(); + string curr = null; + foreach (Match match in csvSplit.Matches(input)) + { + curr = match.Value; + if (0 == curr.Length) + { + list.Add(""); + } + + list.Add(curr.TrimStart(',')); + } + + return list.ToArray(); + } + + private static string GetIndenting(int depthIndex) + { + string indenting = string.Empty; + + for (int i = 0; i < depthIndex; i++) + { + indenting += "\t"; + } + + return indenting; + } + + private static string GetVariable(string name, string value) + { + if (value == string.Empty) + { + return string.Empty; + } + else + { + return $"{name}=\"{value}\" "; + } + } + } +} +#endif diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 0ace19fec..2c4bc8e82 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -169,6 +169,12 @@ PreserveNewest + + + + + + PreserveNewest @@ -250,6 +256,7 @@ PreserveNewest + diff --git a/Barotrauma/BarotraumaServer/Source/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/Source/Characters/CharacterNetworking.cs index 3c37eb862..95f9d1e53 100644 --- a/Barotrauma/BarotraumaServer/Source/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/Source/Characters/CharacterNetworking.cs @@ -166,11 +166,8 @@ namespace Barotrauma //character changed the direction they're facing c.KickAFKTimer = 0.0f; } - - if (newInput.HasFlag(InputNetFlags.Aim)) - { - newAim = msg.ReadUInt16(); - } + + newAim = msg.ReadUInt16(); if (newInput.HasFlag(InputNetFlags.Select) || newInput.HasFlag(InputNetFlags.Use) || newInput.HasFlag(InputNetFlags.Health) || @@ -181,12 +178,15 @@ namespace Barotrauma if (NetIdUtils.IdMoreRecent((ushort)(networkUpdateID - i), LastNetworkUpdateID) && (i < 60)) { + if ((i > 0 && memInput[i - 1].intAim != newAim)) + { + c.KickAFKTimer = 0.0f; + } NetInputMem newMem = new NetInputMem { states = newInput, intAim = newAim, interact = newInteract, - networkUpdateID = (ushort)(networkUpdateID - i) }; memInput.Insert(i, newMem); diff --git a/Barotrauma/BarotraumaServer/Source/Items/Inventory.cs b/Barotrauma/BarotraumaServer/Source/Items/Inventory.cs index 3dd111358..6d13e1aaf 100644 --- a/Barotrauma/BarotraumaServer/Source/Items/Inventory.cs +++ b/Barotrauma/BarotraumaServer/Source/Items/Inventory.cs @@ -62,7 +62,7 @@ namespace Barotrauma if (newItemIDs[i] == 0 || (newItem != Items[i])) { - if (Items[i] != null) Items[i].Drop(); + if (Items[i] != null) Items[i].Drop(null); System.Diagnostics.Debug.Assert(Items[i] == null); } } diff --git a/Barotrauma/BarotraumaServer/Source/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/Source/Networking/GameServer.cs index 9eb5a5e2f..666aad960 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/GameServer.cs @@ -355,8 +355,10 @@ namespace Barotrauma.Networking entityEventManager.Update(connectedClients); - foreach (Character character in Character.CharacterList) + //go through the characters backwards to give rejoining clients control of the latest created character + for (int i = Character.CharacterList.Count - 1; i >= 0; i--) { + Character character = Character.CharacterList[i]; if (character.IsDead || !character.ClientDisconnected) continue; character.KillDisconnectedTimer += deltaTime; @@ -1957,8 +1959,8 @@ namespace Barotrauma.Networking if (client == null) return; if (client.Connection == OwnerConnection) return; - string msg = DisconnectReason.Banned.ToString(); - DisconnectClient(client, $"ServerMessage.BannedFromServer~[client]={client.Name}", msg, reason); + string targetMsg = DisconnectReason.Banned.ToString(); + DisconnectClient(client, $"ServerMessage.BannedFromServer~[client]={client.Name}", targetMsg, reason); if (client.SteamID == 0 || range) { @@ -2764,6 +2766,747 @@ namespace Barotrauma.Networking } } + public override void Disconnect() + { + serverSettings.BanList.Save(); + serverSettings.SaveSettings(); + SteamManager.CloseServer(); + + if (registeredToMaster) + { + 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"); + server.Shutdown(DisconnectReason.ServerShutdown.ToString()); + } + + public void SendConsoleMessage(string txt, Client recipient) + { + ChatMessage msg = ChatMessage.Create("", txt, ChatMessageType.Console, null); + SendDirectChatMessage(msg, recipient); + } + + public void SendDirectChatMessage(ChatMessage msg, Client recipient) + { + if (recipient == null) + { + string errorMsg = "Attempted to send a chat message to a null client.\n" + Environment.StackTrace; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("GameServer.SendDirectChatMessage:ClientNull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + return; + } + + msg.NetStateID = recipient.ChatMsgQueue.Count > 0 ? + (ushort)(recipient.ChatMsgQueue.Last().NetStateID + 1) : + (ushort)(recipient.LastRecvChatMsgID + 1); + + recipient.ChatMsgQueue.Add(msg); + recipient.LastChatMsgQueueID = msg.NetStateID; + } + + /// + /// Add the message to the chatbox and pass it to all clients who can receive it + /// + public void SendChatMessage(string message, ChatMessageType? type = null, Client senderClient = null, Character senderCharacter = null) + { + string senderName = ""; + + Client targetClient = null; + + if (type == null) + { + string command = ChatMessage.GetChatMessageCommand(message, out string tempStr); + switch (command.ToLowerInvariant()) + { + case "r": + case "radio": + type = ChatMessageType.Radio; + break; + case "d": + case "dead": + type = ChatMessageType.Dead; + break; + default: + if (command != "") + { + if (command == name.ToLowerInvariant()) + { + //a private message to the host + } + else + { + targetClient = connectedClients.Find(c => + command == c.Name.ToLowerInvariant() || + (c.Character != null && command == c.Character.Name.ToLowerInvariant())); + + if (targetClient == null) + { + if (senderClient != null) + { + 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; + } + else + { + AddChatMessage($"ServerMessage.PlayerNotFound~[player]={command}", ChatMessageType.Error); + } + + return; + } + } + + type = ChatMessageType.Private; + } + else + { + type = ChatMessageType.Default; + } + break; + } + + message = tempStr; + } + + if (gameStarted) + { + if (senderClient == null) + { + //msg sent by the server + if (senderCharacter == null) + { + senderName = name; + } + else //msg sent by an AI character + { + senderName = senderCharacter.Name; + } + } + else //msg sent by a client + { + senderCharacter = senderClient.Character; + senderName = senderCharacter == null ? senderClient.Name : senderCharacter.Name; + + //sender doesn't have a character or the character can't speak -> only ChatMessageType.Dead allowed + if (senderCharacter == null || senderCharacter.IsDead || senderCharacter.SpeechImpediment >= 100.0f) + { + type = ChatMessageType.Dead; + } + else if (type == ChatMessageType.Private) + { + //sender has an alive character, sending private messages not allowed + return; + } + + } + } + else + { + if (senderClient == null) + { + //msg sent by the server + if (senderCharacter == null) + { + senderName = name; + } + else //sent by an AI character, not allowed when the game is not running + { + return; + } + } + else //msg sent by a client + { + //game not started -> clients can only send normal and private chatmessages + if (type != ChatMessageType.Private) type = ChatMessageType.Default; + senderName = senderClient.Name; + } + } + + //check if the client is allowed to send the message + WifiComponent senderRadio = null; + switch (type) + { + case ChatMessageType.Radio: + case ChatMessageType.Order: + if (senderCharacter == null) return; + + //return if senderCharacter doesn't have a working radio + var radio = senderCharacter.Inventory?.Items.FirstOrDefault(i => i != null && i.GetComponent() != null); + if (radio == null || !senderCharacter.HasEquippedItem(radio)) return; + + senderRadio = radio.GetComponent(); + if (!senderRadio.CanTransmit()) return; + break; + case ChatMessageType.Dead: + //character still alive and capable of speaking -> dead chat not allowed + if (senderClient != null && senderCharacter != null && !senderCharacter.IsDead && senderCharacter.SpeechImpediment < 100.0f) + { + return; + } + break; + } + + if (type == ChatMessageType.Server) + { + senderName = null; + senderCharacter = null; + } + else if (type == ChatMessageType.Radio) + { + //send to chat-linked wifi components + senderRadio.TransmitSignal(0, message, senderRadio.Item, senderCharacter, false); + } + + //check which clients can receive the message and apply distance effects + foreach (Client client in ConnectedClients) + { + string modifiedMessage = message; + + switch (type) + { + case ChatMessageType.Default: + case ChatMessageType.Radio: + case ChatMessageType.Order: + if (senderCharacter != null && + client.Character != null && !client.Character.IsDead) + { + modifiedMessage = ChatMessage.ApplyDistanceEffect(message, (ChatMessageType)type, senderCharacter, client.Character); + + //too far to hear the msg -> don't send + if (string.IsNullOrWhiteSpace(modifiedMessage)) continue; + } + break; + case ChatMessageType.Dead: + //character still alive -> don't send + if (client != senderClient && client.Character != null && !client.Character.IsDead) continue; + break; + case ChatMessageType.Private: + //private msg sent to someone else than this client -> don't send + if (client != targetClient && client != senderClient) continue; + break; + } + + var chatMsg = ChatMessage.Create( + senderName, + modifiedMessage, + (ChatMessageType)type, + senderCharacter); + + SendDirectChatMessage(chatMsg, client); + } + + 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)) + { + AddChatMessage(myReceivedMessage, (ChatMessageType)type, senderName, senderCharacter); + } + } + } + + public void SendOrderChatMessage(OrderChatMessage message) + { + if (message.Sender == null || message.Sender.SpeechImpediment >= 100.0f) return; + ChatMessageType messageType = ChatMessage.CanUseRadio(message.Sender) ? ChatMessageType.Radio : ChatMessageType.Default; + + //check which clients can receive the message and apply distance effects + foreach (Client client in ConnectedClients) + { + string modifiedMessage = message.Text; + + if (message.Sender != null && + client.Character != null && !client.Character.IsDead) + { + modifiedMessage = ChatMessage.ApplyDistanceEffect(message.Text, messageType, message.Sender, client.Character); + + //too far to hear the msg -> don't send + if (string.IsNullOrWhiteSpace(modifiedMessage)) continue; + } + + SendDirectChatMessage(message, client); + } + + string myReceivedMessage = message.Text; + + if (!string.IsNullOrWhiteSpace(myReceivedMessage)) + { + AddChatMessage(new OrderChatMessage(message.Order, message.OrderOption, myReceivedMessage, message.TargetEntity, message.TargetCharacter, message.Sender)); + } + } + + private void FileTransferChanged(FileSender.FileTransferOut transfer) + { + Client recipient = connectedClients.Find(c => c.Connection == transfer.Connection); + if (transfer.FileType == FileTransferType.CampaignSave && + (transfer.Status == FileTransferStatus.Sending || transfer.Status == FileTransferStatus.Finished) && + recipient.LastCampaignSaveSendTime != null) + { + recipient.LastCampaignSaveSendTime.Second = (float)NetTime.Now; + } + } + + public void SendCancelTransferMsg(FileSender.FileTransferOut transfer) + { + NetOutgoingMessage msg = server.CreateMessage(); + 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); + } + + public void UpdateVoteStatus() + { + if (server.Connections.Count == 0 || connectedClients.Count == 0) return; + + Client.UpdateKickVotes(connectedClients); + + var clientsToKick = connectedClients.FindAll(c => + c.Connection != OwnerConnection && + c.KickVoteCount >= connectedClients.Count * serverSettings.KickVoteRequiredRatio); + foreach (Client c in clientsToKick) + { + SendChatMessage($"ServerMessage.KickedFromServer~[client]={c.Name}", ChatMessageType.Server, null); + KickClient(c, "ServerMessage.KickedByVote"); + BanClient(c, "ServerMessage.KickedByVoteAutoBan", duration: TimeSpan.FromSeconds(serverSettings.AutoBanTime)); + } + + GameMain.NetLobbyScreen.LastUpdateID++; + + SendVoteStatus(connectedClients); + + if (serverSettings.Voting.AllowEndVoting && EndVoteMax > 0 && + ((float)EndVoteCount / (float)EndVoteMax) >= serverSettings.EndVoteRequiredRatio) + { + Log("Ending round by votes (" + EndVoteCount + "/" + (EndVoteMax - EndVoteCount) + ")", ServerLog.MessageType.ServerMessage); + EndGame(); + } + } + + public void SendVoteStatus(List recipients) + { + NetOutgoingMessage msg = server.CreateMessage(); + 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); + } + + public void UpdateClientPermissions(Client client) + { + if (client.SteamID > 0) + { + serverSettings.ClientPermissions.RemoveAll(cp => cp.SteamID == client.SteamID); + if (client.Permissions != ClientPermissions.None) + { + serverSettings.ClientPermissions.Add(new ServerSettings.SavedClientPermission( + client.Name, + client.SteamID, + client.Permissions, + client.PermittedConsoleCommands)); + } + } + else + { + serverSettings.ClientPermissions.RemoveAll(cp => client.IPMatches(cp.IP)); + if (client.Permissions != ClientPermissions.None) + { + serverSettings.ClientPermissions.Add(new ServerSettings.SavedClientPermission( + client.Name, + client.Connection.RemoteEndPoint.Address, + client.Permissions, + client.PermittedConsoleCommands)); + } + } + + var msg = server.CreateMessage(); + 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 }; + foreach (Client otherClient in connectedClients) + { + if (otherClient.HasPermission(ClientPermissions.ManagePermissions) && !recipients.Contains(otherClient.Connection)) + { + recipients.Add(otherClient.Connection); + } + } + server.SendMessage(msg, recipients, NetDeliveryMethod.ReliableUnordered, 0); + + serverSettings.SaveClientPermissions(); + } + + public void GiveAchievement(Character character, string achievementIdentifier) + { + achievementIdentifier = achievementIdentifier.ToLowerInvariant(); + foreach (Client client in connectedClients) + { + if (client.Character == character) + { + GiveAchievement(client, achievementIdentifier); + return; + } + } + } + + public void GiveAchievement(Client client, string achievementIdentifier) + { + if (client.GivenAchievements.Contains(achievementIdentifier)) return; + client.GivenAchievements.Add(achievementIdentifier); + + var msg = server.CreateMessage(); + msg.Write((byte)ServerPacketHeader.ACHIEVEMENT); + msg.Write(achievementIdentifier); + + CompressOutgoingMessage(msg); + + server.SendMessage(msg, client.Connection, NetDeliveryMethod.ReliableUnordered); + } + + public void UpdateCheatsEnabled() + { + var msg = server.CreateMessage(); + 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); + } + + public void SetClientCharacter(Client client, Character newCharacter) + { + if (client == null) return; + + //the client's previous character is no longer a remote player + if (client.Character != null) + { + client.Character.IsRemotePlayer = false; + client.Character.OwnerClientIP = null; + client.Character.OwnerClientName = null; + } + + if (newCharacter == null) + { + if (client.Character != null) //removing control of the current character + { + CreateEntityEvent(client.Character, new object[] { NetEntityEvent.Type.Control, null }); + client.Character = null; + } + } + else //taking control of a new character + { + newCharacter.ClientDisconnected = false; + newCharacter.KillDisconnectedTimer = 0.0f; + newCharacter.ResetNetState(); + if (client.Character != null) + { + newCharacter.LastNetworkUpdateID = client.Character.LastNetworkUpdateID; + } + + newCharacter.OwnerClientIP = client.Connection.RemoteEndPoint.Address.ToString(); + newCharacter.OwnerClientName = client.Name; + newCharacter.IsRemotePlayer = true; + newCharacter.Enabled = true; + client.Character = newCharacter; + CreateEntityEvent(newCharacter, new object[] { NetEntityEvent.Type.Control, client }); + } + } + + private void UpdateCharacterInfo(NetIncomingMessage message, Client sender) + { + sender.SpectateOnly = message.ReadBoolean() && (serverSettings.AllowSpectating || sender.Connection == OwnerConnection); + if (sender.SpectateOnly) + { + return; + } + + Gender gender = Gender.Male; + Race race = Race.White; + int headSpriteId = 0; + try + { + gender = (Gender)message.ReadByte(); + race = (Race)message.ReadByte(); + headSpriteId = message.ReadByte(); + } + catch (Exception e) + { + //gender = Gender.Male; + //race = Race.White; + //headSpriteId = 0; + DebugConsole.Log("Received invalid characterinfo from \"" + sender.Name + "\"! { " + e.Message + " }"); + } + int hairIndex = message.ReadByte(); + int beardIndex = message.ReadByte(); + int moustacheIndex = message.ReadByte(); + int faceAttachmentIndex = message.ReadByte(); + + List jobPreferences = new List(); + int count = message.ReadByte(); + for (int i = 0; i < Math.Min(count, 3); i++) + { + string jobIdentifier = message.ReadString(); + + JobPrefab jobPrefab = JobPrefab.List.Find(jp => jp.Identifier == jobIdentifier); + if (jobPrefab != null) jobPreferences.Add(jobPrefab); + } + + sender.CharacterInfo = new CharacterInfo(Character.HumanConfigFile, sender.Name); + sender.CharacterInfo.RecreateHead(headSpriteId, race, gender, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); + + //if the client didn't provide job preferences, we'll use the preferences that are randomly assigned in the Client constructor + Debug.Assert(sender.JobPreferences.Count > 0); + if (jobPreferences.Count > 0) + { + sender.JobPreferences = jobPreferences; + } + } + + public void AssignJobs(List unassigned) + { + unassigned = new List(unassigned); + + Dictionary assignedClientCount = new Dictionary(); + foreach (JobPrefab jp in JobPrefab.List) + { + assignedClientCount.Add(jp, 0); + } + + Character.TeamType teamID = Character.TeamType.None; + if (unassigned.Count > 0) { teamID = unassigned[0].TeamID; } + + //if we're playing a multiplayer campaign, check which clients already have a character and a job + //(characters are persistent in campaigns) + if (GameMain.GameSession.GameMode is MultiPlayerCampaign multiplayerCampaign) + { + var campaignAssigned = multiplayerCampaign.GetAssignedJobs(connectedClients); + //remove already assigned clients from unassigned + unassigned.RemoveAll(u => campaignAssigned.ContainsKey(u)); + //add up to assigned client count + foreach (KeyValuePair clientJob in campaignAssigned) + { + assignedClientCount[clientJob.Value.Prefab]++; + clientJob.Key.AssignedJob = clientJob.Value.Prefab; + } + } + + //count the clients who already have characters with an assigned job + foreach (Client c in connectedClients) + { + if (c.TeamID != teamID || unassigned.Contains(c)) continue; + if (c.Character?.Info?.Job != null && !c.Character.IsDead) + { + assignedClientCount[c.Character.Info.Job.Prefab]++; + } + } + + //if any of the players has chosen a job that is Always Allowed, give them that job + for (int i = unassigned.Count - 1; i >= 0; i--) + { + if (unassigned[i].JobPreferences.Count == 0) continue; + if (!unassigned[i].JobPreferences[0].AllowAlways) continue; + unassigned[i].AssignedJob = unassigned[i].JobPreferences[0]; + unassigned.RemoveAt(i); + } + + //go throught the jobs whose MinNumber>0 (i.e. at least one crew member has to have the job) + bool unassignedJobsFound = true; + while (unassignedJobsFound && unassigned.Count > 0) + { + unassignedJobsFound = false; + + foreach (JobPrefab jobPrefab in JobPrefab.List) + { + if (unassigned.Count == 0) break; + if (jobPrefab.MinNumber < 1 || assignedClientCount[jobPrefab] >= jobPrefab.MinNumber) continue; + + //find the client that wants the job the most, or force it to random client if none of them want it + Client assignedClient = FindClientWithJobPreference(unassigned, jobPrefab, true); + + assignedClient.AssignedJob = jobPrefab; + assignedClientCount[jobPrefab]++; + unassigned.Remove(assignedClient); + + //the job still needs more crew members, set unassignedJobsFound to true to keep the while loop running + if (assignedClientCount[jobPrefab] < jobPrefab.MinNumber) unassignedJobsFound = true; + } + } + + //attempt to give the clients a job they have in their job preferences + for (int i = unassigned.Count - 1; i >= 0; i--) + { + foreach (JobPrefab preferredJob in unassigned[i].JobPreferences) + { + //the maximum number of players that can have this job hasn't been reached yet + // -> assign it to the client + if (assignedClientCount[preferredJob] < preferredJob.MaxNumber && unassigned[i].Karma >= preferredJob.MinKarma) + { + unassigned[i].AssignedJob = preferredJob; + assignedClientCount[preferredJob]++; + unassigned.RemoveAt(i); + break; + } + } + } + + //give random jobs to rest of the clients + foreach (Client c in unassigned) + { + //find all jobs that are still available + var remainingJobs = JobPrefab.List.FindAll(jp => assignedClientCount[jp] < jp.MaxNumber && c.Karma >= jp.MinKarma); + + //all jobs taken, give a random job + if (remainingJobs.Count == 0) + { + DebugConsole.ThrowError("Failed to assign a suitable job for \"" + c.Name + "\" (all jobs already have the maximum numbers of players). Assigning a random job..."); + int jobIndex = Rand.Range(0, JobPrefab.List.Count); + int skips = 0; + while (c.Karma < JobPrefab.List[jobIndex].MinKarma) + { + jobIndex++; + skips++; + if (jobIndex >= JobPrefab.List.Count) jobIndex -= JobPrefab.List.Count; + if (skips >= JobPrefab.List.Count) break; + } + c.AssignedJob = JobPrefab.List[jobIndex]; + assignedClientCount[c.AssignedJob]++; + } + else //some jobs still left, choose one of them by random + { + c.AssignedJob = remainingJobs[Rand.Range(0, remainingJobs.Count)]; + assignedClientCount[c.AssignedJob]++; + } + } + } + + public void AssignBotJobs(List bots, Character.TeamType teamID) + { + Dictionary assignedPlayerCount = new Dictionary(); + foreach (JobPrefab jp in JobPrefab.List) + { + assignedPlayerCount.Add(jp, 0); + } + + //count the clients who already have characters with an assigned job + foreach (Client c in connectedClients) + { + if (c.TeamID != teamID) continue; + if (c.Character?.Info?.Job != null && !c.Character.IsDead) + { + assignedPlayerCount[c.Character.Info.Job.Prefab]++; + } + else if (c.CharacterInfo?.Job != null) + { + assignedPlayerCount[c.CharacterInfo?.Job.Prefab]++; + } + } + + List unassignedBots = new List(bots); + foreach (CharacterInfo bot in bots) + { + foreach (JobPrefab jobPrefab in JobPrefab.List) + { + if (jobPrefab.MinNumber < 1 || assignedPlayerCount[jobPrefab] >= jobPrefab.MinNumber) continue; + bot.Job = new Job(jobPrefab); + assignedPlayerCount[jobPrefab]++; + unassignedBots.Remove(bot); + break; + } + } + + //find a suitable job for the rest of the players + foreach (CharacterInfo c in unassignedBots) + { + //find all jobs that are still available + var remainingJobs = JobPrefab.List.FindAll(jp => assignedPlayerCount[jp] < jp.MaxNumber); + //all jobs taken, give a random job + if (remainingJobs.Count == 0) + { + DebugConsole.ThrowError("Failed to assign a suitable job for bot \"" + c.Name + "\" (all jobs already have the maximum numbers of players). Assigning a random job..."); + c.Job = new Job(JobPrefab.List[Rand.Range(0, JobPrefab.List.Count)]); + assignedPlayerCount[c.Job.Prefab]++; + } + else //some jobs still left, choose one of them by random + { + c.Job = new Job(remainingJobs[Rand.Range(0, remainingJobs.Count)]); + assignedPlayerCount[c.Job.Prefab]++; + } + } + } + + private Client FindClientWithJobPreference(List clients, JobPrefab job, bool forceAssign = false) + { + int bestPreference = 0; + Client preferredClient = null; + foreach (Client c in clients) + { + if (c.Karma < job.MinKarma) continue; + int index = c.JobPreferences.IndexOf(job); + if (index == -1) index = 1000; + + if (preferredClient == null || index < bestPreference) + { + bestPreference = index; + preferredClient = c; + } + } + + //none of the clients wants the job, assign it to random client + if (forceAssign && preferredClient == null) + { + preferredClient = clients[Rand.Int(clients.Count)]; + } + + return preferredClient; + } + + public static void Log(string line, ServerLog.MessageType messageType) + { + if (GameMain.Server == null || !GameMain.Server.ServerSettings.SaveServerLogs) return; + + GameMain.Server.ServerSettings.ServerLog.WriteLine(line, messageType); + + foreach (Client client in GameMain.Server.ConnectedClients) + { + if (!client.HasPermission(ClientPermissions.ServerLog)) continue; + //use sendername as the message type + GameMain.Server.SendDirectChatMessage( + ChatMessage.Create(messageType.ToString(), line, ChatMessageType.ServerLog, null), + client); + } + } + public override void Disconnect() { serverSettings.BanList.Save(); diff --git a/Barotrauma/BarotraumaServer/Source/Networking/GameServerLogin.cs b/Barotrauma/BarotraumaServer/Source/Networking/GameServerLogin.cs index e657f8a7f..fe0b66db2 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/GameServerLogin.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/GameServerLogin.cs @@ -140,7 +140,7 @@ namespace Barotrauma.Networking 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() + ")"); + unauthClient.Connection.Disconnect(DisconnectReason.SteamAuthenticationFailed.ToString() + "/ (" + status.ToString() + ")"); } else { @@ -500,7 +500,7 @@ namespace Barotrauma.Networking private void DisconnectUnauthClient(NetIncomingMessage inc, UnauthenticatedClient unauthClient, DisconnectReason reason, string message) { - inc.SenderConnection.Disconnect(reason.ToString() + "; " + message); + inc.SenderConnection.Disconnect(reason.ToString() + "/ " + message); if (unauthClient.SteamID > 0) { Steam.SteamManager.StopAuthSession(unauthClient.SteamID); } if (unauthClient != null) { diff --git a/Barotrauma/BarotraumaShared/SharedContent.projitems b/Barotrauma/BarotraumaShared/SharedContent.projitems index d5ac9408f..bec8045ae 100644 --- a/Barotrauma/BarotraumaShared/SharedContent.projitems +++ b/Barotrauma/BarotraumaShared/SharedContent.projitems @@ -1029,25 +1029,34 @@ PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + PreserveNewest @@ -1298,15 +1307,6 @@ PreserveNewest - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - PreserveNewest @@ -3108,5 +3108,8 @@ PreserveNewest + + PreserveNewest + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/HumanAIController.cs index 73c7d98f4..898af1d9a 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/HumanAIController.cs @@ -22,7 +22,6 @@ namespace Barotrauma public const float HULL_SAFETY_THRESHOLD = 50; - // TODO: update the list when someone gives a report public HashSet UnsafeHulls { get; private set; } = new HashSet(); private SteeringManager outsideSteering, insideSteering; @@ -136,10 +135,11 @@ namespace Barotrauma Character.AnimController.IgnorePlatforms = ignorePlatforms; - if (Character.IsClimbing) - { - Character.AnimController.TargetMovement = new Vector2(0.0f, Math.Sign(Character.AnimController.TargetMovement.Y)); - } + // Suspect that this causes issues when trying to exit from the ladders -> could try to check if the next node is ladder? + //if (Character.IsClimbing) + //{ + // Character.AnimController.TargetMovement = new Vector2(0.0f, Math.Sign(Character.AnimController.TargetMovement.Y)); + //} Vector2 targetMovement = AnimController.TargetMovement; @@ -187,7 +187,7 @@ namespace Barotrauma // Try to put the mask in an Any slot, and drop it if that fails if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List() { InvSlotType.Any })) { - mask.Drop(); + mask.Drop(Character); } } } @@ -198,7 +198,7 @@ namespace Barotrauma if (extinguisherItem != null && Character.HasEquippedItem(extinguisherItem)) { // TODO: take the item where it was taken from? - extinguisherItem.Drop(); + extinguisherItem.Drop(Character); } } @@ -227,6 +227,10 @@ namespace Barotrauma PropagateHullSafety(Character, Character.CurrentHull); } } + + private void ReportProblems() + { + if (GameMain.Client != null) return; protected void ReportProblems() { @@ -271,10 +275,6 @@ namespace Barotrauma } } } - - private void ReportProblems() - { - if (GameMain.Client != null) return; private void UpdateSpeaking() { @@ -293,41 +293,68 @@ namespace Barotrauma Character.Speak(TextManager.Get("DialogPressure").Replace("[roomname]", Character.CurrentHull.RoomName), null, 0, "pressure", 30.0f); } } - + public override void OnAttacked(Character attacker, AttackResult attackResult) { - float totalDamage = attackResult.Damage; - if (totalDamage <= 0.0f || attacker == null) return; - - if (attacker.SpeciesName == Character.SpeciesName) + float damage = attackResult.Damage; + if (damage < 0) { return; } + if (attacker == null || attacker.IsDead || attacker.Removed) + { + objectiveManager.GetObjective().Priority = 100; + return; + } + if (IsFriendly(attacker)) { - if (!attacker.IsRemotePlayer && Character.Controlled != attacker && attacker.AIController != null && attacker.AIController.Enabled) - { - // Don't react to damage done by friendly ai, because we know that it's accidental - return; - } if (attacker.AnimController.Anim == Barotrauma.AnimController.Animation.CPR && attacker.SelectedCharacter == Character) { // Don't attack characters that damage you while doing cpr, because let's assume that they are helping you. // Should not cancel any existing ai objectives (so that if the character attacked you and then helped, we still would want to retaliate). return; } + if (!attacker.IsRemotePlayer && Character.Controlled != attacker && attacker.AIController != null && attacker.AIController.Enabled) + { + // Don't react to damage done by friendly ai, because we know that it's accidental + objectiveManager.GetObjective().Priority = 100; + return; + } float currentVitality = Character.CharacterHealth.Vitality; - float dmgPercentage = totalDamage / currentVitality * 100; + float dmgPercentage = damage / currentVitality * 100; if (dmgPercentage < currentVitality / 10) { // Don't react to a minor amount of (accidental) dmg done by friendly characters + objectiveManager.GetObjective().Priority = 100; return; } + if (ObjectiveManager.CurrentObjective is AIObjectiveCombat combatObjective) + { + if (combatObjective.Enemy != attacker) + { + // Replace the old objective with the new. + ObjectiveManager.Objectives.Remove(combatObjective); + objectiveManager.AddObjective(new AIObjectiveCombat(Character, attacker)); + } + } + else + { + objectiveManager.AddObjective(new AIObjectiveCombat(Character, attacker), Rand.Range(0.5f, 1f, Rand.RandSync.Unsynced)); + } } - - objectiveManager.AddObjective(new AIObjectiveCombat(Character, attacker), Rand.Range(0.5f, 1, Rand.RandSync.Unsynced), () => + else { - //the objective in the manager is not necessarily the same as the one we just instantiated, - //because the objective isn't added if there's already an identical objective in the manager - var combatObjective = objectiveManager.GetObjective(); - combatObjective.MaxEnemyDamage = Math.Max(totalDamage, combatObjective.MaxEnemyDamage); - }); + if (ObjectiveManager.CurrentObjective is AIObjectiveCombat combatObjective) + { + if (combatObjective.Enemy != attacker) + { + // Replace the old objective with the new. + ObjectiveManager.Objectives.Remove(combatObjective); + objectiveManager.AddObjective(new AIObjectiveCombat(Character, attacker)); + } + } + else + { + objectiveManager.AddObjective(new AIObjectiveCombat(Character, attacker)); + } + } } public void SetOrder(Order order, string option, Character orderGiver, bool speak = true) @@ -420,7 +447,8 @@ namespace Barotrauma bool ignoreFire = ObjectiveManager.CurrentObjective is AIObjectiveExtinguishFire || ObjectiveManager.CurrentOrder is AIObjectiveExtinguishFires; bool ignoreWater = HasDivingSuit(Character); bool ignoreOxygen = ignoreWater || HasDivingGear(Character); - return GetHullSafety(hull, Character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies: false); + bool ignoreEnemies = ObjectiveManager.CurrentObjective is AIObjectiveCombat || ObjectiveManager.CurrentOrder is AIObjectiveCombat; + return GetHullSafety(hull, Character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies); } public static float GetHullSafety(Hull hull, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false) @@ -443,5 +471,8 @@ namespace Barotrauma float safety = oxygenFactor * waterFactor * fireFactor * enemyFactor; return MathHelper.Clamp(safety * 100, 0, 100); } + + // TODO: If the aliens are quaranteed to be in another team than the player, we wouldn't need to check the species. + public bool IsFriendly(Character other) => other.TeamID == Character.TeamID && other.SpeciesName == Character.SpeciesName; } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjective.cs index eb5aa57bd..a4ae023a2 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjective.cs @@ -15,7 +15,7 @@ namespace Barotrauma public virtual bool KeepDivingGearOn => false; protected readonly List subObjectives = new List(); - protected float priority; + public float Priority { get; set; } protected readonly Character character; protected string option; protected bool abandon; @@ -26,6 +26,7 @@ namespace Barotrauma protected HumanAIController HumanAIController => character.AIController as HumanAIController; protected IndoorsSteeringManager PathSteering => HumanAIController.PathSteering; + protected SteeringManager SteeringManager => HumanAIController.SteeringManager; public string Option { @@ -103,20 +104,20 @@ namespace Barotrauma CurrentSubObjective.SortSubObjectives(objectiveManager); } - public virtual float GetPriority(AIObjectiveManager objectiveManager) => priority; + public virtual float GetPriority(AIObjectiveManager objectiveManager) => Priority; public virtual void Update(AIObjectiveManager objectiveManager, float deltaTime) { var subObjective = objectiveManager.CurrentObjective?.CurrentSubObjective; if (objectiveManager.CurrentOrder == this) { - priority = AIObjectiveManager.OrderPriority; + Priority = AIObjectiveManager.OrderPriority; } else if (objectiveManager.CurrentObjective == this || subObjective == this) { - priority += Devotion * deltaTime; + Priority += Devotion * deltaTime; } - priority = MathHelper.Clamp(priority, 0, 100); + Priority = MathHelper.Clamp(Priority, 0, 100); subObjectives.ForEach(so => so.Update(objectiveManager, deltaTime)); } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveCombat.cs index 0463c7ef2..1f8813858 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -1,194 +1,268 @@ using Barotrauma.Items.Components; using Microsoft.Xna.Framework; -using System; using System.Collections.Generic; using System.Linq; +using Barotrauma.Extensions; namespace Barotrauma { class AIObjectiveCombat : AIObjective { public override string DebugTag => "combat"; - public override bool ForceRun => true; public override bool KeepDivingGearOn => true; const float CoolDown = 10.0f; - //the largest amount of damage the enemy has inflicted on this character - //(may be higher than enemyStrength if the enemy is e.g. a human using items) - public float MaxEnemyDamage; - - private Character enemy; - - private AIObjectiveFindSafety escapeObjective; - + public Character Enemy { get; private set; } + + private Item _weapon; + private Item Weapon + { + get { return _weapon; } + set + { + _weapon = value; + _weaponComponent = null; + reloadWeaponObjective = null; + } + } + private ItemComponent _weaponComponent; + private ItemComponent WeaponComponent + { + get + { + if (_weaponComponent == null) + { + _weaponComponent = + Weapon.GetComponent() as ItemComponent ?? + Weapon.GetComponent() as ItemComponent ?? + Weapon.GetComponent() as ItemComponent; + } + return _weaponComponent; + } + } private AIObjectiveContainItem reloadWeaponObjective; + private Hull retreatTarget; + private AIObjectiveGoTo retreatObjective; private float coolDownTimer; public AIObjectiveCombat(Character character, Character enemy) : base(character, "") { - this.enemy = enemy; + Enemy = enemy; coolDownTimer = CoolDown; + HumanAIController.ObjectiveManager.GetObjective().Priority = 0; } protected override void Act(float deltaTime) { coolDownTimer -= deltaTime; - - var weapon = character.Inventory.FindItemByTag("weapon"); - if (weapon == null) + if (Weapon != null && character.Inventory.Items.Contains(_weapon)) + { + Weapon = null; + } + if (Weapon == null) + { + Weapon = GetWeapon(); + } + if (Weapon == null) { Escape(deltaTime); } - else + else if (Equip(deltaTime)) { - if (!character.SelectedItems.Contains(weapon)) + if (Reload(deltaTime)) { - if (character.Inventory.TryPutItem(weapon, 3, true, false, character)) - { - weapon.Equip(character); - } - else - { - //couldn't equip the item, escape - Escape(deltaTime); - return; - } + Attack(deltaTime); } + } + if (!abandon) + { + Move(deltaTime); + } + } - //make sure the weapon is loaded - var weaponComponent = - weapon.GetComponent() as ItemComponent ?? - weapon.GetComponent() as ItemComponent ?? - weapon.GetComponent() as ItemComponent; - if (weaponComponent != null && weaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained)) + private Item GetWeapon() + { + _weaponComponent = null; + var weapon = character.Inventory.FindItemByTag("weapon"); + if (weapon == null) + { + foreach (var item in character.Inventory.Items) { - var containedItems = weapon.ContainedItems; - foreach (RelatedItem requiredItem in weaponComponent.requiredItems[RelatedItem.RelationType.Contained]) + if (item == null) { continue; } + foreach (var component in item.Components) { - Item containedItem = containedItems.FirstOrDefault(it => it.Condition > 0.0f && requiredItem.MatchesItem(it)); - if (containedItem == null) + if (component is MeleeWeapon || component is RangedWeapon) { - var newReloadWeaponObjective = new AIObjectiveContainItem(character, requiredItem.Identifiers, weapon.GetComponent()); - if (!newReloadWeaponObjective.IsDuplicate(reloadWeaponObjective)) + return item; + } + var effects = component.statusEffectLists; + if (effects != null) + { + foreach (var statusEffects in effects.Values) { - reloadWeaponObjective = newReloadWeaponObjective; + foreach (var statusEffect in statusEffects) + { + if (statusEffect.Afflictions.Any()) + { + return item; + } + } } } } } + } + return weapon; + } - if (reloadWeaponObjective != null) + private bool Equip(float deltaTime) + { + if (!character.SelectedItems.Contains(Weapon)) + { + if (character.Inventory.TryPutItem(Weapon, 3, true, false, character)) { - if (reloadWeaponObjective.IsCompleted()) - { - reloadWeaponObjective = null; - } - else if (!reloadWeaponObjective.CanBeCompleted) - { - Escape(deltaTime); - } - else - { - reloadWeaponObjective.TryComplete(deltaTime); - } - return; + Weapon.Equip(character); } - - character.CursorPosition = enemy.Position; - character.SetInput(InputType.Aim, false, true); - - Vector2 enemyDiff = Vector2.Normalize(enemy.Position - character.Position); - if (!MathUtils.IsValid(enemyDiff)) enemyDiff = Rand.Vector(1.0f); - float weaponAngle = ((weapon.body.Dir == 1.0f) ? weapon.body.Rotation : weapon.body.Rotation - MathHelper.Pi); - Vector2 weaponDir = new Vector2((float)Math.Cos(weaponAngle), (float)Math.Sin(weaponAngle)); - - if (Vector2.Dot(enemyDiff, weaponDir) < 0.9f) return; - - List ignoredBodies = new List(); - foreach (Limb limb in character.AnimController.Limbs) + else { - ignoredBodies.Add(limb.body.FarseerBody); + //couldn't equip the item, escape + Escape(deltaTime); + return false; } + } + return true; + } - var pickedBody = Submarine.PickBody(character.SimPosition, enemy.SimPosition, ignoredBodies); - if (pickedBody != null && !(pickedBody.UserData is Limb)) return; + private void Move(float deltaTime) + { + // Retreat to safety + // TODO: aggressive behaviour, chasing? + if (retreatTarget == null || (retreatObjective != null && !retreatObjective.CanBeCompleted)) + { + retreatTarget = HumanAIController.ObjectiveManager.GetObjective().FindBestHull(); + } + if (retreatTarget != null) + { + if (retreatObjective == null || retreatObjective.Target != retreatTarget) + { + retreatObjective = new AIObjectiveGoTo(retreatTarget, character, false, true); + } + retreatObjective.TryComplete(deltaTime); + } + } - weapon.Use(deltaTime, character); + private bool Reload(float deltaTime) + { + if (WeaponComponent != null && WeaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained)) + { + var containedItems = Weapon.ContainedItems; + foreach (RelatedItem requiredItem in WeaponComponent.requiredItems[RelatedItem.RelationType.Contained]) + { + Item containedItem = containedItems.FirstOrDefault(it => it.Condition > 0.0f && requiredItem.MatchesItem(it)); + if (containedItem == null) + { + if (reloadWeaponObjective == null) + { + reloadWeaponObjective = new AIObjectiveContainItem(character, requiredItem.Identifiers, Weapon.GetComponent()); + } + } + } + } + if (reloadWeaponObjective != null) + { + if (reloadWeaponObjective.IsCompleted()) + { + reloadWeaponObjective = null; + } + else if (!reloadWeaponObjective.CanBeCompleted) + { + Escape(deltaTime); + } + else + { + reloadWeaponObjective.TryComplete(deltaTime); + } + return false; + } + return true; + } + + private IEnumerable myBodies; + private void Attack(float deltaTime) + { + character.CursorPosition = Enemy.Position; + character.SetInput(InputType.Aim, false, true); + if (WeaponComponent is MeleeWeapon meleeWeapon) + { + if (Vector2.DistanceSquared(character.Position, Enemy.Position) <= meleeWeapon.Range * meleeWeapon.Range) + { + Weapon.Use(deltaTime, character); + } + } + else + { + if (WeaponComponent is RepairTool repairTool) + { + if (Vector2.DistanceSquared(character.Position, Enemy.Position) > repairTool.Range * repairTool.Range) { return; } + } + if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.Position - character.Position) < MathHelper.PiOver4) + { + if (myBodies == null) + { + myBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody); + } + var pickedBody = Submarine.PickBody(character.SimPosition, Enemy.SimPosition, myBodies); + if (pickedBody != null) + { + Character target = null; + if (pickedBody.UserData is Character c) + { + target = c; + } + else if (pickedBody.UserData is Limb limb) + { + target = limb.character; + } + if (target != null && target == Enemy) + { + Weapon.Use(deltaTime, character); + } + } + } } } private void Escape(float deltaTime) { - // TODO: just let the find safety run? - if (escapeObjective == null) - { - escapeObjective = new AIObjectiveFindSafety(character); - } - - if (enemy.AnimController.CurrentHull == character.AnimController.CurrentHull) - { - escapeObjective.OverrideCurrentHullSafety = 0.0f; - } - else - { - escapeObjective.OverrideCurrentHullSafety = null; - } - - escapeObjective.TryComplete(deltaTime); + abandon = true; + SteeringManager.Reset(); + HumanAIController.ObjectiveManager.GetObjective().Priority = 100; } - public override bool IsCompleted() - { - return enemy == null || enemy.Removed || enemy.IsDead || coolDownTimer <= 0.0f; - } - - public override float GetPriority(AIObjectiveManager objectiveManager) - { - if (enemy == null || enemy.Removed) - { - return 0.0f; - } - - if (objectiveManager.CurrentOrder == this) - { - return AIObjectiveManager.OrderPriority; - } - - //clamp the strength to the health of this character - //(it doesn't make a difference whether the enemy does 200 or 600 damage, it's one hit kill anyway) - - float enemyDanger = Math.Min(Math.Max(CalculateEnemyStrength(), MaxEnemyDamage), character.Health) + enemy.Health / 10.0f; - - if (enemy.AIController is EnemyAIController enemyAI) - { - if (enemyAI.SelectedAiTarget == character.AiTarget) enemyDanger *= 2.0f; - } - - return Math.Max(enemyDanger, AIObjectiveManager.OrderPriority); - } + public override bool IsCompleted() => Enemy == null || Enemy.Removed || Enemy.IsDead || coolDownTimer <= 0.0f; + public override bool CanBeCompleted => !abandon && (reloadWeaponObjective == null || reloadWeaponObjective.CanBeCompleted) && (retreatObjective == null || retreatObjective.CanBeCompleted); + public override float GetPriority(AIObjectiveManager objectiveManager) => Enemy == null || Enemy.Removed || Enemy.IsDead ? 0 : 100; public override bool IsDuplicate(AIObjective otherObjective) { - AIObjectiveCombat objective = otherObjective as AIObjectiveCombat; - if (objective == null) return false; - - return objective.enemy == enemy; + if (!(otherObjective is AIObjectiveCombat objective)) return false; + return objective.Enemy == Enemy; } - private float CalculateEnemyStrength() - { - float enemyStrength = 0; - AttackContext currentContext = character.GetAttackContext(); - foreach (Limb limb in enemy.AnimController.Limbs) - { - if (limb.attack == null) continue; - if (!limb.attack.IsValidContext(currentContext)) { continue; } - if (!limb.attack.IsValidTarget(AttackTarget.Character)) { continue; } - enemyStrength += limb.attack.GetTotalDamage(false); - } - return enemyStrength; - } + //private float CalculateEnemyStrength() + //{ + // float enemyStrength = 0; + // AttackContext currentContext = character.GetAttackContext(); + // foreach (Limb limb in Enemy.AnimController.Limbs) + // { + // if (limb.attack == null) continue; + // if (!limb.attack.IsValidContext(currentContext)) { continue; } + // if (!limb.attack.IsValidTarget(AttackTarget.Character)) { continue; } + // enemyStrength += limb.attack.GetTotalDamage(false); + // } + // return enemyStrength; + //} } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs index fd208bb1a..ad375e55e 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs @@ -32,7 +32,7 @@ namespace Barotrauma float dist = Math.Abs(character.WorldPosition.X - targetHull.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - targetHull.WorldPosition.Y) * 2.0f; float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 10000, dist)); float severityFactor = MathHelper.Lerp(0, 1, MathHelper.Clamp(targetHull.FireSources.Sum(fs => fs.Size.X) / targetHull.Size.X, 0, 1)); - return MathHelper.Clamp(priority * severityFactor * distanceFactor, 0, 100); + return MathHelper.Clamp(Priority * severityFactor * distanceFactor, 0, 100); } public override bool IsCompleted() diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs index 51569bd04..b2158a614 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs @@ -62,7 +62,7 @@ namespace Barotrauma if (containedItem == null) continue; if (containedItem.Condition <= 0.0f) { - containedItem.Drop(); + containedItem.Drop(character); } else if (containedItem.Prefab.Identifier == "oxygentank" || containedItem.HasTag("oxygensource")) { diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFindSafety.cs index e7beb84bf..6ad1eeca8 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -16,18 +16,17 @@ namespace Barotrauma const float priorityIncrease = 25; const float priorityDecrease = 10; const float SearchHullInterval = 3.0f; + const float clearUnreachableInterval = 30; private List unreachable = new List(); private float currenthullSafety; - + private float unreachableClearTimer; private float searchHullTimer; private AIObjectiveGoTo goToObjective; private AIObjective divingGearObjective; - public float? OverrideCurrentHullSafety; - public AIObjectiveFindSafety(Character character) : base(character, "") { } public override bool IsCompleted() => false; @@ -52,7 +51,7 @@ namespace Barotrauma if (divingGearObjective.IsCompleted()) { divingGearObjective = null; - priority = 0; + Priority = 0; } else if (divingGearObjective.CanBeCompleted) { @@ -61,6 +60,16 @@ namespace Barotrauma } } + if (unreachableClearTimer > 0) + { + unreachableClearTimer -= deltaTime; + } + else + { + unreachableClearTimer = clearUnreachableInterval; + unreachable.Clear(); + } + if (searchHullTimer > 0.0f) { searchHullTimer -= deltaTime; @@ -147,16 +156,35 @@ namespace Barotrauma } } - private Hull FindBestHull() + public Hull FindBestHull() { Hull bestHull = character.CurrentHull; float bestValue = currenthullSafety; foreach (Hull hull in Hull.hullList) { - if (unreachable.Contains(hull)) { continue; } if (hull.Submarine == null) { continue; } float hullSafety = 0; - if (character.Submarine == null) + if (character.Submarine != null && SteeringManager == PathSteering) + { + // Inside or outside near the sub + if (unreachable.Contains(hull)) { continue; } + if (!character.Submarine.IsConnectedTo(hull.Submarine)) { continue; } + hullSafety = HumanAIController.GetHullSafety(hull, character); + var path = PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition); + int unsafeNodes = path.Nodes.Count(n => n.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(n.CurrentHull)); + // Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally) + float dist = Math.Abs(character.WorldPosition.X - hull.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y) * 2.0f; + float distanceFactor = MathHelper.Lerp(1, 0.9f, MathUtils.InverseLerp(0, 10000, dist)); + hullSafety *= distanceFactor; + // Each unsafe node reduces the hull safety value. + hullSafety /= 1 + unsafeNodes; + // If the target is not inside a friendly submarine, considerably reduce the hull safety. + if (!character.Submarine.IsEntityFoundOnThisSub(hull, true)) + { + hullSafety /= 10; + } + } + else { // Outside if (hull.RoomName?.ToLowerInvariant() == "airlock") @@ -177,7 +205,8 @@ namespace Barotrauma } // Huge preference for closer targets - float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, 100000, Vector2.Distance(character.WorldPosition, hull.WorldPosition))); + float distance = Vector2.DistanceSquared(character.WorldPosition, hull.WorldPosition); + float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, MathUtils.Pow(100000, 2), distance)); hullSafety *= distanceFactor; // If the target is not inside a friendly submarine, considerably reduce the hull safety. if (hull.Submarine.TeamID != character.TeamID && hull.Submarine.TeamID != Character.TeamType.FriendlyNPC) @@ -185,26 +214,6 @@ namespace Barotrauma hullSafety /= 10; } } - else - { - // Inside - // Not connected. - if (!character.Submarine.GetConnectedSubs().Contains(hull.Submarine)) { continue; } - hullSafety = HumanAIController.GetHullSafety(hull, character); - var path = PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition); - int unsafeNodes = path.Nodes.Count(n => n.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(n.CurrentHull)); - // Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally) - float dist = Math.Abs(character.WorldPosition.X - hull.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y) * 2.0f; - float distanceFactor = MathHelper.Lerp(1, 0.9f, MathUtils.InverseLerp(0, 10000, dist)); - hullSafety *= distanceFactor; - // Each unsafe node reduces the hull safety value. - hullSafety /= 1 + unsafeNodes; - // If the target is not inside a friendly submarine, considerably reduce the hull safety. - if (!character.Submarine.IsEntityFoundOnThisSub(hull, true)) - { - hullSafety /= 10; - } - } if (hullSafety > bestValue) { bestHull = hull; @@ -224,24 +233,24 @@ namespace Barotrauma if (character.CurrentHull == null) { currenthullSafety = 0; - priority = 5; + Priority = 5; return; } - if (character.OxygenAvailable < CharacterHealth.LowOxygenThreshold) { priority = 100; } - currenthullSafety = OverrideCurrentHullSafety ?? HumanAIController.GetHullSafety(character.CurrentHull); + if (character.OxygenAvailable < CharacterHealth.LowOxygenThreshold) { Priority = 100; } + currenthullSafety = HumanAIController.GetHullSafety(character.CurrentHull); if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD) { - priority -= priorityDecrease * deltaTime; + Priority -= priorityDecrease * deltaTime; } else { float dangerFactor = (100 - currenthullSafety) / 100; - priority += dangerFactor * priorityIncrease * deltaTime; + Priority += dangerFactor * priorityIncrease * deltaTime; } - priority = MathHelper.Clamp(priority, 0, 100); + Priority = MathHelper.Clamp(Priority, 0, 100); if (divingGearObjective != null && !divingGearObjective.IsCompleted() && divingGearObjective.CanBeCompleted) { - priority = Math.Max(priority, AIObjectiveManager.OrderPriority + 10); + Priority = Math.Max(Priority, AIObjectiveManager.OrderPriority + 10); } } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFixLeak.cs index 517076479..8a29fefcd 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -11,6 +11,7 @@ namespace Barotrauma public override string DebugTag => "fix leak"; public override bool KeepDivingGearOn => true; + public override bool ForceRun => true; private readonly Gap leak; diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFixLeaks.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFixLeaks.cs index 690db42c8..0e814171e 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFixLeaks.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFixLeaks.cs @@ -10,6 +10,7 @@ namespace Barotrauma { public override string DebugTag => "fix leaks"; public override bool KeepDivingGearOn => true; + public override bool ForceRun => true; public AIObjectiveFixLeaks(Character character) : base (character, "") { } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveGetItem.cs index d3ded6a94..68428100b 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -144,7 +144,7 @@ namespace Barotrauma if (character.Inventory.TryPutItem(character.Inventory.Items[i], character, new List() { InvSlotType.Any })) continue; //if everything else fails, simply drop the existing item - character.Inventory.Items[i].Drop(); + character.Inventory.Items[i].Drop(character); } } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveGoTo.cs index 5a88f2646..f4a2ab366 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -45,17 +45,22 @@ namespace Barotrauma get { bool canComplete = !cannotReach && !abandon; - if (FollowControlledCharacter && Character.Controlled == null) { canComplete = false; } - else if (Target != null && Target.Removed) { canComplete = false; } - else if (repeat || waitUntilPathUnreachable > 0.0f) { canComplete = true; } - else if (character.AIController.SteeringManager is IndoorsSteeringManager pathSteering) + if (canComplete) { - //path doesn't exist (= hasn't been searched for yet), assume for now that the target is reachable TODO: add a timer? - if (pathSteering.CurrentPath == null) { canComplete = true; } - else if (!AllowGoingOutside && pathSteering.CurrentPath.HasOutdoorsNodes) { canComplete = false; } - if (canComplete) + if (FollowControlledCharacter && Character.Controlled == null) { - canComplete = !pathSteering.CurrentPath.Unreachable; + canComplete = false; + } + else if (Target != null && Target.Removed) + { + canComplete = false; + } + else if (!repeat && waitUntilPathUnreachable < 0) + { + if (SteeringManager == PathSteering && PathSteering.CurrentPath != null) + { + canComplete = !PathSteering.CurrentPath.Unreachable; + } } } if (!canComplete) @@ -144,9 +149,9 @@ namespace Barotrauma } else { - var indoorSteering = character.AIController.SteeringManager as IndoorsSteeringManager; - bool targetIsOutside = (Target != null && Target.Submarine == null) || (indoorSteering != null && indoorSteering.CurrentPath != null && indoorSteering.CurrentPath.HasOutdoorsNodes); - if (targetIsOutside && !AllowGoingOutside) + bool targetIsOutside = (Target != null && Target.Submarine == null) || + (SteeringManager == PathSteering && PathSteering.CurrentPath != null && PathSteering.CurrentPath.HasOutdoorsNodes); + if (targetIsOutside && character.CurrentHull != null && !AllowGoingOutside) { cannotReach = true; } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveIdle.cs index 8c2fd39ac..7d0794a5a 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -11,6 +11,12 @@ namespace Barotrauma public override string DebugTag => "idle"; const float WallAvoidDistance = 150.0f; + private readonly float newTargetIntervalMin = 5; + private readonly float newTargetIntervalMax = 15; + private readonly float standStillMin = 1; + private readonly float standStillMax = 10; + private readonly float walkDurationMin = 3; + private readonly float walkDurationMax = 10; private Hull currentTarget; private float newTargetTimer; @@ -46,7 +52,10 @@ namespace Barotrauma character.SelectedConstruction = null; } - if (currentTarget == null && (IsForbidden(character.CurrentHull) || HumanAIController.UnsafeHulls.Contains(character.CurrentHull))) + bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) || + (PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull))); + + if (currentTargetIsInvalid || (currentTarget == null && IsForbidden(character.CurrentHull))) { newTargetTimer = 0; standStillTimer = 0; @@ -70,7 +79,7 @@ namespace Barotrauma PathSteering.SetPath(path); } - newTargetTimer = currentTarget == null ? 5.0f : 15.0f; + newTargetTimer = currentTarget != null && character.AnimController.InWater ? newTargetIntervalMin : Rand.Range(newTargetIntervalMin, newTargetIntervalMax); } newTargetTimer -= deltaTime; @@ -79,20 +88,20 @@ namespace Barotrauma // - if reached the end of the path // - if the target is unreachable // - if the path requires going outside - if (PathSteering == null || (PathSteering.CurrentPath != null && + if (SteeringManager != PathSteering || (PathSteering.CurrentPath != null && (PathSteering.CurrentPath.NextNode == null || PathSteering.CurrentPath.Unreachable || PathSteering.CurrentPath.HasOutdoorsNodes))) { standStillTimer -= deltaTime; if (standStillTimer > 0.0f) { - walkDuration = Rand.Range(1.0f, 5.0f); + walkDuration = Rand.Range(walkDurationMin, walkDurationMax); PathSteering.Reset(); return; } if (standStillTimer < -walkDuration) { - standStillTimer = Rand.Range(1.0f, 10.0f); + standStillTimer = Rand.Range(standStillMin, standStillMax); } //steer away from edges of the hull @@ -128,12 +137,11 @@ namespace Barotrauma } character.AIController.SteeringManager.SteeringWander(); - if (!character.IsClimbing) + if (!character.IsClimbing && !character.AnimController.InWater) { //reset vertical steering to prevent dropping down from platforms etc character.AIController.SteeringManager.ResetY(); } - return; } @@ -150,6 +158,7 @@ namespace Barotrauma { var idCard = character.Inventory.FindItemByIdentifier("idcard"); Hull targetHull = null; + bool isCurrentHullOK = !HumanAIController.UnsafeHulls.Contains(character.CurrentHull) && !IsForbidden(character.CurrentHull); //random chance of navigating back to the room where the character spawned if (Rand.Int(5) == 1 && idCard != null) { @@ -186,9 +195,13 @@ namespace Barotrauma continue; } } - // Check that there is no unsafe or forbidden hulls on the way to the target - var path = PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition); - if (path.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull) || IsForbidden(n.CurrentHull))) { continue; } + if (isCurrentHullOK) + { + // Check that there is no unsafe or forbidden hulls on the way to the target + // Only do this when the current hull is ok, because otherwise the would block all paths from the current hull to the target hull. + var path = PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition); + if (path.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull) || IsForbidden(n.CurrentHull))) { continue; } + } // If we want to do a steering check, we should do it here, before setting the path //if (path.Cost > 1000.0f) { continue; } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveManager.cs index b456d9f62..31b2a9015 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveManager.cs @@ -56,23 +56,6 @@ namespace Barotrauma DelayedObjectives.Add(objective, coroutine); } - public Dictionary DelayedObjectives { get; private set; } = new Dictionary(); - public void AddObjective(AIObjective objective, float delay, Action callback = null) - { - if (DelayedObjectives.TryGetValue(objective, out CoroutineHandle coroutine)) - { - CoroutineManager.StopCoroutines(coroutine); - DelayedObjectives.Remove(objective); - } - coroutine = CoroutineManager.InvokeAfter(() => - { - DelayedObjectives.Remove(objective); - AddObjective(objective); - callback?.Invoke(); - }, delay); - DelayedObjectives.Add(objective, coroutine); - } - public T GetObjective() where T : AIObjective { foreach (AIObjective objective in Objectives) @@ -200,27 +183,6 @@ namespace Barotrauma if (order.TargetItemComponent == null) return; CurrentOrder = new AIObjectiveOperateItem(order.TargetItemComponent, character, option, false, null, order.UseController); break; - case "chargebatteries": - currentOrder = new AIObjectiveChargeBatteries(character, option); - break; - case "rescue": - currentOrder = new AIObjectiveRescueAll(character); - break; - case "repairsystems": - currentOrder = new AIObjectiveRepairItems(character) { RequireAdequateSkills = option != "all" }; - break; - case "pumpwater": - currentOrder = new AIObjectivePumpWater(character, option); - break; - case "extinguishfires": - currentOrder = new AIObjectiveExtinguishFires(character); - break; - case "steer": - var steering = (order?.TargetEntity as Item)?.GetComponent(); - if (steering != null) steering.PosToMaintain = steering.Item.Submarine?.WorldPosition; - if (order.TargetItemComponent == null) return; - currentOrder = new AIObjectiveOperateItem(order.TargetItemComponent, character, option, false, null, order.UseController); - break; default: if (order.TargetItemComponent == null) return; CurrentOrder = new AIObjectiveOperateItem(order.TargetItemComponent, character, option, false, null, order.UseController); diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveOperateItem.cs index 6b244961a..369ad5b5c 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -135,7 +135,7 @@ namespace Barotrauma if (!character.Inventory.Items[i].AllowedSlots.Contains(InvSlotType.Any) || !character.Inventory.TryPutItem(character.Inventory.Items[i], character, new List() { InvSlotType.Any })) { - character.Inventory.Items[i].Drop(); + character.Inventory.Items[i].Drop(character); } } if (character.Inventory.TryPutItem(component.Item, i, true, false, character)) diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRepairItem.cs index 8a6b47f1f..fe0730eff 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -35,7 +35,7 @@ namespace Barotrauma float damagePriority = MathHelper.Lerp(1, 0, (Item.Condition + 10) / Item.MaxCondition); float successFactor = MathHelper.Lerp(0, 1, Item.Repairables.Average(r => r.DegreeOfSuccess(character))); float isSelected = character.SelectedConstruction == Item ? 50 : 0; - float baseLevel = Math.Max(priority + isSelected, 1); + float baseLevel = Math.Max(Priority + isSelected, 1); return MathHelper.Clamp(baseLevel * damagePriority * distanceFactor * successFactor, 0, 100); } @@ -84,6 +84,7 @@ namespace Barotrauma } if (character.CanInteractWith(Item)) { + OperateRepairTool(deltaTime); foreach (Repairable repairable in Item.Repairables) { if (repairable.CurrentFixer != null && repairable.CurrentFixer != character) @@ -123,5 +124,33 @@ namespace Barotrauma AddSubObjective(goToObjective); } } + + private void OperateRepairTool(float deltaTime) + { + // Operate repair tool, if required. + foreach (Repairable repairable in Item.Repairables) + { + foreach (var kvp in repairable.requiredItems) + { + foreach (RelatedItem requiredItem in kvp.Value) + { + foreach (var item in character.Inventory.Items) + { + if (requiredItem.MatchesItem(item)) + { + var repairTool = item.GetComponent(); + if (repairTool != null) + { + character.CursorPosition = Item.Position; + character.SetInput(InputType.Aim, false, true); + repairTool.Use(deltaTime, character); + return; + } + } + } + } + } + } + } } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/Source/Characters/Health/CharacterHealth.cs index 17a67ca3f..dff106cfb 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Health/CharacterHealth.cs @@ -549,8 +549,9 @@ namespace Barotrauma limbHealths[i].Afflictions.RemoveAt(j); } } - foreach (Affliction affliction in limbHealths[i].Afflictions) + for (int j = limbHealths[i].Afflictions.Count - 1; j >= 0; j--) { + var affliction = limbHealths[i].Afflictions[j]; Limb targetLimb = Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == i); affliction.Update(this, targetLimb, deltaTime); affliction.DamagePerSecondTimer += deltaTime; diff --git a/Barotrauma/BarotraumaShared/Source/Events/EventManager.cs b/Barotrauma/BarotraumaShared/Source/Events/EventManager.cs index 13c0a8d55..b75f7012e 100644 --- a/Barotrauma/BarotraumaShared/Source/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/Source/Events/EventManager.cs @@ -120,18 +120,30 @@ namespace Barotrauma private void CreateEvents() { + //don't create new events if docked to the start oupost + if (Level.Loaded?.StartOutpost != null && + Submarine.MainSub.DockedTo.Contains(Level.Loaded.StartOutpost)) + { + return; + } + for (int i = selectedEventSets.Count - 1; i >= 0; i--) { ScriptedEventSet eventSet = selectedEventSets[i]; + float distFromStart = Vector2.Distance(Submarine.MainSub.WorldPosition, level.StartPosition); + float distFromEnd = Vector2.Distance(Submarine.MainSub.WorldPosition, level.EndPosition); + float distanceTraveled = MathHelper.Clamp( (Submarine.MainSub.WorldPosition.X - level.StartPosition.X) / (level.EndPosition.X - level.StartPosition.X), 0.0f, 1.0f); - if (Level.Loaded?.StartOutpost != null && - Submarine.MainSub.DockedTo.Contains(Level.Loaded.StartOutpost)) + //don't create new events if within 50 meters of the start/end of the level + if (distanceTraveled <= 0.0f || + distFromStart * Physics.DisplayToRealWorldRatio < 50.0f || + distFromEnd * Physics.DisplayToRealWorldRatio < 50.0f) { - distanceTraveled = 0.0f; + continue; } if ((Submarine.MainSub == null || distanceTraveled < eventSet.MinDistanceTraveled) && diff --git a/Barotrauma/BarotraumaShared/Source/GameSettings.cs b/Barotrauma/BarotraumaShared/Source/GameSettings.cs index 49e1ca3b0..e8b625621 100644 --- a/Barotrauma/BarotraumaShared/Source/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/Source/GameSettings.cs @@ -65,12 +65,6 @@ namespace Barotrauma public int ParticleLimit { get; set; } - public float LightMapScale { get; set; } - public bool SpecularityEnabled { get; set; } - public bool ChromaticAberrationEnabled { get; set; } - - public bool MuteOnFocusLost { get; set; } - public int ParticleLimit { get; set; } public float LightMapScale { get; set; } @@ -405,8 +399,6 @@ namespace Barotrauma AimAssistAmount = doc.Root.GetAttributeFloat("aimassistamount", 0.5f); - AimAssistAmount = doc.Root.GetAttributeFloat("aimassistamount", 0.5f); - AimAssistAmount = doc.Root.GetAttributeFloat("aimassistamount", 0.5f); keyMapping = new KeyOrMouse[Enum.GetNames(typeof(InputType)).Length]; @@ -620,98 +612,6 @@ namespace Barotrauma } } - TextManager.LoadTextPacks(SelectedContentPackages); - - //display error messages after all content packages have been loaded - //to make sure the package that contains text files has been loaded before we attempt to use TextManager - foreach (string missingPackagePath in missingPackagePaths) - { - DebugConsole.ThrowError(TextManager.Get("ContentPackageNotFound").Replace("[packagepath]", missingPackagePath)); - } - foreach (ContentPackage incompatiblePackage in incompatiblePackages) - { - DebugConsole.ThrowError(TextManager.Get(incompatiblePackage.GameVersion <= new Version(0, 0, 0, 0) ? "IncompatibleContentPackageUnknownVersion" : "IncompatibleContentPackage") - .Replace("[packagename]", incompatiblePackage.Name) - .Replace("[packageversion]", incompatiblePackage.GameVersion.ToString()) - .Replace("[gameversion]", GameMain.Version.ToString())); - } - foreach (ContentPackage contentPackage in SelectedContentPackages) - { - foreach (ContentFile file in contentPackage.Files) - { - if (!System.IO.File.Exists(file.Path)) - { - DebugConsole.ThrowError("Error in content package \"" + contentPackage.Name + "\" - file \"" + file.Path + "\" not found."); - continue; - } - ToolBox.IsProperFilenameCase(file.Path); - } - } - - TextManager.LoadTextPacks(SelectedContentPackages); - - //display error messages after all content packages have been loaded - //to make sure the package that contains text files has been loaded before we attempt to use TextManager - foreach (string missingPackagePath in missingPackagePaths) - { - DebugConsole.ThrowError(TextManager.Get("ContentPackageNotFound").Replace("[packagepath]", missingPackagePath)); - } - foreach (ContentPackage incompatiblePackage in incompatiblePackages) - { - DebugConsole.ThrowError(TextManager.Get(incompatiblePackage.GameVersion <= new Version(0, 0, 0, 0) ? "IncompatibleContentPackageUnknownVersion" : "IncompatibleContentPackage") - .Replace("[packagename]", incompatiblePackage.Name) - .Replace("[packageversion]", incompatiblePackage.GameVersion.ToString()) - .Replace("[gameversion]", GameMain.Version.ToString())); - } - foreach (ContentPackage contentPackage in SelectedContentPackages) - { - foreach (ContentFile file in contentPackage.Files) - { - if (!System.IO.File.Exists(file.Path)) - { - DebugConsole.ThrowError("Error in content package \"" + contentPackage.Name + "\" - file \"" + file.Path + "\" not found."); - continue; - } - ToolBox.IsProperFilenameCase(file.Path); - } - } - - TextManager.LoadTextPacks(SelectedContentPackages); - - //display error messages after all content packages have been loaded - //to make sure the package that contains text files has been loaded before we attempt to use TextManager - foreach (string missingPackagePath in missingPackagePaths) - { - DebugConsole.ThrowError(TextManager.Get("ContentPackageNotFound").Replace("[packagepath]", missingPackagePath)); - } - foreach (ContentPackage incompatiblePackage in incompatiblePackages) - { - DebugConsole.ThrowError(TextManager.Get(incompatiblePackage.GameVersion <= new Version(0, 0, 0, 0) ? "IncompatibleContentPackageUnknownVersion" : "IncompatibleContentPackage") - .Replace("[packagename]", incompatiblePackage.Name) - .Replace("[packageversion]", incompatiblePackage.GameVersion.ToString()) - .Replace("[gameversion]", GameMain.Version.ToString())); - } - foreach (ContentPackage contentPackage in SelectedContentPackages) - { - foreach (ContentFile file in contentPackage.Files) - { - if (!System.IO.File.Exists(file.Path)) - { - DebugConsole.ThrowError("Error in content package \"" + contentPackage.Name + "\" - file \"" + file.Path + "\" not found."); - continue; - } - ToolBox.IsProperFilenameCase(file.Path); - } - } - if (!SelectedContentPackages.Any()) - { - var availablePackage = ContentPackage.List.FirstOrDefault(cp => cp.IsCompatible() && cp.CorePackage); - if (availablePackage != null) - { - SelectedContentPackages.Add(availablePackage); - } - } - //save to get rid of the invalid selected packages in the config file if (missingPackagePaths.Count > 0 || incompatiblePackages.Count > 0) { SaveNewPlayerConfig(); } } @@ -1084,6 +984,369 @@ namespace Barotrauma } #endregion + #region Save PlayerConfig + public void SaveNewPlayerConfig() + { + XDocument doc = new XDocument(); + UnsavedSettings = false; + + if (doc.Root == null) + { + doc.Add(new XElement("config")); + } + + doc.Root.Add( + new XAttribute("language", TextManager.Language), + new XAttribute("masterserverurl", MasterServerUrl), + new XAttribute("autocheckupdates", AutoCheckUpdates), + new XAttribute("musicvolume", musicVolume), + new XAttribute("soundvolume", soundVolume), + new XAttribute("verboselogging", VerboseLogging), + new XAttribute("savedebugconsolelogs", SaveDebugConsoleLogs), + new XAttribute("enablesplashscreen", EnableSplashScreen), + new XAttribute("usesteammatchmaking", useSteamMatchmaking), + new XAttribute("quickstartsub", QuickStartSubmarineName), + new XAttribute("requiresteamauthentication", requireSteamAuthentication), + new XAttribute("autoupdateworkshopitems", AutoUpdateWorkshopItems), + new XAttribute("aimassistamount", aimAssistAmount)); + + if (!ShowUserStatisticsPrompt) + { + doc.Root.Add(new XAttribute("senduserstatistics", sendUserStatistics)); + } + + XElement gMode = doc.Root.Element("graphicsmode"); + if (gMode == null) + { + gMode = new XElement("graphicsmode"); + doc.Root.Add(gMode); + } + if (GraphicsWidth == 0 || GraphicsHeight == 0) + { + gMode.ReplaceAttributes(new XAttribute("displaymode", windowMode)); + } + else + { + gMode.ReplaceAttributes( + new XAttribute("width", GraphicsWidth), + new XAttribute("height", GraphicsHeight), + new XAttribute("vsync", VSyncEnabled), + new XAttribute("displaymode", windowMode)); + } + + XElement gSettings = doc.Root.Element("graphicssettings"); + if (gSettings == null) + { + gSettings = new XElement("graphicssettings"); + doc.Root.Add(gSettings); + } + + gSettings.ReplaceAttributes( + new XAttribute("particlelimit", ParticleLimit), + new XAttribute("lightmapscale", LightMapScale), + new XAttribute("specularity", SpecularityEnabled), + new XAttribute("chromaticaberration", ChromaticAberrationEnabled), + new XAttribute("losmode", LosMode), + new XAttribute("hudscale", HUDScale), + new XAttribute("inventoryscale", InventoryScale)); + + foreach (ContentPackage contentPackage in SelectedContentPackages) + { + if (contentPackage.Path.Contains(vanillaContentPackagePath)) + { + doc.Root.Add(new XElement("contentpackage", new XAttribute("path", contentPackage.Path))); + break; + } + } + + var keyMappingElement = new XElement("keymapping"); + doc.Root.Add(keyMappingElement); + for (int i = 0; i < keyMapping.Length; i++) + { + if (keyMapping[i].MouseButton == null) + { + keyMappingElement.Add(new XAttribute(((InputType)i).ToString(), keyMapping[i].Key)); + } + else + { + keyMappingElement.Add(new XAttribute(((InputType)i).ToString(), keyMapping[i].MouseButton)); + } + } + + var gameplay = new XElement("gameplay"); + var jobPreferences = new XElement("jobpreferences"); + foreach (string jobName in JobPreferences) + { + jobPreferences.Add(new XElement("job", new XAttribute("identifier", jobName))); + } + gameplay.Add(jobPreferences); + doc.Root.Add(gameplay); + + var playerElement = new XElement("player", + new XAttribute("name", defaultPlayerName ?? ""), + new XAttribute("headindex", CharacterHeadIndex), + new XAttribute("gender", CharacterGender), + new XAttribute("race", CharacterRace), + new XAttribute("hairindex", CharacterHairIndex), + new XAttribute("beardindex", CharacterBeardIndex), + new XAttribute("moustacheindex", CharacterMoustacheIndex), + new XAttribute("faceattachmentindex", CharacterFaceAttachmentIndex)); + doc.Root.Add(playerElement); + + XmlWriterSettings settings = new XmlWriterSettings + { + Indent = true, + OmitXmlDeclaration = true, + NewLineOnAttributes = true + }; + + try + { + using (var writer = XmlWriter.Create(savePath, settings)) + { + doc.WriteTo(writer); + writer.Flush(); + } + } + catch (Exception e) + { + DebugConsole.ThrowError("Saving game settings failed.", e); + GameAnalyticsManager.AddErrorEventOnce("GameSettings.Save:SaveFailed", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + "Saving game settings failed.\n" + e.Message + "\n" + e.StackTrace); + } + } + #endregion + + #region Load PlayerConfig + // TODO: DRY + public void LoadPlayerConfig() + { + XDocument doc = XMLExtensions.LoadXml(playerSavePath); + + if (doc == null || doc.Root == null) + { + ShowUserStatisticsPrompt = true; + SaveNewPlayerConfig(); + return; + } + + Language = doc.Root.GetAttributeString("language", Language); + AutoCheckUpdates = doc.Root.GetAttributeBool("autocheckupdates", AutoCheckUpdates); + sendUserStatistics = doc.Root.GetAttributeBool("senduserstatistics", true); + + XElement graphicsMode = doc.Root.Element("graphicsmode"); + GraphicsWidth = graphicsMode.GetAttributeInt("width", GraphicsWidth); + GraphicsHeight = graphicsMode.GetAttributeInt("height", GraphicsHeight); + VSyncEnabled = graphicsMode.GetAttributeBool("vsync", VSyncEnabled); + + XElement graphicsSettings = doc.Root.Element("graphicssettings"); + ParticleLimit = graphicsSettings.GetAttributeInt("particlelimit", ParticleLimit); + LightMapScale = MathHelper.Clamp(graphicsSettings.GetAttributeFloat("lightmapscale", LightMapScale), 0.1f, 1.0f); + SpecularityEnabled = graphicsSettings.GetAttributeBool("specularity", SpecularityEnabled); + ChromaticAberrationEnabled = graphicsSettings.GetAttributeBool("chromaticaberration", ChromaticAberrationEnabled); + HUDScale = graphicsSettings.GetAttributeFloat("hudscale", HUDScale); + InventoryScale = graphicsSettings.GetAttributeFloat("inventoryscale", InventoryScale); + var losModeStr = graphicsSettings.GetAttributeString("losmode", "Transparent"); + if (!Enum.TryParse(losModeStr, out losMode)) + { + losMode = LosMode.Transparent; + } + +#if CLIENT + if (GraphicsWidth == 0 || GraphicsHeight == 0) + { + GraphicsWidth = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Width; + GraphicsHeight = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Height; + } +#endif + + var windowModeStr = graphicsMode.GetAttributeString("displaymode", "Fullscreen"); + if (!Enum.TryParse(windowModeStr, out windowMode)) + { + windowMode = WindowMode.Fullscreen; + } + + XElement audioSettings = doc.Root.Element("audio"); + if (audioSettings != null) + { + SoundVolume = audioSettings.GetAttributeFloat("soundvolume", SoundVolume); + MusicVolume = audioSettings.GetAttributeFloat("musicvolume", MusicVolume); + VoiceChatVolume = audioSettings.GetAttributeFloat("voicechatvolume", VoiceChatVolume); + string voiceSettingStr = audioSettings.GetAttributeString("voicesetting", "Disabled"); + VoiceCaptureDevice = audioSettings.GetAttributeString("voicecapturedevice", ""); + NoiseGateThreshold = audioSettings.GetAttributeFloat("noisegatethreshold", -45); + var voiceSetting = VoiceMode.Disabled; + if (Enum.TryParse(voiceSettingStr, out voiceSetting)) + { + VoiceSetting = voiceSetting; + } + } + + useSteamMatchmaking = doc.Root.GetAttributeBool("usesteammatchmaking", useSteamMatchmaking); + requireSteamAuthentication = doc.Root.GetAttributeBool("requiresteamauthentication", requireSteamAuthentication); + + EnableSplashScreen = doc.Root.GetAttributeBool("enablesplashscreen", EnableSplashScreen); + + AimAssistAmount = doc.Root.GetAttributeFloat("aimassistamount", AimAssistAmount); + + foreach (XElement subElement in doc.Root.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "keymapping": + foreach (XAttribute attribute in subElement.Attributes()) + { + if (Enum.TryParse(attribute.Name.ToString(), true, out InputType inputType)) + { + if (int.TryParse(attribute.Value.ToString(), out int mouseButton)) + { + keyMapping[(int)inputType] = new KeyOrMouse(mouseButton); + } + else + { + if (Enum.TryParse(attribute.Value.ToString(), true, out Keys key)) + { + keyMapping[(int)inputType] = new KeyOrMouse(key); + } + } + } + } + break; + case "gameplay": + jobPreferences = new List(); + foreach (XElement ele in subElement.Element("jobpreferences").Elements("job")) + { + string jobIdentifier = ele.GetAttributeString("identifier", ""); + if (string.IsNullOrEmpty(jobIdentifier)) continue; + jobPreferences.Add(jobIdentifier); + } + break; + case "player": + defaultPlayerName = subElement.GetAttributeString("name", defaultPlayerName); + CharacterHeadIndex = subElement.GetAttributeInt("headindex", CharacterHeadIndex); + if (Enum.TryParse(subElement.GetAttributeString("gender", "none"), true, out Gender g)) + { + CharacterGender = g; + } + if (Enum.TryParse(subElement.GetAttributeString("race", "white"), true, out Race r)) + { + CharacterRace = r; + } + else + { + CharacterRace = Race.White; + } + CharacterHairIndex = subElement.GetAttributeInt("hairindex", CharacterHairIndex); + CharacterBeardIndex = subElement.GetAttributeInt("beardindex", CharacterBeardIndex); + CharacterMoustacheIndex = subElement.GetAttributeInt("moustacheindex", CharacterMoustacheIndex); + CharacterFaceAttachmentIndex = subElement.GetAttributeInt("faceattachmentindex", CharacterFaceAttachmentIndex); + break; + case "tutorials": + foreach (XElement tutorialElement in subElement.Elements()) + { + CompletedTutorialNames.Add(tutorialElement.GetAttributeString("name", "")); + } + break; + } + } + + foreach (InputType inputType in Enum.GetValues(typeof(InputType))) + { + if (keyMapping[(int)inputType] == null) + { + DebugConsole.ThrowError("Key binding for the input type \"" + inputType + " not set!"); + keyMapping[(int)inputType] = new KeyOrMouse(Keys.D1); + } + } + + UnsavedSettings = false; + + selectedContentPackagePaths = new HashSet(); + + foreach (XElement subElement in doc.Root.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "contentpackage": + string path = System.IO.Path.GetFullPath(subElement.GetAttributeString("path", "")); + selectedContentPackagePaths.Add(path); + break; + } + } + + LoadContentPackages(selectedContentPackagePaths); + } + + public void ReloadContentPackages() + { + LoadContentPackages(selectedContentPackagePaths); + } + + private void LoadContentPackages(IEnumerable contentPackagePaths) + { + var missingPackagePaths = new List(); + var incompatiblePackages = new List(); + SelectedContentPackages.Clear(); + foreach (string path in contentPackagePaths) + { + var matchingContentPackage = ContentPackage.List.Find(cp => System.IO.Path.GetFullPath(cp.Path) == path); + + if (matchingContentPackage == null) + { + missingPackagePaths.Add(path); + } + else if (!matchingContentPackage.IsCompatible()) + { + incompatiblePackages.Add(matchingContentPackage); + } + else + { + SelectedContentPackages.Add(matchingContentPackage); + } + } + + TextManager.LoadTextPacks(SelectedContentPackages); + + foreach (ContentPackage contentPackage in SelectedContentPackages) + { + foreach (ContentFile file in contentPackage.Files) + { + if (!System.IO.File.Exists(file.Path)) + { + DebugConsole.ThrowError("Error in content package \"" + contentPackage.Name + "\" - file \"" + file.Path + "\" not found."); + continue; + } + ToolBox.IsProperFilenameCase(file.Path); + } + } + if (!SelectedContentPackages.Any()) + { + var availablePackage = ContentPackage.List.FirstOrDefault(cp => cp.IsCompatible() && cp.CorePackage); + if (availablePackage != null) + { + SelectedContentPackages.Add(availablePackage); + } + } + + //save to get rid of the invalid selected packages in the config file + if (missingPackagePaths.Count > 0 || incompatiblePackages.Count > 0) { SaveNewPlayerConfig(); } + + //display error messages after all content packages have been loaded + //to make sure the package that contains text files has been loaded before we attempt to use TextManager + foreach (string missingPackagePath in missingPackagePaths) + { + DebugConsole.ThrowError(TextManager.Get("ContentPackageNotFound").Replace("[packagepath]", missingPackagePath)); + } + foreach (ContentPackage incompatiblePackage in incompatiblePackages) + { + DebugConsole.ThrowError(TextManager.Get(incompatiblePackage.GameVersion <= new Version(0, 0, 0, 0) ? "IncompatibleContentPackageUnknownVersion" : "IncompatibleContentPackage") + .Replace("[packagename]", incompatiblePackage.Name) + .Replace("[packageversion]", incompatiblePackage.GameVersion.ToString()) + .Replace("[gameversion]", GameMain.Version.ToString())); + } + } + #endregion + #region Save PlayerConfig public void SaveNewPlayerConfig() { diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/Holdable.cs index 593b02460..5d3f4dcfc 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/Holdable.cs @@ -438,7 +438,7 @@ namespace Barotrauma.Items.Components GameServer.Log(character.LogName + " attached " + item.Name + " to a wall", ServerLog.MessageType.ItemInteraction); } #endif - item.Drop(); + item.Drop(character); } AttachToWall(); diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/RepairTool.cs index 400e69223..2885ca95f 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/RepairTool.cs @@ -294,9 +294,10 @@ namespace Barotrauma.Items.Components character.CursorPosition = leak.Position; character.SetInput(InputType.Aim, false, true); - if (VectorExtensions.Angle(VectorExtensions.Forward(item.body.TransformedRotation), fromItemToLeak) < MathHelper.PiOver4) + // Press the trigger only when the tool is approximately facing the target. + // If the character is climbing, ignore the check, because we cannot aim while climbing. + if (character.IsClimbing || VectorExtensions.Angle(VectorExtensions.Forward(item.body.TransformedRotation), fromItemToLeak) < MathHelper.PiOver4) { - // Press the trigger only when the tool is approximately facing the target. Use(deltaTime, character); } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/Throwable.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/Throwable.cs index 9fd196905..b6107eada 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/Throwable.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/Throwable.cs @@ -105,7 +105,7 @@ namespace Barotrauma.Items.Components GameServer.Log(picker.LogName + " threw " + item.Name, ServerLog.MessageType.ItemInteraction); #endif - item.Drop(); + item.Drop(picker); item.body.ApplyLinearImpulse(throwVector * throwForce * item.body.Mass * 3.0f); ac.GetLimb(LimbType.Head).body.ApplyLinearImpulse(throwVector*10.0f); diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/ItemContainer.cs index 97c950c1c..11a3ea4e0 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/ItemContainer.cs @@ -279,7 +279,7 @@ namespace Barotrauma.Items.Components foreach (Item item in Inventory.Items) { if (item == null) continue; - item.Drop(); + item.Drop(null); } } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Fabricator.cs index 49e2b38a0..a3af545f4 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Fabricator.cs @@ -420,7 +420,7 @@ namespace Barotrauma.Items.Components if (inputContainer.Inventory.Items.All(it => it != null)) { var unneededItem = inputContainer.Inventory.Items.FirstOrDefault(it => !usedItems.Contains(it)); - unneededItem?.Drop(); + unneededItem?.Drop(null); } inputContainer.Inventory.TryPutItem(matchingItem, user: null, createNetworkEvent: true); } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Reactor.cs index 741306c51..660bc5dd8 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Reactor.cs @@ -437,7 +437,7 @@ namespace Barotrauma.Items.Components { if (item != null && item.Condition <= 0.0f) { - item.Drop(); + item.Drop(character); } } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Projectile.cs index d8f76c924..320adf9ab 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Projectile.cs @@ -173,7 +173,7 @@ namespace Barotrauma.Items.Components private void Launch(Vector2 impulse) { - item.Drop(); + item.Drop(null); item.body.Enabled = true; item.body.ApplyLinearImpulse(impulse); @@ -213,7 +213,7 @@ namespace Barotrauma.Items.Components { float rotation = item.body.Rotation; Vector2 simPositon = item.SimPosition; - item.Drop(); + item.Drop(null); item.body.Enabled = true; //set the velocity of the body because the OnProjectileCollision method diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Turret.cs index 968e06ea2..a74c4f4e2 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Turret.cs @@ -350,7 +350,7 @@ namespace Barotrauma.Items.Components { reload = reloadTime; - projectile.Drop(); + projectile.Drop(null); projectile.body.Dir = 1.0f; projectile.body.ResetDynamics(); diff --git a/Barotrauma/BarotraumaShared/Source/Items/Item.cs b/Barotrauma/BarotraumaShared/Source/Items/Item.cs index 2290db26a..29915b1d1 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Item.cs @@ -1563,7 +1563,7 @@ namespace Barotrauma return isCombined; } - public void Drop(Character dropper = null) + public void Drop(Character dropper) { if (parentInventory != null && !parentInventory.Owner.Removed && !Removed && GameMain.NetworkMember != null && (GameMain.NetworkMember.IsServer || Character.Controlled == dropper)) diff --git a/Barotrauma/BarotraumaShared/Source/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/Source/Map/Map/Map.cs index 5e0b5c4dc..c3d5d2926 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Map/Map.cs @@ -430,13 +430,6 @@ namespace Barotrauma } CurrentLocation.SelectedMissionIndex = missionIndex; - //the destination must be the same as the destination of the mission - if (CurrentLocation.SelectedMission != null && - CurrentLocation.SelectedMission.Locations[1] != SelectedLocation) - { - SelectLocation(CurrentLocation.SelectedMission.Locations[1]); - } - SelectedLocation = location; SelectedConnection = connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation)); OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection); diff --git a/Barotrauma/BarotraumaShared/Source/Map/Submarine.cs b/Barotrauma/BarotraumaShared/Source/Map/Submarine.cs index 92e6c3a45..44fbb7299 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Submarine.cs @@ -467,6 +467,7 @@ namespace Barotrauma public List GetConnectedSubs() { connectedSubs.Clear(); + connectedSubs.Add(this); GetConnectedSubsRecursive(connectedSubs); return connectedSubs; @@ -522,6 +523,30 @@ namespace Barotrauma { maxX = Math.Min(maxX, ruin.Area.X - 100.0f); } + else + { + maxX = Math.Min(maxX, ruin.Area.X - 100.0f); + } + } + + if (minX < 0.0f && maxX > Level.Loaded.Size.X) + { + //no walls found at either side, just use the initial spawnpos and hope for the best + } + else if (minX < 0) + { + //no wall found at the left side, spawn to the left from the right-side wall + spawnPos.X = maxX - minWidth - 100.0f; + } + else if (maxX > Level.Loaded.Size.X) + { + //no wall found at right side, spawn to the right from the left-side wall + spawnPos.X = minX + minWidth + 100.0f; + } + else + { + //walls found at both sides, use their midpoint + spawnPos.X = (minX + maxX) / 2; } if (minX < 0.0f && maxX > Level.Loaded.Size.X) @@ -655,7 +680,7 @@ namespace Barotrauma } } - public static Body PickBody(Vector2 rayStart, Vector2 rayEnd, List ignoredBodies = null, Category? collisionCategory = null, bool ignoreSensors = true, Predicate customPredicate = null) + public static Body PickBody(Vector2 rayStart, Vector2 rayEnd, IEnumerable ignoredBodies = null, Category? collisionCategory = null, bool ignoreSensors = true, Predicate customPredicate = null) { if (Vector2.DistanceSquared(rayStart, rayEnd) < 0.00001f) { @@ -1011,6 +1036,31 @@ namespace Barotrauma return false; } + /// + /// Returns true if the sub is same as the other. + /// + public bool IsConnectedTo(Submarine otherSub) => this == otherSub || GetConnectedSubs().Contains(otherSub); + + public List GetHulls(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Hull.hullList); + public List GetGaps(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Gap.GapList); + public List GetItems(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Item.ItemList); + + public List GetEntities(bool includingConnectedSubs, List list) where T : MapEntity + { + return list.FindAll(e => IsEntityFoundOnThisSub(e, includingConnectedSubs)); + } + + public bool IsEntityFoundOnThisSub(MapEntity entity, bool includingConnectedSubs) + { + if (entity.Submarine == this) { return true; } + if (entity.Submarine == null) { return false; } + if (includingConnectedSubs) + { + return GetConnectedSubs().Any(s => s == entity.Submarine && entity.Submarine.TeamID == TeamID); + } + return false; + } + /// /// Finds the sub whose borders contain the position /// diff --git a/Barotrauma/BarotraumaShared/Source/PlayerInput.cs b/Barotrauma/BarotraumaShared/Source/PlayerInput.cs index fc56c8fc7..dac0e7d44 100644 --- a/Barotrauma/BarotraumaShared/Source/PlayerInput.cs +++ b/Barotrauma/BarotraumaShared/Source/PlayerInput.cs @@ -160,16 +160,6 @@ namespace Barotrauma } #endif - public void SetState() - { - hit = binding.IsHit(); - if (hit) hitQueue = true; - - held = binding.IsDown(); - if (held) heldQueue = true; - } -#endif - public bool Hit { get diff --git a/Barotrauma/BarotraumaShared/Submarines/Dugong.sub b/Barotrauma/BarotraumaShared/Submarines/Dugong.sub index e6b2f571a..97196e8ec 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Dugong.sub and b/Barotrauma/BarotraumaShared/Submarines/Dugong.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Venture.sub b/Barotrauma/BarotraumaShared/Submarines/Venture.sub new file mode 100644 index 000000000..8cb2b6a3d Binary files /dev/null and b/Barotrauma/BarotraumaShared/Submarines/Venture.sub differ