diff --git a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs index b38f63525..8d7e7d4e0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs @@ -37,7 +37,7 @@ namespace Barotrauma public float MinZoom { get { return minZoom;} - set { minZoom = MathHelper.Clamp(value, 0.01f, 10.0f); } + set { minZoom = MathHelper.Clamp(value, 0.001f, 10.0f); } } private float maxZoom = 2.0f; @@ -51,8 +51,6 @@ namespace Barotrauma private float zoom; - private float offsetAmount; - private Matrix transform, shaderTransform, viewMatrix; private Vector2 position; private float rotation; @@ -67,16 +65,9 @@ namespace Barotrauma public float Shake; private Vector2 shakePosition; private float shakeTimer; - - //the area of the world inside the camera view - private Rectangle worldView; private float globalZoomScale = 1.0f; - private Point resolution; - - private Vector2 targetPos; - //used to smooth out the movement when in freecam private float targetZoom; private Vector2 velocity; @@ -89,10 +80,10 @@ namespace Barotrauma zoom = MathHelper.Clamp(value, GameMain.DebugDraw ? 0.01f : MinZoom, MaxZoom); Vector2 center = WorldViewCenter; - float newWidth = resolution.X / zoom; - float newHeight = resolution.Y / zoom; + float newWidth = Resolution.X / zoom; + float newHeight = Resolution.Y / zoom; - worldView = new Rectangle( + WorldView = new Rectangle( (int)(center.X - newWidth / 2.0f), (int)(center.Y + newHeight / 2.0f), (int)newWidth, @@ -122,29 +113,20 @@ namespace Barotrauma } } - public float OffsetAmount - { - get { return offsetAmount; } - set { offsetAmount = value; } - } + public float OffsetAmount { get; set; } - public Point Resolution - { - get { return resolution; } - } + public Point Resolution { get; private set; } - public Rectangle WorldView - { - get { return worldView; } - } + //the area of the world inside the camera view + public Rectangle WorldView { get; private set; } public Vector2 WorldViewCenter { get { return new Vector2( - worldView.X + worldView.Width / 2.0f, - worldView.Y - worldView.Height / 2.0f); + WorldView.X + WorldView.Width / 2.0f, + WorldView.Y - WorldView.Height / 2.0f); } } @@ -171,12 +153,13 @@ namespace Barotrauma UpdateTransform(false); } - public Vector2 TargetPos + ~Camera() { - get { return targetPos; } - set { targetPos = value; } + GameMain.Instance.ResolutionChanged -= CreateMatrices; } + public Vector2 TargetPos { get; set; } + public Vector2 GetPosition() { return position; @@ -204,21 +187,29 @@ namespace Barotrauma public void SetResolution(Point res) { - resolution = res; + Resolution = res; - worldView = new Rectangle(0, 0, res.X, res.Y); + WorldView = new Rectangle(0, 0, res.X, res.Y); viewMatrix = Matrix.CreateTranslation(new Vector3(res.X / 2.0f, res.Y / 2.0f, 0)); - globalZoomScale = (float)Math.Pow(new Vector2(GUI.UIWidth, resolution.Y).Length() / GUI.ReferenceResolution.Length(), 2); + float newGlobalZoomScale = (float)new Vector2(GUI.UIWidth, Resolution.Y).Length() / GUI.ReferenceResolution.Length(); + if (globalZoomScale > 0.0f) + { + Zoom *= newGlobalZoomScale / globalZoomScale; + targetZoom *= newGlobalZoomScale / globalZoomScale; + prevZoom *= newGlobalZoomScale / globalZoomScale; + } + globalZoomScale = newGlobalZoomScale; } - public void UpdateTransform(bool interpolate = true) + public void UpdateTransform(bool interpolate = true, bool updateListener = true) { Vector2 interpolatedPosition = interpolate ? Timing.Interpolate(prevPosition, position) : position; float interpolatedZoom = interpolate ? Timing.Interpolate(prevZoom, zoom) : zoom; - worldView.X = (int)(interpolatedPosition.X - worldView.Width / 2.0); - worldView.Y = (int)(interpolatedPosition.Y + worldView.Height / 2.0); + WorldView = new Rectangle((int)(interpolatedPosition.X - WorldView.Width / 2.0), + (int)(interpolatedPosition.Y + WorldView.Height / 2.0), + WorldView.Width, WorldView.Height); transform = Matrix.CreateTranslation( new Vector3(-interpolatedPosition.X, interpolatedPosition.Y, 0)) * @@ -227,19 +218,22 @@ namespace Barotrauma shaderTransform = Matrix.CreateTranslation( new Vector3( - -interpolatedPosition.X - resolution.X / interpolatedZoom / 2.0f, - -interpolatedPosition.Y - resolution.Y / interpolatedZoom / 2.0f, 0)) * + -interpolatedPosition.X - Resolution.X / interpolatedZoom / 2.0f, + -interpolatedPosition.Y - Resolution.Y / interpolatedZoom / 2.0f, 0)) * Matrix.CreateScale(new Vector3(interpolatedZoom, interpolatedZoom, 1)) * viewMatrix * Matrix.CreateRotationZ(-rotation); - if (Character.Controlled == null) + if (updateListener) { - GameMain.SoundManager.ListenerPosition = new Vector3(WorldViewCenter.X, WorldViewCenter.Y, -(100.0f / zoom)); - } - else - { - GameMain.SoundManager.ListenerPosition = new Vector3(Character.Controlled.WorldPosition.X, Character.Controlled.WorldPosition.Y, -(100.0f / zoom)); + if (Character.Controlled == null) + { + GameMain.SoundManager.ListenerPosition = new Vector3(WorldViewCenter.X, WorldViewCenter.Y, -(100.0f / zoom)); + } + else + { + GameMain.SoundManager.ListenerPosition = new Vector3(Character.Controlled.WorldPosition.X, Character.Controlled.WorldPosition.Y, -(100.0f / zoom)); + } } @@ -257,7 +251,7 @@ namespace Barotrauma /// public bool Freeze { get; set; } - public void MoveCamera(float deltaTime, bool allowMove = true, bool allowZoom = true) + public void MoveCamera(float deltaTime, bool allowMove = true, bool allowZoom = true, bool? followSub = null) { prevPosition = position; prevZoom = zoom; @@ -265,7 +259,7 @@ namespace Barotrauma float moveSpeed = 20.0f / zoom; Vector2 moveCam = Vector2.Zero; - if (targetPos == Vector2.Zero) + if (TargetPos == Vector2.Zero) { Vector2 moveInput = Vector2.Zero; if (allowMove && !Freeze) @@ -284,7 +278,7 @@ namespace Barotrauma velocity = Vector2.Lerp(velocity, moveInput, deltaTime * 10.0f); moveCam = velocity * moveSpeed * deltaTime * FreeCamMoveSpeed * 60.0f; - if (Screen.Selected == GameMain.GameScreen && FollowSub) + if (Screen.Selected == GameMain.GameScreen && (followSub ?? FollowSub)) { var closestSub = Submarine.FindClosest(WorldViewCenter); if (closestSub != null) @@ -294,7 +288,7 @@ namespace Barotrauma } } - if (allowZoom && GUI.MouseOn == null) + if (allowZoom) { Vector2 mouseInWorld = ScreenToWorld(PlayerInput.MousePosition); Vector2 diffViewCenter; @@ -318,14 +312,15 @@ namespace Barotrauma else if (allowMove) { Vector2 mousePos = PlayerInput.MousePosition; - Vector2 offset = mousePos - resolution.ToVector2() / 2; - offset.X = offset.X / (resolution.X * 0.4f); - offset.Y = -offset.Y / (resolution.Y * 0.3f); + Vector2 offset = mousePos - Resolution.ToVector2() / 2; + offset.X = offset.X / (Resolution.X * 0.4f); + offset.Y = -offset.Y / (Resolution.Y * 0.3f); if (offset.LengthSquared() > 1.0f) offset.Normalize(); - offset *= offsetAmount; + float offsetUnscaledLen = offset.Length(); + offset *= OffsetAmount; // Freeze the camera movement by default, when the cursor is on top of an ui element. // Setting a positive value to the OffsetAmount, will override this behaviour. - if (GUI.MouseOn != null && offsetAmount > 0) + if (GUI.MouseOn != null && OffsetAmount > 0) { Freeze = true; } @@ -336,7 +331,7 @@ namespace Barotrauma } if (Freeze) { - offset = previousOffset; + if (offset.LengthSquared() > 0.001f) { offset = previousOffset; } } else { @@ -344,24 +339,19 @@ namespace Barotrauma } //how much to zoom out (zoom completely out when offset is 1000) - float zoomOutAmount = GetZoomAmount(offset); - //zoom amount when resolution is not taken into account - float unscaledZoom = MathHelper.Lerp(DefaultZoom, MinZoom, zoomOutAmount); - //zoom with resolution taken into account (zoom further out on smaller resolutions) - float scaledZoom = unscaledZoom * globalZoomScale; - - //an ad-hoc way of allowing the players to have roughly the same maximum view distance regardless of the resolution, - //while still keeping the zoom around 1.0 when not looking further away (because otherwise we'd always be downsampling - //on lower resolutions, which doesn't look that good) - float newZoom = MathHelper.Lerp(unscaledZoom, scaledZoom, - (GameMain.Config == null || GameMain.Config.EnableMouseLook) ? (float)Math.Sqrt(zoomOutAmount) : 0.3f); + float zoomOutAmount = GetZoomAmount(offset); + //scaled zoom amount + float scaledZoom = MathHelper.Lerp(DefaultZoom, MinZoom, zoomOutAmount) * globalZoomScale; + //zoom in further if zoomOutAmount is low and resolution is lower than reference + float newZoom = scaledZoom * (MathHelper.Lerp(0.3f * (1f - Math.Min(globalZoomScale, 1f)), 0f, + (GameMain.Config == null || GameMain.Config.EnableMouseLook) ? (float)Math.Sqrt(offsetUnscaledLen) : 0.3f) + 1f); Zoom += (newZoom - zoom) / ZoomSmoothness; //force targetzoom to the current zoom value, so the camera stays at the same zoom when switching to freecam targetZoom = Zoom; - Vector2 diff = (targetPos + offset) - position; + Vector2 diff = (TargetPos + offset) - position; moveCam = diff / MoveSmoothness; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/AITarget.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/AITarget.cs index a1be8d950..cdf6ce5f5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/AITarget.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/AITarget.cs @@ -11,6 +11,9 @@ namespace Barotrauma { if (!ShowAITargets) { return; } var pos = new Vector2(WorldPosition.X, -WorldPosition.Y); + float thickness = 1 / Screen.Selected.Cam.Zoom; + + float offset = MathUtils.VectorToAngle(new Vector2(sectorDir.X, -sectorDir.Y)) - (sectorRad / 2f); if (soundRange > 0.0f) { Color color; @@ -26,8 +29,16 @@ namespace Barotrauma { color = Color.OrangeRed; } - ShapeExtensions.DrawCircle(spriteBatch, pos, SoundRange, 100, color, thickness: 1 / Screen.Selected.Cam.Zoom); - ShapeExtensions.DrawCircle(spriteBatch, pos, 3, 8, color, thickness: 2 / Screen.Selected.Cam.Zoom); + + if (sectorRad < MathHelper.TwoPi) + { + spriteBatch.DrawSector(pos, SoundRange, sectorRad, 100, color, offset: offset, thickness: thickness); + } + else + { + spriteBatch.DrawCircle(pos, SoundRange, 100, color, thickness: thickness); + } + spriteBatch.DrawCircle(pos, 3, 8, color, thickness: 2 / Screen.Selected.Cam.Zoom); GUI.DrawLine(spriteBatch, pos, pos + Vector2.UnitY * SoundRange, color, width: (int)(1 / Screen.Selected.Cam.Zoom) + 1); } if (sightRange > 0.0f) @@ -47,7 +58,14 @@ namespace Barotrauma // disable the indicators for structures and hulls, because they clutter the debug view return; } - ShapeExtensions.DrawCircle(spriteBatch, pos, SightRange, 100, color, thickness: 1 / Screen.Selected.Cam.Zoom); + if (sectorRad < MathHelper.TwoPi) + { + spriteBatch.DrawSector(pos, SightRange, sectorRad, 100, color, offset: offset, thickness: thickness); + } + else + { + spriteBatch.DrawCircle(pos, SightRange, 100, color, thickness: thickness); + } ShapeExtensions.DrawCircle(spriteBatch, pos, 6, 8, color, thickness: 2 / Screen.Selected.Cam.Zoom); GUI.DrawLine(spriteBatch, pos, pos + Vector2.UnitY * SightRange, color, width: (int)(1 / Screen.Selected.Cam.Zoom) + 1); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs index cff4a5b16..42ffa3a40 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs @@ -1,20 +1,12 @@ using Microsoft.Xna.Framework; using FarseerPhysics; +using System; +using System.Linq; namespace Barotrauma { partial class HumanAIController : AIController { - partial void InitProjSpecific() - { - /*if (GameMain.GameSession != null && GameMain.GameSession.CrewManager != null) - { - CurrentOrder = Order.GetPrefab("dismissed"); - objectiveManager.SetOrder(CurrentOrder, "", null); - GameMain.GameSession.CrewManager.SetCharacterOrder(Character, CurrentOrder, null, null); - }*/ - } - public override void DebugDraw(Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch) { if (Character == Character.Controlled) { return; } @@ -22,6 +14,7 @@ namespace Barotrauma Vector2 pos = Character.WorldPosition; pos.Y = -pos.Y; Vector2 textOffset = new Vector2(-40, -160); + textOffset.Y -= Math.Max(ObjectiveManager.CurrentOrders.Count - 1, 0) * 20; if (SelectedAiTarget?.Entity != null) { @@ -29,56 +22,55 @@ namespace Barotrauma //GUI.DrawString(spriteBatch, pos + textOffset, $"AI TARGET: {SelectedAiTarget.Entity.ToString()}", Color.White, Color.Black); } - GUI.DrawString(spriteBatch, pos + textOffset, Character.Name, Color.White, Color.Black); + Vector2 stringDrawPos = pos + textOffset; + GUI.DrawString(spriteBatch, stringDrawPos, Character.Name, Color.White, Color.Black); var currentOrder = ObjectiveManager.CurrentOrder; - if (currentOrder != null) + if (ObjectiveManager.CurrentOrders.Any()) { - GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 20), $"ORDER: {currentOrder.DebugTag} ({currentOrder.Priority.FormatZeroDecimal()})", Color.White, Color.Black); + var currentOrders = ObjectiveManager.CurrentOrders; + currentOrders.Sort((x, y) => y.ManualPriority.CompareTo(x.ManualPriority)); + for (int i = 0; i < currentOrders.Count; i++) + { + stringDrawPos += new Vector2(0, 20); + var order = currentOrders[i]; + GUI.DrawString(spriteBatch, stringDrawPos, $"ORDER {i + 1}: {order.Objective.DebugTag} ({order.Objective.Priority.FormatZeroDecimal()})", Color.White, Color.Black); + } } else if (ObjectiveManager.WaitTimer > 0) { - GUI.DrawString(spriteBatch, pos + new Vector2(0, 20), $"Waiting... {ObjectiveManager.WaitTimer.FormatZeroDecimal()}", Color.White, Color.Black); + stringDrawPos += new Vector2(0, 20); + GUI.DrawString(spriteBatch, stringDrawPos - textOffset, $"Waiting... {ObjectiveManager.WaitTimer.FormatZeroDecimal()}", Color.White, Color.Black); } var currentObjective = ObjectiveManager.CurrentObjective; if (currentObjective != null) { - int offset = currentOrder != null ? 20 : 0; + int offset = currentOrder != null ? 20 + ((ObjectiveManager.CurrentOrders.Count - 1) * 20) : 0; if (currentOrder == null || currentOrder.Priority <= 0) { - GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 20 + offset), $"MAIN OBJECTIVE: {currentObjective.DebugTag} ({currentObjective.Priority.FormatZeroDecimal()})", Color.White, Color.Black); + stringDrawPos += new Vector2(0, 20); + GUI.DrawString(spriteBatch, stringDrawPos, $"MAIN OBJECTIVE: {currentObjective.DebugTag} ({currentObjective.Priority.FormatZeroDecimal()})", Color.White, Color.Black); } var subObjective = currentObjective.CurrentSubObjective; if (subObjective != null) { - GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 40 + offset), $"SUBOBJECTIVE: {subObjective.DebugTag} ({subObjective.Priority.FormatZeroDecimal()})", Color.White, Color.Black); + stringDrawPos += new Vector2(0, 20); + GUI.DrawString(spriteBatch, stringDrawPos, $"SUBOBJECTIVE: {subObjective.DebugTag} ({subObjective.Priority.FormatZeroDecimal()})", Color.White, Color.Black); } var activeObjective = ObjectiveManager.GetActiveObjective(); if (activeObjective != null) { - GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 60 + offset), $"ACTIVE OBJECTIVE: {activeObjective.DebugTag} ({activeObjective.Priority.FormatZeroDecimal()})", Color.White, Color.Black); + stringDrawPos += new Vector2(0, 20); + GUI.DrawString(spriteBatch, stringDrawPos, $"ACTIVE OBJECTIVE: {activeObjective.DebugTag} ({activeObjective.Priority.FormatZeroDecimal()})", Color.White, Color.Black); } } + + Vector2 objectiveStringDrawPos = stringDrawPos + new Vector2(120, 40); for (int i = 0; i < ObjectiveManager.Objectives.Count; i++) { var objective = ObjectiveManager.Objectives[i]; - int offsetMultiplier; - if (ObjectiveManager.CurrentOrder == null) - { - if (i == 0) - { - continue; - } - else - { - offsetMultiplier = i - 1; - } - } - else - { - offsetMultiplier = i + 1; - } - GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(120, offsetMultiplier * 18 + 100), $"{objective.DebugTag} ({objective.Priority.FormatZeroDecimal()})", Color.White, Color.Black * 0.5f); + GUI.DrawString(spriteBatch, objectiveStringDrawPos, $"{objective.DebugTag} ({objective.Priority.FormatZeroDecimal()})", Color.White, Color.Black * 0.5f); + objectiveStringDrawPos += new Vector2(0, 18); } if (steeringManager is IndoorsSteeringManager pathSteering) @@ -106,7 +98,7 @@ namespace Barotrauma new Vector2(path.CurrentNode.DrawPosition.X, -path.CurrentNode.DrawPosition.Y), Color.BlueViolet, 0, 3); - GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 100), "Path cost: " + path.Cost.FormatZeroDecimal(), Color.White, Color.Black * 0.5f); + GUI.DrawString(spriteBatch, stringDrawPos + new Vector2(0, 40), "Path cost: " + path.Cost.FormatZeroDecimal(), Color.White, Color.Black * 0.5f); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 07f9c4823..20feee440 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -55,6 +55,11 @@ namespace Barotrauma set { if (controlled == value) return; + if ((!(controlled is null)) && (!(Screen.Selected?.Cam is null)) && value is null) + { + Screen.Selected.Cam.TargetPos = Vector2.Zero; + Lights.LightManager.ViewTarget = null; + } controlled = value; if (controlled != null) controlled.Enabled = true; CharacterHealth.OpenHealthWindow = null; @@ -96,6 +101,13 @@ namespace Barotrauma get { return chromaticAberrationStrength; } set { chromaticAberrationStrength = MathHelper.Clamp(value, 0.0f, 100.0f); } } + + private float grainStrength; + public float GrainStrength + { + get => grainStrength; + set => grainStrength = MathHelper.Clamp(value, 0.0f, 1.0f); + } private readonly List bloodEmitters = new List(); public IEnumerable BloodEmitters @@ -115,6 +127,9 @@ namespace Barotrauma get { return gibEmitters; } } + public static bool IsMouseOnUI => GUI.MouseOn != null || + (CharacterInventory.IsMouseOnInventory() && !CharacterInventory.DraggingItemToWorld); + public class ObjectiveEntity { public Entity Entity; @@ -217,8 +232,7 @@ namespace Barotrauma float targetOffsetAmount = 0.0f; if (moveCam) { - if (NeedsAir && - pressureProtection < 80.0f && + if (NeedsAir && !IsProtectedFromPressure() && (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure > 0.0f)) { float pressure = AnimController.CurrentHull == null ? 100.0f : AnimController.CurrentHull.LethalPressure; @@ -285,6 +299,10 @@ namespace Barotrauma cam.OffsetAmount = targetOffsetAmount = 0.0f; } } + else if (IsMouseOnUI) + { + targetOffsetAmount = cam.OffsetAmount; + } else if (Vector2.DistanceSquared(AnimController.Limbs[0].SimPosition, mouseSimPos) > 1.0f) { Body body = Submarine.CheckVisibility(AnimController.Limbs[0].SimPosition, mouseSimPos); @@ -375,25 +393,63 @@ namespace Barotrauma partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool log) { + HintManager.OnCharacterKilled(this); + if (GameMain.NetworkMember != null && controlled == this) { string chatMessage = CauseOfDeath.Type == CauseOfDeathType.Affliction ? CauseOfDeath.Affliction.SelfCauseOfDeathDescription : TextManager.Get("Self_CauseOfDeathDescription." + CauseOfDeath.Type.ToString(), fallBackTag: "Self_CauseOfDeathDescription.Damage"); - if (GameMain.Client != null) chatMessage += " " + TextManager.Get("DeathChatNotification"); + if (GameMain.Client != null) { chatMessage += " " + TextManager.Get("DeathChatNotification"); } + + if (GameMain.NetworkMember.RespawnManager?.UseRespawnPrompt ?? false) + { + CoroutineManager.InvokeAfter(() => + { + if (controlled != null || (!(GameMain.GameSession?.IsRunning ?? false))) { return; } + var respawnPrompt = new GUIMessageBox( + TextManager.Get("tutorial.tryagainheader"), TextManager.Get("respawnquestionprompt"), + new string[] { TextManager.Get("respawnquestionpromptrespawn"), TextManager.Get("respawnquestionpromptwait") }); + respawnPrompt.Buttons[0].OnClicked += (btn, userdata) => + { + GameMain.Client?.SendRespawnPromptResponse(waitForNextRoundRespawn: false); + respawnPrompt.Close(); + return true; + }; + respawnPrompt.Buttons[1].OnClicked += (btn, userdata) => + { + GameMain.Client?.SendRespawnPromptResponse(waitForNextRoundRespawn: true); + respawnPrompt.Close(); + return true; + }; + }, delay: 5.0f); + } GameMain.NetworkMember.AddChatMessage(chatMessage, ChatMessageType.Dead); GameMain.LightManager.LosEnabled = false; controlled = null; + if (!(Screen.Selected?.Cam is null)) + { + Screen.Selected.Cam.TargetPos = Vector2.Zero; + Lights.LightManager.ViewTarget = null; + } } - + PlaySound(CharacterSound.SoundType.Die); } partial void DisposeProjSpecific() { - if (controlled == this) controlled = null; + if (controlled == this) + { + controlled = null; + if (!(Screen.Selected?.Cam is null)) + { + Screen.Selected.Cam.TargetPos = Vector2.Zero; + Lights.LightManager.ViewTarget = null; + } + } if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.GetCharacters().Contains(this)) @@ -634,9 +690,9 @@ namespace Barotrauma } } - partial void SetOrderProjSpecific(Order order, string orderOption) + partial void SetOrderProjSpecific(Order order, string orderOption, int priority) { - GameMain.GameSession?.CrewManager?.AddCurrentOrderIcon(this, order, orderOption); + GameMain.GameSession?.CrewManager?.AddCurrentOrderIcon(this, order, orderOption, priority); } public static void AddAllToGUIUpdateList() @@ -815,7 +871,7 @@ namespace Barotrauma iconPos.Y = -iconPos.Y; nameColor = iconStyle.Color; var icon = iconStyle.Sprites[GUIComponent.ComponentState.None].First(); - float iconScale = 30.0f / icon.Sprite.size.X / cam.Zoom; + float iconScale = (30.0f / icon.Sprite.size.X / cam.Zoom) * GUI.Scale; icon.Sprite.Draw(spriteBatch, iconPos + new Vector2(-35.0f, -25.0f), iconStyle.Color * hudInfoAlpha, scale: iconScale); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index 3b2b2fe3f..7cb31a3a8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; @@ -10,7 +11,57 @@ using System.Linq; namespace Barotrauma { class CharacterHUD - { + { + const float BossHealthBarDuration = 120.0f; + + class BossHealthBar + { + public readonly Character Character; + public float FadeTimer; + + public readonly GUIComponent TopContainer; + public readonly GUIComponent SideContainer; + + public readonly GUIProgressBar TopHealthBar; + public readonly GUIProgressBar SideHealthBar; + + public BossHealthBar(Character character) + { + Character = character; + FadeTimer = BossHealthBarDuration; + + TopContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.18f, 0.03f), HUDFrame.RectTransform, Anchor.TopCenter) + { + MinSize = new Point(100, 50), + RelativeOffset = new Vector2(0.0f, 0.01f) + }, isHorizontal: false, childAnchor: Anchor.TopCenter); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), TopContainer.RectTransform), character.DisplayName, textAlignment: Alignment.Center, textColor: GUI.Style.Red); + TopHealthBar = new GUIProgressBar(new RectTransform(new Vector2(1.0f, 0.6f), TopContainer.RectTransform) + { + MinSize = new Point(100, HUDLayoutSettings.HealthBarArea.Size.Y) + }, barSize: 0.0f, style: "CharacterHealthBarCentered") + { + Color = GUI.Style.Red + }; + + SideContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), bossHealthContainer.RectTransform) + { + MinSize = new Point(80, 60) + }, isHorizontal: false, childAnchor: Anchor.TopRight); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), SideContainer.RectTransform), character.DisplayName, textAlignment: Alignment.CenterRight, textColor: GUI.Style.Red); + SideHealthBar = new GUIProgressBar(new RectTransform(new Vector2(1.0f, 0.7f), SideContainer.RectTransform), barSize: 0.0f, style: "CharacterHealthBar") + { + Color = GUI.Style.Red + }; + + TopContainer.Visible = SideContainer.Visible = false; + TopContainer.CanBeFocused = false; + TopContainer.Children.ForEach(c => c.CanBeFocused = false); + SideContainer.CanBeFocused = false; + SideContainer.Children.ForEach(c => c.CanBeFocused = false); + } + } + private static readonly Dictionary orderIndicatorCount = new Dictionary(); const float ItemOverlayDelay = 1.0f; private static Item focusedItem; @@ -19,8 +70,12 @@ namespace Barotrauma private static readonly List brokenItems = new List(); private static float brokenItemsCheckTimer; + private static readonly List bossHealthBars = new List(); + private static readonly Dictionary cachedHudTexts = new Dictionary(); + private static GUILayoutGroup bossHealthContainer; + private static GUIFrame hudFrame; public static GUIFrame HUDFrame { @@ -33,6 +88,13 @@ namespace Barotrauma { CanBeFocused = false }; + bossHealthContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.15f, 0.5f), hudFrame.RectTransform, Anchor.CenterRight) + { + RelativeOffset = new Vector2(0.005f, 0.0f) + }) + { + AbsoluteSpacing = GUI.IntScale(10) + }; } return hudFrame; } @@ -70,7 +132,7 @@ namespace Barotrauma public static void AddToGUIUpdateList(Character character) { - if (GUI.DisableHUD) return; + if (GUI.DisableHUD) { return; } if (!character.IsIncapacitated && character.Stun <= 0.0f && !IsCampaignInterfaceOpen) { @@ -83,7 +145,7 @@ namespace Barotrauma foreach (ItemComponent ic in item.Components) { - if (ic.DrawHudWhenEquipped) ic.AddToGUIUpdateList(); + if (ic.DrawHudWhenEquipped) { ic.AddToGUIUpdateList(); } } } } @@ -99,13 +161,14 @@ namespace Barotrauma public static void Update(float deltaTime, Character character, Camera cam) { + UpdateBossHealthBars(deltaTime); + if (GUI.DisableHUD) { if (character.Inventory != null && !LockInventory(character)) { character.Inventory.UpdateSlotInput(); } - return; } @@ -130,17 +193,6 @@ namespace Barotrauma { character.Inventory.ClearSubInventories(); } - - for (int i = 0; i < character.Inventory.Capacity; i++) - { - var item = character.Inventory.GetItemAt(i); - if (item == null || character.Inventory.SlotTypes[i] == InvSlotType.Any) { continue; } - - foreach (ItemComponent ic in item.Components) - { - if (ic.DrawHudWhenEquipped) ic.UpdateHUD(character, deltaTime, cam); - } - } } if (character.IsHumanoid && character.SelectedCharacter != null && character.SelectedCharacter.Inventory != null) @@ -221,10 +273,10 @@ namespace Barotrauma } } - if (DrawIcon(character.CurrentOrder)) + if (character.GetCurrentOrderWithTopPriority()?.Order is Order currentOrder && DrawIcon(currentOrder)) { - DrawOrderIndicator(spriteBatch, cam, character, character.CurrentOrder, 1.0f); - } + DrawOrderIndicator(spriteBatch, cam, character, currentOrder, 1.0f); + } static bool DrawIcon(Order o) => o != null && @@ -253,7 +305,7 @@ namespace Barotrauma return Math.Min((maxDistance - dist) / maxDistance * 2.0f, 1.0f); } - if (!character.IsIncapacitated && character.Stun <= 0.0f && !IsCampaignInterfaceOpen && (!character.IsKeyDown(InputType.Aim) || character.HeldItems.Any(it => it?.GetComponent() == null))) + if (!character.IsIncapacitated && character.Stun <= 0.0f && !IsCampaignInterfaceOpen && (!character.IsKeyDown(InputType.Aim) || character.HeldItems.None(it => it?.GetComponent() != null))) { if (character.FocusedCharacter != null && character.FocusedCharacter.CanBeSelected) { @@ -499,6 +551,86 @@ namespace Barotrauma } } + public static void ShowBossHealthBar(Character character) + { + if (character == null || character.IsDead || character.Removed) { return; } + + var existingBar = bossHealthBars.Find(b => b.Character == character); + if (existingBar != null) + { + existingBar.FadeTimer = BossHealthBarDuration; + return; + } + + if (bossHealthBars.Count > 5) + { + BossHealthBar oldestHealthBar = bossHealthBars.First(); + foreach (var bar in bossHealthBars) + { + if (bar.TopHealthBar.BarSize < oldestHealthBar.TopHealthBar.BarSize) + { + oldestHealthBar = bar; + } + } + oldestHealthBar.FadeTimer = Math.Min(oldestHealthBar.FadeTimer, 1.0f); + } + + bossHealthBars.Add(new BossHealthBar(character)); + } + + public static void UpdateBossHealthBars(float deltaTime) + { + for (int i = 0; i < bossHealthBars.Count; i++) + { + var bossHealthBar = bossHealthBars[i]; + + bool showTopBar = i == 0; + if (showTopBar != bossHealthBar.TopContainer.Visible) + { + bossHealthContainer.Recalculate(); + } + + bossHealthBar.TopContainer.Visible = showTopBar; + bossHealthBar.SideContainer.Visible = !bossHealthBar.TopContainer.Visible; + + float health = bossHealthBar.Character.Vitality / bossHealthBar.Character.MaxVitality; + + float alpha = Math.Min(bossHealthBar.FadeTimer, 1.0f); + foreach (var c in bossHealthBar.SideContainer.GetAllChildren().Concat(bossHealthBar.TopContainer.GetAllChildren())) + { + c.Color = new Color(c.Color, (byte)(alpha * 255)); + if (c is GUITextBlock textBlock) + { + textBlock.TextColor = new Color(bossHealthBar.Character.IsDead ? Color.Gray : textBlock.TextColor, (byte)(alpha * 255)); + } + } + + bossHealthBar.TopHealthBar.BarSize = bossHealthBar.SideHealthBar.BarSize = health; + + if (bossHealthBar.Character.Removed || !bossHealthBar.Character.Enabled) + { + bossHealthBar.FadeTimer = Math.Min(bossHealthBar.FadeTimer, 1.0f); + } + else if (bossHealthBar.Character.IsDead) + { + bossHealthBar.FadeTimer = Math.Min(bossHealthBar.FadeTimer, 5.0f); + } + bossHealthBar.FadeTimer -= deltaTime; + } + + for (int i = bossHealthBars.Count - 1; i >= 0 ; i--) + { + var bossHealthBar = bossHealthBars[i]; + if (bossHealthBar.FadeTimer <= 0) + { + bossHealthBar.SideContainer.Parent?.RemoveChild(bossHealthBar.SideContainer); + bossHealthBar.TopContainer.Parent?.RemoveChild(bossHealthBar.TopContainer); + bossHealthBars.RemoveAt(i); + bossHealthContainer.Recalculate(); + } + } + } + private static bool LockInventory(Character character) { if (character?.Inventory == null || !character.AllowInput || character.LockHands || IsCampaignInterfaceOpen) { return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index 2427989ad..1d2892d72 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -100,7 +100,22 @@ namespace Barotrauma Color textColor = Color.White * (0.5f + skill.Level / 200.0f); var skillName = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillsArea.RectTransform), TextManager.Get("SkillName." + skill.Identifier), textColor: textColor, font: font) { Padding = Vector4.Zero }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), skillName.RectTransform), ((int)skill.Level).ToString(), textColor: textColor, font: font, textAlignment: Alignment.CenterRight); + + float modifiedSkillLevel = skill.Level; + if (Character != null) + { + modifiedSkillLevel = Character.GetSkillLevel(skill.Identifier); + } + if (!MathUtils.NearlyEqual(MathF.Round(modifiedSkillLevel), MathF.Round(skill.Level))) + { + int skillChange = (int)MathF.Round(modifiedSkillLevel - skill.Level); + string changeText = $"{(skillChange > 0 ? "+" : "") + skillChange}"; + new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), skillName.RectTransform), $"{(int)skill.Level} ({changeText})", textColor: textColor, font: font, textAlignment: Alignment.CenterRight); + } + else + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), skillName.RectTransform), ((int)skill.Level).ToString(), textColor: textColor, font: font, textAlignment: Alignment.CenterRight); + } } } else if (Character != null && Character.IsDead) @@ -529,6 +544,7 @@ namespace Barotrauma { ushort infoID = inc.ReadUInt16(); string newName = inc.ReadString(); + string originalName = inc.ReadString(); int gender = inc.ReadByte(); int race = inc.ReadByte(); int headSpriteID = inc.ReadByte(); @@ -556,7 +572,7 @@ namespace Barotrauma } // TODO: animations - CharacterInfo ch = new CharacterInfo(speciesName, newName, jobPrefab, ragdollFile, variant) + CharacterInfo ch = new CharacterInfo(speciesName, newName, originalName, jobPrefab, ragdollFile, variant) { ID = infoID, }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 630ec6bd2..3f1b518ac 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -292,7 +292,7 @@ namespace Barotrauma break; case ServerNetObject.ENTITY_EVENT: - int eventType = msg.ReadRangedInteger(0, 5); + int eventType = msg.ReadRangedInteger(0, 6); switch (eventType) { case 0: //NetEntityEvent.Type.InventoryState @@ -330,6 +330,8 @@ namespace Barotrauma GameMain.Client.HasSpawned = true; GameMain.Client.Character = this; GameMain.LightManager.LosEnabled = true; + GameMain.LightManager.LosAlpha = 1f; + GameMain.Client.WaitForNextRoundRespawn = null; } else { @@ -390,6 +392,19 @@ namespace Barotrauma byte campaignInteractionType = msg.ReadByte(); (GameMain.GameSession?.GameMode as CampaignMode)?.AssignNPCMenuInteraction(this, (CampaignMode.InteractionType)campaignInteractionType); break; + case 6: //NetEntityEvent.Type.ObjectiveManagerOrderState + bool properData = msg.ReadBoolean(); + if (!properData) { break; } + int orderIndex = msg.ReadRangedInteger(0, Order.PrefabList.Count); + var orderPrefab = Order.PrefabList[orderIndex]; + string option = null; + if (orderPrefab.HasOptions) + { + int optionIndex = msg.ReadRangedInteger(0, orderPrefab.Options.Length); + option = orderPrefab.Options[optionIndex]; + } + GameMain.GameSession.CrewManager.SetHighlightedOrderIcon(this, orderPrefab.Identifier, option); + break; } msg.ReadPadBits(); break; @@ -441,13 +456,15 @@ namespace Barotrauma (GameMain.GameSession.GameMode as CampaignMode)?.AssignNPCMenuInteraction(character, character.CampaignInteractionType); } - // Check if the character has a current order - if (inc.ReadBoolean()) + // Check if the character has current orders + int orderCount = inc.ReadByte(); + for (int i = 0; i < orderCount; i++) { int orderPrefabIndex = inc.ReadByte(); Entity targetEntity = FindEntityByID(inc.ReadUInt16()); Character orderGiver = inc.ReadBoolean() ? FindEntityByID(inc.ReadUInt16()) as Character : null; int orderOptionIndex = inc.ReadByte(); + int orderPriority = inc.ReadByte(); OrderTarget targetPosition = null; if (inc.ReadBoolean()) { @@ -468,7 +485,7 @@ namespace Barotrauma new Order(orderPrefab, targetPosition, orderGiver: orderGiver); character.SetOrder(order, orderOptionIndex >= 0 && orderOptionIndex < orderPrefab.Options.Length ? orderPrefab.Options[orderOptionIndex] : null, - orderGiver, speak: false); + orderPriority, orderGiver, speak: false); } else { @@ -487,7 +504,7 @@ namespace Barotrauma character.ReadStatus(inc); } - if (character.IsHuman && character.TeamID != CharacterTeamType.FriendlyNPC && !character.IsDead) + if (character.IsHuman && character.TeamID != CharacterTeamType.FriendlyNPC && character.TeamID != CharacterTeamType.None && !character.IsDead) { CharacterInfo duplicateCharacterInfo = GameMain.GameSession.CrewManager.GetCharacterInfos().FirstOrDefault(c => c.ID == info.ID); GameMain.GameSession.CrewManager.RemoveCharacterInfo(duplicateCharacterInfo); @@ -501,6 +518,7 @@ namespace Barotrauma if (!character.IsDead) { Controlled = character; } GameMain.LightManager.LosEnabled = true; + GameMain.LightManager.LosAlpha = 1f; character.memInput.Clear(); character.memState.Clear(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index d059cf7f1..8bcb7e503 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -240,6 +240,8 @@ namespace Barotrauma Character.Controlled.SelectedConstruction = null; } } + + HintManager.OnShowHealthInterface(); } } @@ -295,6 +297,7 @@ namespace Barotrauma barSize: 1.0f, color: GUI.Style.HealthBarColorHigh, style: horizontal ? "CharacterHealthBar" : "GUIProgressBarVertical") { HoverCursor = CursorState.Hand, + ToolTip = TextManager.GetWithVariable("hudbutton.healthinterface", "[key]", GameMain.Config.KeyBindText(InputType.Health)), Enabled = true, IsHorizontal = horizontal }; @@ -668,12 +671,17 @@ namespace Barotrauma bloodParticleTimer -= deltaTime * (affliction.Strength / 10.0f); if (bloodParticleTimer <= 0.0f) { + var emitter = Character.BloodEmitters.FirstOrDefault(); + float particleMinScale = emitter != null ? emitter.Prefab.ScaleMin : 0.5f; + float particleMaxScale = emitter != null ? emitter.Prefab.ScaleMax : 1; + float severity = Math.Min(affliction.Strength / affliction.Prefab.MaxStrength * Character.Params.BleedParticleMultiplier, 1); + float bloodParticleSize = MathHelper.Lerp(particleMinScale, particleMaxScale, severity); bool inWater = Character.AnimController.InWater; - float bloodParticleSize = MathHelper.Lerp(0.5f, 1.0f, affliction.Strength / 100.0f); if (!inWater) { bloodParticleSize *= 2.0f; } + var blood = GameMain.ParticleManager.CreateParticle( inWater ? Character.Params.BleedParticleWater : Character.Params.BleedParticleAir, targetLimb.WorldPosition, Rand.Vector(affliction.Strength), 0.0f, Character.AnimController.CurrentHull); @@ -682,7 +690,7 @@ namespace Barotrauma { blood.Size *= bloodParticleSize; } - bloodParticleTimer = 1.0f; + bloodParticleTimer = MathHelper.Lerp(2, 0.5f, severity); } } @@ -713,6 +721,7 @@ namespace Barotrauma int dmgPerSecond = Math.Sign(a2.DamagePerSecond - a1.DamagePerSecond); return dmgPerSecond != 0 ? dmgPerSecond : Math.Sign(a1.Strength - a1.Strength); }); + HintManager.OnAfflictionDisplayed(Character, currentDisplayedAfflictions); updateDisplayedAfflictionsTimer = UpdateDisplayedAfflictionsInterval; } @@ -732,6 +741,7 @@ namespace Barotrauma float distortSpeed = 0.0f; float radialDistortStrength = 0.0f; float chromaticAberrationStrength = 0.0f; + float grainStrength = 0.0f; if (Character.IsUnconscious) { @@ -752,6 +762,7 @@ namespace Barotrauma blurStrength = Math.Max(blurStrength, affliction.GetScreenBlurStrength()); radialDistortStrength = Math.Max(radialDistortStrength, affliction.GetRadialDistortStrength()); chromaticAberrationStrength = Math.Max(chromaticAberrationStrength, affliction.GetChromaticAberrationStrength()); + grainStrength = Math.Max(grainStrength, affliction.GetScreenGrainStrength()); } foreach (LimbHealth limbHealth in limbHealths) { @@ -766,6 +777,7 @@ namespace Barotrauma Character.RadialDistortStrength = radialDistortStrength; Character.ChromaticAberrationStrength = chromaticAberrationStrength; + Character.GrainStrength = grainStrength; if (blurStrength > 0.0f) { distortTimer = (distortTimer + deltaTime * distortSpeed) % MathHelper.TwoPi; @@ -986,8 +998,8 @@ namespace Barotrauma cprButton.Visible = Character == Character.Controlled?.SelectedCharacter - && (Character.IsUnconscious || Character.Stun > 0.0f) && !Character.IsDead + && Character.IsKnockedDown && openHealthWindow == this; cprButton.IgnoreLayoutGroups = !cprButton.Visible; cprButton.Selected = @@ -1183,7 +1195,7 @@ namespace Barotrauma } } - private Color GetAfflictionIconColor(AfflictionPrefab prefab, Affliction affliction) + public static Color GetAfflictionIconColor(AfflictionPrefab prefab, Affliction affliction) { // No specific colors, use generic if (prefab.IconColors == null) @@ -1203,6 +1215,8 @@ namespace Barotrauma } } + public static Color GetAfflictionIconColor(Affliction affliction) => GetAfflictionIconColor(affliction.Prefab, affliction); + private void UpdateAfflictionContainer(LimbHealth selectedLimb) { selectedLimbText.Text = selectedLimb == null ? "" : selectedLimb.Name; @@ -1270,7 +1284,7 @@ namespace Barotrauma var afflictionIcon = new GUIImage(new RectTransform(Vector2.One * 0.8f, button.RectTransform, Anchor.Center), affliction.Prefab.Icon, scaleToFit: true) { - Color = GetAfflictionIconColor(affliction.Prefab, affliction), + Color = GetAfflictionIconColor(affliction), CanBeFocused = false }; afflictionIcon.PressedColor = afflictionIcon.Color; @@ -1906,7 +1920,7 @@ namespace Barotrauma float alpha = MathHelper.Lerp(0.3f, 1.0f, (affliction.Strength - showIconThreshold) / Math.Min(affliction.Prefab.MaxStrength - showIconThreshold, 10.0f)); - affliction.Prefab.Icon.Draw(spriteBatch, iconPos - iconSize / 2.0f, GetAfflictionIconColor(affliction.Prefab, affliction) * alpha, 0, iconScale); + affliction.Prefab.Icon.Draw(spriteBatch, iconPos - iconSize / 2.0f, GetAfflictionIconColor(affliction) * alpha, 0, iconScale); iconPos += new Vector2(10.0f, 20.0f) * iconScale; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index 31345e1ba..1b8a94262 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -299,7 +299,12 @@ namespace Barotrauma { var textContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), listBox.Content.RectTransform), style: "InnerFrame", color: Color.White) { - CanBeFocused = false + CanBeFocused = true, + OnSecondaryClicked = (component, data) => + { + GUIContextMenu.CreateContextMenu(new ContextMenuOption("editor.copytoclipboard", true, () => { Clipboard.SetText(msg.Text); })); + return true; + } }; var textBlock = new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width - 5, 0), textContainer.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(2, 2) }, msg.Text, textAlignment: Alignment.TopLeft, font: GUI.SmallFont, wrap: true) @@ -480,7 +485,7 @@ namespace Barotrauma var subInfo = new SubmarineInfo(string.Join(" ", args)); Submarine.MainSub = Submarine.Load(subInfo, true); } - GameMain.SubEditorScreen.Select(); + GameMain.SubEditorScreen.Select(enableAutoSave: Screen.Selected != GameMain.GameScreen); }, isCheat: true)); commands.Add(new Command("editparticles|particleeditor", "editparticles/particleeditor: Switch to the Particle Editor to edit particle effects.", (string[] args) => @@ -620,6 +625,11 @@ namespace Barotrauma NewMessage(SubEditorScreen.ShouldDrawGrid ? "Enabled submarine grid." : "Disabled submarine grid.", GUI.Style.Green); })); + commands.Add(new Command("spreadsheetexport", "Export items in format recognized by the spreadsheet importer.", (string[] args) => + { + SpreadsheetExport.Export(); + })); + commands.Add(new Command("wikiimage_character", "Save an image of the currently controlled character with a transparent background.", (string[] args) => { if (Character.Controlled == null) { return; } @@ -649,6 +659,7 @@ namespace Barotrauma AssignRelayToServer("bindkey", false); AssignRelayToServer("unbindkey", false); AssignRelayToServer("savebinds", false); + AssignRelayToServer("spreadsheetexport", false); #if DEBUG AssignRelayToServer("crash", false); AssignRelayToServer("showballastflorasprite", false); @@ -1236,6 +1247,7 @@ namespace Barotrauma GameMain.DebugDraw = false; GameMain.LightManager.LightingEnabled = true; GameMain.LightManager.LosEnabled = true; + GameMain.LightManager.LosAlpha = 1f; } NewMessage(HumanAIController.debugai ? "AI debug info visible" : "AI debug info hidden", Color.White); }); @@ -1523,7 +1535,9 @@ namespace Barotrauma List lines = missingTags.Select(t => "\"" + t.Key + "\"\n missing from " + string.Join(", ", t.Value)).ToList(); string filePath = "missingloca.txt"; + Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; File.WriteAllLines(filePath, lines); + Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; ToolBox.OpenFileWithShell(Path.GetFullPath(filePath)); TextManager.Language = "English"; })); @@ -1532,7 +1546,9 @@ namespace Barotrauma { var debugLines = EventSet.GetDebugStatistics(); string filePath = "eventstats.txt"; + Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; File.WriteAllLines(filePath, debugLines); + Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; ToolBox.OpenFileWithShell(Path.GetFullPath(filePath)); })); @@ -1705,6 +1721,15 @@ namespace Barotrauma { GameMain.Client?.ForceTimeOut(); }, isCheat: false)); + commands.Add(new Command("bumpitem", "", (string[] args) => + { + float vel = 10.0f; + if (args.Length > 0) + { + float.TryParse(args[0], NumberStyles.Number, CultureInfo.InvariantCulture, out vel); + } + Character.Controlled?.FocusedItem?.body?.ApplyLinearImpulse(Rand.Vector(vel)); + }, isCheat: false)); #endif @@ -1813,7 +1838,9 @@ namespace Barotrauma lines.Add("" + me.Name + ""); lines.Add("" + me.Description + ""); } + Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; File.WriteAllLines(filePath, lines); + Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; })); commands.Add(new Command("dumpeventtexts", "dumpeventtexts [filepath]: gets the texts from event files and 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/EventTexts.txt", (string[] args) => @@ -1832,16 +1859,15 @@ namespace Barotrauma docs.Add(eventPrefab.ConfigElement.Document); getTextsFromElement(eventPrefab.ConfigElement, lines, eventPrefab.Identifier); } + Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; File.WriteAllLines(filePath, lines); - ToolBox.OpenFileWithShell(Path.GetFullPath(filePath)); System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { Indent = true, NewLineOnAttributes = false - }; - + }; foreach (XDocument doc in docs) { using (var writer = XmlWriter.Create(new System.Uri(doc.BaseUri).LocalPath, settings)) @@ -1850,6 +1876,7 @@ namespace Barotrauma writer.Flush(); } } + Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; void getTextsFromElement(XElement element, List list, string parentName) { @@ -1996,10 +2023,17 @@ namespace Barotrauma lines.Add("[/table]"); lines.Add(""); } + Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; File.WriteAllLines(filePath, lines); + Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; ToolBox.OpenFileWithShell(Path.GetFullPath(filePath)); })); #if DEBUG + commands.Add(new Command("playovervc", "Plays a sound over voice chat.", (args) => + { + VoipCapture.Instance?.SetOverrideSound(args.Length > 0 ? args[0] : null); + })); + commands.Add(new Command("querylobbies", "Queries all SteamP2P lobbies", (args) => { TaskPool.Add("DebugQueryLobbies", @@ -2033,7 +2067,9 @@ namespace Barotrauma commands.Add(new Command("printproperties", "Goes through the currently collected property list for missing localizations and writes them to a file.", (string[] args) => { string path = Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + "\\propertylocalization.txt"; + Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; File.WriteAllLines(path, SerializableEntityEditor.MissingLocalizations); + Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; })); commands.Add(new Command("getproperties", "Goes through the MapEntity prefabs and checks their serializable properties for localization issues.", (string[] args) => @@ -2248,12 +2284,15 @@ namespace Barotrauma "revokeperm", (string[] args) => { - if (args.Length < 1) return; + if (args.Length < 1) { return; } - NewMessage("Valid permissions are:", Color.White); - foreach (ClientPermissions permission in Enum.GetValues(typeof(ClientPermissions))) + if (args.Length < 2) { - NewMessage(" - " + permission.ToString(), Color.White); + NewMessage("Valid permissions are:", Color.White); + foreach (ClientPermissions permission in Enum.GetValues(typeof(ClientPermissions))) + { + NewMessage(" - " + permission.ToString(), Color.White); + } } ShowQuestionPrompt("Permission to revoke from client " + args[0] + "?", (perm) => diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs index 31c5b8913..2d569af49 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs @@ -560,6 +560,22 @@ namespace Barotrauma }; } break; + case NetworkEventType.UNLOCKPATH: + UInt16 connectionIndex = msg.ReadUInt16(); + if (GameMain.GameSession?.Map?.Connections != null) + { + if (connectionIndex >= GameMain.GameSession.Map.Connections.Count) + { + DebugConsole.ThrowError($"Failed to unlock a path on the campaign map. Connection index out of bounds (index: {connectionIndex}, number of connections: {GameMain.GameSession.Map.Connections.Count})"); + } + else + { + GameMain.GameSession.Map.Connections[connectionIndex].Locked = false; + new GUIMessageBox(string.Empty, TextManager.Get("pathunlockedgeneric"), + new string[0], type: GUIMessageBox.Type.InGame, iconStyle: "UnlockPathIcon", relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128)); + } + } + break; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs new file mode 100644 index 000000000..d4d0afdd6 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs @@ -0,0 +1,64 @@ +using Barotrauma.Networking; + +namespace Barotrauma +{ + partial class AbandonedOutpostMission : Mission + { + public override int State + { + get { return base.State; } + protected set + { + if (state != value) + { + base.State = value; + if (state == HostagesKilledState && !string.IsNullOrEmpty(hostagesKilledMessage)) + { + CreateMessageBox(string.Empty, hostagesKilledMessage); + } + } + } + } + + public override void ClientReadInitial(IReadMessage msg) + { + byte characterCount = msg.ReadByte(); + + for (int i = 0; i < characterCount; i++) + { + Character character = Character.ReadSpawnData(msg); + characters.Add(character); + if (msg.ReadBoolean()) { requireKill.Add(character); } + if (msg.ReadBoolean()) + { + requireRescue.Add(character); +#if CLIENT + GameMain.GameSession.CrewManager.AddCharacterToCrewList(character); +#endif + } + ushort itemCount = msg.ReadUInt16(); + for (int j = 0; j < itemCount; j++) + { + Item.ReadSpawnData(msg); + } + if (character.Submarine != null && character.AIController is EnemyAIController enemyAi) + { + enemyAi.UnattackableSubmarines.Add(character.Submarine); + enemyAi.UnattackableSubmarines.Add(Submarine.MainSub); + foreach (Submarine sub in Submarine.MainSub.DockedTo) + { + enemyAi.UnattackableSubmarines.Add(sub); + } + } + } + if (characters.Contains(null)) + { + throw new System.Exception("Error in AbandonedOutpostMission.ClientReadInitial: character list contains null (mission: " + Prefab.Identifier + ")"); + } + if (characters.Count != characterCount) + { + throw new System.Exception("Error in AbandonedOutpostMission.ClientReadInitial: character count does not match the server count (" + characters + " != " + characters.Count + "mission: " + Prefab.Identifier + ")"); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs index ccdcb04da..56e2e95f8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs @@ -1,10 +1,66 @@ using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; +using System.Globalization; namespace Barotrauma { abstract partial class Mission { + private readonly List shownMessages = new List(); + public IEnumerable ShownMessages + { + get { return shownMessages; } + } + + public Color GetDifficultyColor() + { + int v = Difficulty ?? MissionPrefab.MinDifficulty; + float t = MathUtils.InverseLerp(MissionPrefab.MinDifficulty, MissionPrefab.MaxDifficulty, v); + return ToolBox.GradientLerp(t, GUI.Style.Green, GUI.Style.Orange, GUI.Style.Red); + } + + public string GetMissionRewardText() + { + string rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", Reward)); + return TextManager.GetWithVariable("missionreward", "[reward]", $"‖color:gui.orange‖{rewardText}‖end‖"); + } + + public string GetReputationRewardText(Location currLocation) + { + List reputationRewardTexts = new List(); + foreach (var reputationReward in ReputationRewards) + { + string name = ""; + + if (reputationReward.Key.Equals("location", StringComparison.OrdinalIgnoreCase)) + { + name = $"‖color:gui.orange‖{currLocation.Name}‖end‖"; + } + else + { + var faction = FactionPrefab.Prefabs.Find(f => f.Identifier.Equals(reputationReward.Key, StringComparison.OrdinalIgnoreCase)); + if (faction != null) + { + name = $"‖color:{XMLExtensions.ColorToString(faction.IconColor)}‖{faction.Name}‖end‖"; + } + else + { + name = TextManager.Get(reputationReward.Key); + } + } + float normalizedValue = MathUtils.InverseLerp(-100.0f, 100.0f, reputationReward.Value); + string formattedValue = ((int)reputationReward.Value).ToString("+#;-#;0"); //force plus sign for positive numbers + string rewardText = TextManager.GetWithVariables( + "reputationformat", + new string[] { "[reputationname]", "[reputationvalue]" }, + new string[] { name, $"‖color:{XMLExtensions.ColorToString(Reputation.GetReputationColor(normalizedValue))}‖{formattedValue}‖end‖" }); + reputationRewardTexts.Add(rewardText); + } + return TextManager.AddPunctuation(':', TextManager.Get("reputation"), string.Join(", ", reputationRewardTexts)); + } + partial void ShowMessageProjSpecific(int missionState) { int messageIndex = missionState - 1; @@ -23,11 +79,17 @@ namespace Barotrauma { yield return new WaitForSeconds(1.0f); } - new GUIMessageBox(header, message, buttons: new string[0], type: GUIMessageBox.Type.InGame, icon: Prefab.Icon) + CreateMessageBox(header, message); + yield return CoroutineStatus.Success; + } + + protected void CreateMessageBox(string header, string message) + { + shownMessages.Add(message); + new GUIMessageBox(header, message, buttons: new string[0], type: GUIMessageBox.Type.InGame, icon: Prefab.Icon, parseRichText: true) { IconColor = Prefab.IconColor }; - yield return CoroutineStatus.Success; } public void ClientRead(IReadMessage msg) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionMode.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionMode.cs index 24e8e3db5..342b61004 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionMode.cs @@ -1,19 +1,17 @@ -using Microsoft.Xna.Framework; -using System; - -namespace Barotrauma +namespace Barotrauma { abstract partial class MissionMode : GameMode { public override void ShowStartMessage() { - if (mission == null) return; - - new GUIMessageBox(mission.Name, mission.Description, new string[0], type: GUIMessageBox.Type.InGame, icon: mission.Prefab.Icon) + foreach (Mission mission in missions) { - IconColor = mission.Prefab.IconColor, - UserData = "missionstartmessage" - }; + new GUIMessageBox(mission.Name, mission.Description, new string[0], type: GUIMessageBox.Type.InGame, icon: mission.Prefab.Icon, parseRichText: true) + { + IconColor = mission.Prefab.IconColor, + UserData = "missionstartmessage" + }; + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs index 878c814b7..5bc64d50f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs @@ -1,7 +1,5 @@ using Microsoft.Xna.Framework; using System; -using System.Collections.Generic; -using System.Text; using System.Xml.Linq; namespace Barotrauma diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/OutpostDestroyMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/OutpostDestroyMission.cs new file mode 100644 index 000000000..e12fc3691 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/OutpostDestroyMission.cs @@ -0,0 +1,18 @@ +using Barotrauma.Networking; + +namespace Barotrauma +{ + partial class OutpostDestroyMission : AbandonedOutpostMission + { + public override void ClientReadInitial(IReadMessage msg) + { + base.ClientReadInitial(msg); + ushort itemCount = msg.ReadUInt16(); + for (int i = 0; i < itemCount; i++) + { + var item = Item.ReadSpawnData(msg); + items.Add(item); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs b/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs index f6dad566f..2c6ab92ac 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs @@ -422,12 +422,12 @@ namespace Barotrauma } } - public void DrawStringWithColors(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects se, float layerDepth, List richTextData) + public void DrawStringWithColors(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects se, float layerDepth, List richTextData, int rtdOffset = 0) { - DrawStringWithColors(sb, text, position, color, rotation, origin, new Vector2(scale), se, layerDepth, richTextData); + DrawStringWithColors(sb, text, position, color, rotation, origin, new Vector2(scale), se, layerDepth, richTextData, rtdOffset); } - public void DrawStringWithColors(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth, List richTextData) + public void DrawStringWithColors(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth, List richTextData, int rtdOffset = 0) { if (textures.Count == 0 && !DynamicLoading) { return; } @@ -457,15 +457,15 @@ namespace Barotrauma Color currentTextColor; - if (currentRichTextData != null && i > currentRichTextData.EndIndex + lineNum) + while (currentRichTextData != null && i + rtdOffset > currentRichTextData.EndIndex + lineNum) { richTextDataIndex++; currentRichTextData = richTextDataIndex < richTextData.Count ? richTextData[richTextDataIndex] : null; } - if (currentRichTextData != null && currentRichTextData.StartIndex + lineNum <= i && i <= currentRichTextData.EndIndex + lineNum) + if (currentRichTextData != null && currentRichTextData.StartIndex + lineNum <= i + rtdOffset && i + rtdOffset <= currentRichTextData.EndIndex + lineNum) { - currentTextColor = currentRichTextData.Color ?? color; + currentTextColor = currentRichTextData.Color * currentRichTextData.Alpha ?? color; if (!string.IsNullOrEmpty(currentRichTextData.Metadata)) { currentTextColor = Color.Lerp(currentTextColor, Color.White, 0.5f); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs index 41ac0aeba..a9b3e4e50 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs @@ -321,6 +321,7 @@ namespace Barotrauma float prevSize = chatBox.BarSize; string displayedText = message.TranslatedText; + string senderName = ""; Color senderColor = Color.White; if (!string.IsNullOrWhiteSpace(message.SenderName)) @@ -377,13 +378,29 @@ namespace Barotrauma } var msgText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), msgHolder.RectTransform) - { AbsoluteOffset = new Point((int)(10 * GUI.Scale), senderNameTimestamp == null ? 0 : senderNameTimestamp.Rect.Height) }, + { AbsoluteOffset = new Point((int)(10 * GUI.Scale), senderNameTimestamp == null ? 0 : senderNameTimestamp.Rect.Height) }, displayedText, textColor: message.Color, font: GUI.SmallFont, textAlignment: Alignment.TopLeft, style: null, wrap: true, - color: ((chatBox.Content.CountChildren % 2) == 0) ? Color.Transparent : Color.Black * 0.1f) + color: ((chatBox.Content.CountChildren % 2) == 0) ? Color.Transparent : Color.Black * 0.1f, parseRichText: true) { UserData = message.SenderName, - CanBeFocused = true + CanBeFocused = false }; + msgText.CalculateHeightFromText(); + if (msgText.RichTextData != null) + { + foreach (var data in msgText.RichTextData) + { + var clickableArea = new GUITextBlock.ClickableArea() + { + Data = data + }; + if (GameMain.NetLobbyScreen != null && GameMain.NetworkMember != null) + { + clickableArea.OnClick = GameMain.NetLobbyScreen.SelectPlayer; + } + msgText.ClickableAreas.Add(clickableArea); + } + } if (message is OrderChatMessage orderChatMsg && Character.Controlled != null && @@ -444,7 +461,7 @@ namespace Barotrauma senderText.RectTransform.MinSize = new Point(0, senderText.Rect.Height); } var msgPopupText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), - displayedText, textColor: message.Color, font: GUI.SmallFont, textAlignment: Alignment.BottomLeft, style: null, wrap: true) + displayedText, textColor: message.Color, font: GUI.SmallFont, textAlignment: Alignment.BottomLeft, style: null, wrap: true, parseRichText: true) { CanBeFocused = false }; @@ -523,6 +540,7 @@ namespace Barotrauma if (ToggleButton != null) { + ToggleButton.Selected = ToggleOpen; ToggleButton.RectTransform.AbsoluteOffset = new Point(GUIFrame.Rect.Right, GUIFrame.Rect.Y + HUDLayoutSettings.ChatBoxArea.Height - ToggleButton.Rect.Height); } @@ -566,6 +584,7 @@ namespace Barotrauma if (ToggleOpen) { + GUIFrame.CanBeFocused = true; openState += deltaTime * 5.0f; //delete all popup messages when the chatbox is open foreach (var popupMsg in popupMessages) @@ -576,6 +595,7 @@ namespace Barotrauma } else { + GUIFrame.CanBeFocused = false; openState -= deltaTime * 5.0f; int yOffset = 0; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs index b84ba031d..9e975d413 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs @@ -1,10 +1,10 @@ using Barotrauma.Extensions; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Globalization; using System.Linq; -using Barotrauma.Networking; namespace Barotrauma { @@ -91,9 +91,10 @@ namespace Barotrauma UserData = "container" }; + int panelMaxWidth = (int)(GUI.xScale * (GUI.HorizontalAspectRatio < 1.4f ? 650 : 560)); var availableMainGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).RectTransform) { - MaxSize = new Point(560, campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Rect.Height) + MaxSize = new Point(panelMaxWidth, campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Rect.Height) }) { Stretch = true, @@ -149,7 +150,7 @@ namespace Barotrauma var pendingAndCrewMainGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).RectTransform, anchor: Anchor.TopRight) { - MaxSize = new Point(560, campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Rect.Height) + MaxSize = new Point(panelMaxWidth, campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Rect.Height) }) { Stretch = true, @@ -177,7 +178,7 @@ namespace Barotrauma var pendingAndCrewGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), anchor: Anchor.Center, parent: new GUIFrame(new RectTransform(new Vector2(1.0f, 13.25f / 14.0f), pendingAndCrewMainGroup.RectTransform) { - MaxSize = new Point(560, campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Rect.Height) + MaxSize = new Point(panelMaxWidth, campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Rect.Height) }).RectTransform)); float height = 0.05f; @@ -188,7 +189,7 @@ namespace Barotrauma }; new GUITextBlock(new RectTransform(new Vector2(1.0f, height), pendingAndCrewGroup.RectTransform), TextManager.Get("campaignmenucrew"), font: GUI.SubHeadingFont); - crewList = new GUIListBox(new RectTransform(new Vector2(1.0f, (8)* height), pendingAndCrewGroup.RectTransform)) + crewList = new GUIListBox(new RectTransform(new Vector2(1.0f, 8 * height), pendingAndCrewGroup.RectTransform)) { Spacing = 1 }; @@ -207,7 +208,7 @@ namespace Barotrauma { ClickSound = GUISoundType.HireRepairClick, ForceUpperCase = true, - OnClicked = (b, o) => ValidatePendingHires(true) + OnClicked = (b, o) => ValidateHires(PendingHires, true) }; clearAllButton = new GUIButton(new RectTransform(new Vector2(1.0f / 3.0f, 1.0f), group.RectTransform), text: TextManager.Get("campaignstore.clearall")) { @@ -335,9 +336,9 @@ namespace Barotrauma jobColor = characterInfo.Job.Prefab.UIColor; } - GUIFrame frame = new GUIFrame(new RectTransform(new Point(listBox.Content.Rect.Width, 55), parent: listBox.Content.RectTransform), "ListBoxElement") + GUIFrame frame = new GUIFrame(new RectTransform(new Point(listBox.Content.Rect.Width, (int)(GUI.yScale * 55)), parent: listBox.Content.RectTransform), "ListBoxElement") { - UserData = new Tuple(characterInfo, skill != null ? skill.Level : 0.0f) + UserData = new Tuple(characterInfo, skill?.Level ?? 0.0f) }; GUILayoutGroup mainGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), frame.RectTransform, anchor: Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft) { @@ -345,19 +346,22 @@ namespace Barotrauma }; float portraitWidth = (0.8f * mainGroup.Rect.Height) / mainGroup.Rect.Width; - new GUICustomComponent(new RectTransform(new Vector2(portraitWidth, 0.8f), mainGroup.RectTransform), + var icon = new GUICustomComponent(new RectTransform(new Vector2(portraitWidth, 0.8f), mainGroup.RectTransform), onDraw: (sb, component) => characterInfo.DrawIcon(sb, component.Rect.Center.ToVector2(), targetAreaSize: component.Rect.Size.ToVector2())) { CanBeFocused = false }; - GUILayoutGroup nameAndJobGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.4f - portraitWidth, 0.8f), mainGroup.RectTransform)); - GUITextBlock nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), nameAndJobGroup.RectTransform), - characterInfo.Name, textColor: jobColor, textAlignment: Alignment.BottomLeft) + GUILayoutGroup nameAndJobGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.4f - portraitWidth, 0.8f), mainGroup.RectTransform)) { CanBeFocused = false }; + GUILayoutGroup nameGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), nameAndJobGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { CanBeFocused = false }; + GUITextBlock nameBlock = new GUITextBlock(new RectTransform(Vector2.One, nameGroup.RectTransform), + listBox == hireableList ? characterInfo.OriginalName : characterInfo.Name, + textColor: jobColor, textAlignment: Alignment.BottomLeft) { CanBeFocused = false }; nameBlock.Text = ToolBox.LimitString(nameBlock.Text, nameBlock.Font, nameBlock.Rect.Width); + GUITextBlock jobBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), nameAndJobGroup.RectTransform), characterInfo.Job.Name, textColor: Color.White, font: GUI.SmallFont, textAlignment: Alignment.TopLeft) { @@ -366,7 +370,7 @@ namespace Barotrauma jobBlock.Text = ToolBox.LimitString(jobBlock.Text, jobBlock.Font, jobBlock.Rect.Width); float width = 0.6f / 3; - if (characterInfo.Job != null) + if (characterInfo.Job != null && skill != null) { GUILayoutGroup skillGroup = new GUILayoutGroup(new RectTransform(new Vector2(width, 0.6f), mainGroup.RectTransform), isHorizontal: true); float iconWidth = (float)skillGroup.Rect.Height / skillGroup.Rect.Width; @@ -383,11 +387,18 @@ namespace Barotrauma if (listBox != crewList) { - new GUITextBlock(new RectTransform(new Vector2(width, 1.0f), mainGroup.RectTransform), FormatCurrency(characterInfo.Salary), textAlignment: Alignment.Center) + new GUITextBlock(new RectTransform(new Vector2(width, 1.0f), mainGroup.RectTransform), + FormatCurrency(characterInfo.Salary), + textAlignment: Alignment.Center) { CanBeFocused = false }; } + else + { + // Just a bit of padding to make list layouts similar + new GUIFrame(new RectTransform(new Vector2(width, 1.0f), mainGroup.RectTransform), style: null) { CanBeFocused = false }; + } if (listBox == hireableList) { @@ -446,6 +457,23 @@ namespace Barotrauma } }; } + + if (listBox == pendingList || listBox == crewList) + { + nameBlock.RectTransform.Resize(new Point(nameBlock.Rect.Width - nameBlock.Rect.Height, nameBlock.Rect.Height)); + nameBlock.Text = ToolBox.LimitString(nameBlock.Text, nameBlock.Font, nameBlock.Rect.Width); + nameBlock.RectTransform.Resize(new Point((int)(nameBlock.Padding.X + nameBlock.TextSize.X + nameBlock.Padding.Z), nameBlock.Rect.Height)); + Point size = new Point((int)(0.7f * nameBlock.Rect.Height)); + new GUIImage(new RectTransform(size, nameGroup.RectTransform), "EditIcon") { CanBeFocused = false }; + size = new Point(3 * mainGroup.AbsoluteSpacing + icon.Rect.Width + nameAndJobGroup.Rect.Width, mainGroup.Rect.Height); + new GUIButton(new RectTransform(size, frame.RectTransform) { RelativeOffset = new Vector2(0.025f) }, style: null) + { + Enabled = HasPermission, + ToolTip = TextManager.GetWithVariable("campaigncrew.givenicknametooltip", "[mouseprimary]", TextManager.Get($"input.{(PlayerInput.MouseButtonsSwapped() ? "rightmouse" : "leftmouse")}")), + UserData = characterInfo, + OnClicked = CreateRenamingComponent + }; + } } private void CreateCharacterPreviewFrame(GUIListBox listBox, GUIFrame characterFrame, CharacterInfo characterInfo) @@ -478,7 +506,10 @@ namespace Barotrauma GUILayoutGroup infoValueGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.6f, 1.0f), infoGroup.RectTransform)) { Stretch = true }; float blockHeight = 1.0f / 4; new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoLabelGroup.RectTransform), TextManager.Get("name")); - new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoValueGroup.RectTransform), characterInfo.Name); + GUITextBlock nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoValueGroup.RectTransform), ""); + string name = listBox == hireableList ? characterInfo.OriginalName : characterInfo.Name; + nameBlock.Text = ToolBox.LimitString(name, nameBlock.Font, nameBlock.Rect.Width); + if (characterInfo.HasGenders) { new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoLabelGroup.RectTransform), TextManager.Get("gender")); @@ -553,9 +584,18 @@ namespace Barotrauma if (PendingHires.Contains(characterInfo)) { PendingHires.Remove(characterInfo); } pendingList.Content.RemoveChild(pendingList.Content.FindChild(c => (c.UserData as Tuple).Item1 == characterInfo)); pendingList.UpdateScrollBarSize(); - CreateCharacterFrame(characterInfo, hireableList); - SortCharacters(hireableList, (SortingMethod)sortingDropDown.SelectedItemData); - hireableList.UpdateScrollBarSize(); + + // Server will reset the names to originals in multiplayer + if (!GameMain.IsMultiplayer) { characterInfo?.ResetName(); } + + if (campaign.Map.CurrentLocation.HireManager.AvailableCharacters.Any(info => info.GetIdentifierUsingOriginalName() == characterInfo.GetIdentifierUsingOriginalName()) && + hireableList.Content.Children.None(c => c.UserData is Tuple userData && userData.Item1.GetIdentifierUsingOriginalName() == characterInfo.GetIdentifierUsingOriginalName())) + { + CreateCharacterFrame(characterInfo, hireableList); + SortCharacters(hireableList, (SortingMethod)sortingDropDown.SelectedItemData); + hireableList.UpdateScrollBarSize(); + } + if (setTotalHireCost) { SetTotalHireCost(); } if (createNetworkMessage) { SendCrewState(true); } return true; @@ -572,36 +612,41 @@ namespace Barotrauma { if (pendingList == null || totalBlock == null || validateHiresButton == null) { return; } int total = 0; - pendingList.Content.Children.ForEach(c => total += (c.UserData as Tuple).Item1.Salary); + pendingList.Content.Children.ForEach(c => + { + total += (c.UserData as Tuple).Item1.Salary; + }); totalBlock.Text = FormatCurrency(total); bool enoughMoney = campaign != null ? total <= campaign.Money : true; totalBlock.TextColor = enoughMoney ? Color.White : Color.Red; validateHiresButton.Enabled = enoughMoney && pendingList.Content.RectTransform.Children.Any(); } - public bool ValidatePendingHires(bool createNetworkEvent = false) + public bool ValidateHires(List hires, bool createNetworkEvent = false) { - List hires = new List(); - int total = 0; - foreach (GUIComponent c in pendingList.Content.Children.ToList()) - { - if (c.UserData is Tuple info) - { - hires.Add(info.Item1); - total += info.Item1.Salary; - } - } + if (hires == null || hires.None()) { return false; } - if (hires.None() || total > campaign.Money) { return false; } + List nonDuplicateHires = new List(); + hires.ForEach(hireInfo => + { + if(campaign.CrewManager.GetCharacterInfos().None(crewInfo => crewInfo.IsNewHire && crewInfo.GetIdentifierUsingOriginalName() == hireInfo.GetIdentifierUsingOriginalName())) + { + nonDuplicateHires.Add(hireInfo); + } + }); + + if (nonDuplicateHires.None()) { return false; } + + int total = nonDuplicateHires.Aggregate(0, (total, info) => total + info.Salary); + + if (total > campaign.Money) { return false; } bool atLeastOneHired = false; - foreach (CharacterInfo ci in hires) + foreach (CharacterInfo ci in nonDuplicateHires) { if (campaign.TryHireCharacter(campaign.Map.CurrentLocation, ci)) { atLeastOneHired = true; - PendingHires.Remove(ci); - pendingList.Content.RemoveChild(pendingList.Content.FindChild(c => (c.UserData as Tuple).Item1 == ci)); } else { @@ -628,6 +673,93 @@ namespace Barotrauma return false; } + private bool CreateRenamingComponent(GUIButton button, object userData) + { + if (!HasPermission || !(userData is CharacterInfo characterInfo)) { return false; } + var outerGlowFrame = new GUIFrame(new RectTransform(new Vector2(1.25f, 1.25f), parentComponent.RectTransform, Anchor.Center), + style: "OuterGlow", color: Color.Black * 0.7f); + var frame = new GUIFrame(new RectTransform(new Vector2(0.33f, 0.4f), outerGlowFrame.RectTransform, anchor: Anchor.Center) + { + MaxSize = new Point(400, 300).Multiply(GUI.Scale) + }); + var layoutGroup = new GUILayoutGroup(new RectTransform((frame.Rect.Size - GUIStyle.ItemFrameMargin).Multiply(new Vector2(0.75f, 1.0f)), frame.RectTransform, anchor: Anchor.Center), childAnchor: Anchor.TopCenter) + { + RelativeSpacing = 0.02f, + Stretch = true + }; + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), layoutGroup.RectTransform), TextManager.Get("campaigncrew.givenickname"), font: GUI.SubHeadingFont, textAlignment: Alignment.Center, wrap: true); + var groupElementSize = new Vector2(1.0f, 0.25f); + var nameBox = new GUITextBox(new RectTransform(groupElementSize, layoutGroup.RectTransform)) + { + MaxTextLength = Client.MaxNameLength + }; + new GUIButton(new RectTransform(groupElementSize, layoutGroup.RectTransform), text: TextManager.Get("confirm")) + { + OnClicked = (button, userData) => + { + if (RenameCharacter(characterInfo, nameBox.Text?.Trim())) + { + parentComponent.RemoveChild(outerGlowFrame); + return true; + } + else + { + nameBox.Flash(color: Color.Red); + return false; + } + + } + }; + new GUIButton(new RectTransform(groupElementSize, layoutGroup.RectTransform), text: TextManager.Get("cancel")) + { + OnClicked = (button, userData) => + { + parentComponent.RemoveChild(outerGlowFrame); + return true; + } + }; + layoutGroup.Recalculate(); + return true; + } + + public bool RenameCharacter(CharacterInfo characterInfo, string newName) + { + if (characterInfo == null || string.IsNullOrEmpty(newName)) { return false; } + if (newName == characterInfo.Name) { return false; } + if (GameMain.IsMultiplayer) + { + SendCrewState(false, renameCharacter: (characterInfo, newName)); + } + else + { + var crewComponent = crewList.Content.FindChild(c => (c.UserData as Tuple).Item1 == characterInfo); + if (crewComponent != null) + { + crewList.Content.RemoveChild(crewComponent); + campaign.CrewManager.RenameCharacter(characterInfo, newName); + CreateCharacterFrame(characterInfo, crewList); + SortCharacters(crewList, SortingMethod.JobAsc); + } + else + { + var pendingComponent = pendingList.Content.FindChild(c => (c.UserData as Tuple).Item1 == characterInfo); + if (pendingComponent != null) + { + pendingList.Content.RemoveChild(pendingComponent); + campaign.Map.CurrentLocation.HireManager.RenameCharacter(characterInfo, newName); + CreateCharacterFrame(characterInfo, pendingList); + SortCharacters(pendingList, SortingMethod.JobAsc); + SetTotalHireCost(); + } + else + { + return false; + } + } + } + return true; + } + private bool FireCharacter(GUIButton button, object selection) { if (!(selection is CharacterInfo characterInfo)) { return false; } @@ -648,9 +780,10 @@ namespace Barotrauma UpdateLocationView(campaign.Map.CurrentLocation, false); } - if ((GUI.MouseOn?.UserData as Tuple)?.Item1 is CharacterInfo characterInfo) + (GUIComponent highlightedFrame, CharacterInfo highlightedInfo) = FindHighlightedCharacter(GUI.MouseOn); + if (highlightedFrame != null && highlightedInfo != null) { - if (characterPreviewFrame == null || characterInfo != characterPreviewFrame.UserData) + if (characterPreviewFrame == null || highlightedInfo != characterPreviewFrame.UserData) { GUIComponent component = GUI.MouseOn; GUIListBox listBox = null; @@ -673,7 +806,7 @@ namespace Barotrauma if (listBox != null) { - SelectCharacter(listBox, GUI.MouseOn as GUIFrame, characterInfo); + SelectCharacter(listBox, highlightedFrame as GUIFrame, highlightedInfo); } } else @@ -687,6 +820,27 @@ namespace Barotrauma characterPreviewFrame.Parent?.RemoveChild(characterPreviewFrame); characterPreviewFrame = null; } + + static (GUIComponent, CharacterInfo) FindHighlightedCharacter(GUIComponent c) + { + if (c == null) + { + return default; + } + if (c.UserData is Tuple highlightedData) + { + return (c, highlightedData.Item1); + } + if (c.Parent != null) + { + if (c.Parent is GUIListBox) + { + return default; + } + return FindHighlightedCharacter(c.Parent); + } + return default; + } } public void SetPendingHires(List characterInfos, Location location) @@ -699,7 +853,7 @@ namespace Barotrauma PendingHires.Clear(); foreach (int identifier in characterInfos) { - CharacterInfo match = location.HireManager.AvailableCharacters.Find(info => info.GetIdentifier() == identifier); + CharacterInfo match = location.HireManager.AvailableCharacters.Find(info => info.GetIdentifierUsingOriginalName() == identifier); if (match != null) { PendingHires.Add(match); @@ -716,9 +870,10 @@ namespace Barotrauma /// Notify the server of crew changes /// /// When set to true will tell the server to update the pending hires + /// When not null tell the server to rename this character. Item1 is the character to rename, Item2 is the new name, Item3 indicates whether the renamed character is already a part of the crew. /// When not null tell the server to fire this character /// When set to true will tell the server to validate pending hires - public void SendCrewState(bool updatePending, CharacterInfo firedCharacter = null, bool validateHires = false) + public void SendCrewState(bool updatePending, (CharacterInfo info, string newName) renameCharacter = default, CharacterInfo firedCharacter = null, bool validateHires = false) { if (campaign is MultiPlayerCampaign) { @@ -731,12 +886,23 @@ namespace Barotrauma msg.Write((ushort)PendingHires.Count); foreach (CharacterInfo pendingHire in PendingHires) { - msg.Write(pendingHire.GetIdentifier()); + msg.Write(pendingHire.GetIdentifierUsingOriginalName()); } } msg.Write(validateHires); + bool validRenaming = renameCharacter.info != null && !string.IsNullOrEmpty(renameCharacter.newName); + msg.Write(validRenaming); + if (validRenaming) + { + int identifier = renameCharacter.info.GetIdentifierUsingOriginalName(); + msg.Write(identifier); + msg.Write(renameCharacter.newName); + bool existingCrewMember = campaign.CrewManager?.GetCharacterInfos().Any(ci => ci.GetIdentifierUsingOriginalName() == identifier) ?? false; + msg.Write(existingCrewMember); + } + msg.Write(firedCharacter != null); if (firedCharacter != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index e9d244dbf..bba4952ca 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -218,6 +218,21 @@ namespace Barotrauma public static bool DisableHUD, DisableUpperHUD, DisableItemHighlights, DisableCharacterNames; + private static bool isSavingIndicatorEnabled; + private static Color savingIndicatorColor = Color.Transparent; + private static bool IsSavingIndicatorVisible => savingIndicatorColor.A > 0; + private static float savingIndicatorSpriteIndex; + private static float savingIndicatorColorLerpAmount; + private static SavingIndicatorState savingIndicatorState = SavingIndicatorState.None; + private static float? timeUntilSavingIndicatorDisabled; + + private enum SavingIndicatorState + { + None, + FadingIn, + FadingOut + } + public static void Init(GameWindow window, IEnumerable selectedContentPackages, GraphicsDevice graphicsDevice) { GraphicsDevice = graphicsDevice; @@ -345,7 +360,11 @@ namespace Barotrauma } #endif - if (DisableHUD) { return; } + if (DisableHUD) + { + DrawSavingIndicator(spriteBatch); + return; + } if (GameMain.ShowFPS || GameMain.DebugDraw) { @@ -539,60 +558,42 @@ namespace Barotrauma } } + IEnumerable strings; if (MouseOn != null) { RectTransform mouseOnRect = MouseOn.RectTransform; bool isAbsoluteOffsetInUse = mouseOnRect.AbsoluteOffset != Point.Zero || mouseOnRect.RelativeOffset == Vector2.Zero; - string selectedString = $"Selected UI Element: {MouseOn.GetType().Name} ({ MouseOn.Style?.Element.Name.LocalName ?? "no style" }, {MouseOn.Rect}"; - string offsetString = $"Relative Offset: {mouseOnRect.RelativeOffset} | Absolute Offset: {(isAbsoluteOffsetInUse ? mouseOnRect.AbsoluteOffset : mouseOnRect.ParentRect.MultiplySize(mouseOnRect.RelativeOffset))}{(isAbsoluteOffsetInUse ? "" : " (Calculated from RelativeOffset)")}"; - string anchorPivotString = $"Anchor: {mouseOnRect.Anchor} | Pivot: {mouseOnRect.Pivot}"; - Vector2 selectedStringSize = SmallFont.MeasureString(selectedString); - Vector2 offsetStringSize = SmallFont.MeasureString(offsetString); - Vector2 anchorPivotStringSize = SmallFont.MeasureString(anchorPivotString); - - int padding = IntScale(10); - int yPos = padding; - - DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)selectedStringSize.X - padding, yPos), selectedString, Color.LightGreen, Color.Black, 0, SmallFont); - yPos += (int)selectedStringSize.Y + padding / 2; - - DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)offsetStringSize.X - padding, yPos), offsetString, Color.LightGreen, Color.Black, 0, SmallFont); - yPos += (int)offsetStringSize.Y + padding / 2; - - DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)anchorPivotStringSize.X - padding, yPos), anchorPivotString, Color.LightGreen, Color.Black, 0, SmallFont); - yPos += (int)anchorPivotStringSize.Y + padding / 2; + strings = new string[] + { + $"Selected UI Element: {MouseOn.GetType().Name} ({ MouseOn.Style?.Element.Name.LocalName ?? "no style" }, {MouseOn.Rect}", + $"Relative Offset: {mouseOnRect.RelativeOffset} | Absolute Offset: {(isAbsoluteOffsetInUse ? mouseOnRect.AbsoluteOffset : mouseOnRect.ParentRect.MultiplySize(mouseOnRect.RelativeOffset))}{(isAbsoluteOffsetInUse ? "" : " (Calculated from RelativeOffset)")}", + $"Anchor: {mouseOnRect.Anchor} | Pivot: {mouseOnRect.Pivot}" + }; } else { - string guiScaleString = $"GUI.Scale: {Scale}"; - string guixScaleString = $"GUI.xScale: {xScale}"; - string guiyScaleString = $"GUI.yScale: {yScale}"; - string relativeHorizontalAspectRatioString = $"RelativeHorizontalAspectRatio: {RelativeHorizontalAspectRatio}"; - string relativeVerticalAspectRatioString = $"RelativeVerticalAspectRatio: {RelativeVerticalAspectRatio}"; - Vector2 guiScaleStringSize = SmallFont.MeasureString(guiScaleString); - Vector2 guixScaleStringSize = SmallFont.MeasureString(guixScaleString); - Vector2 guiyScaleStringSize = SmallFont.MeasureString(guiyScaleString); - Vector2 relativeHorizontalAspectRatioStringSize = SmallFont.MeasureString(relativeHorizontalAspectRatioString); - Vector2 relativeVerticalAspectRatioStringSize = SmallFont.MeasureString(relativeVerticalAspectRatioString); + strings = new string[] + { + $"GUI.Scale: {Scale}", + $"GUI.xScale: {xScale}", + $"GUI.yScale: {yScale}", + $"RelativeHorizontalAspectRatio: {RelativeHorizontalAspectRatio}", + $"RelativeVerticalAspectRatio: {RelativeVerticalAspectRatio}", + }; + } - int padding = IntScale(10); - int yPos = padding; + strings = strings.Concat(new string[] { $"Cam.Zoom: {Screen.Selected.Cam?.Zoom ?? 0f}" }); - DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)guiScaleStringSize.X - padding, yPos), guiScaleString, Color.LightGreen, Color.Black, 0, SmallFont); - yPos += (int)guiScaleStringSize.Y + padding / 2; + int padding = IntScale(10); + int yPos = padding; - DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)guixScaleStringSize.X - padding, yPos), guixScaleString, Color.LightGreen, Color.Black, 0, SmallFont); - yPos += (int)guixScaleStringSize.Y + padding / 2; + foreach (string str in strings) + { + Vector2 stringSize = SmallFont.MeasureString(str); - DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)guiyScaleStringSize.X - padding, yPos), guiyScaleString, Color.LightGreen, Color.Black, 0, SmallFont); - yPos += (int)guiyScaleStringSize.Y + padding / 2; - - DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)relativeHorizontalAspectRatioStringSize.X - padding, yPos), relativeHorizontalAspectRatioString, Color.LightGreen, Color.Black, 0, SmallFont); - yPos += (int)relativeHorizontalAspectRatioStringSize.Y + padding / 2; - - DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)relativeVerticalAspectRatioStringSize.X - padding, yPos), relativeVerticalAspectRatioString, Color.LightGreen, Color.Black, 0, SmallFont); - yPos += (int)relativeVerticalAspectRatioStringSize.Y + padding / 2; + DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)stringSize.X - padding, yPos), str, Color.LightGreen, Color.Black, 0, SmallFont); + yPos += (int)stringSize.Y + padding / 2; } } @@ -645,6 +646,8 @@ namespace Barotrauma } } + DrawSavingIndicator(spriteBatch); + if (GameMain.WindowActive && !HideCursor) { spriteBatch.End(); @@ -855,6 +858,7 @@ namespace Barotrauma lock (mutex) { GUIMessageBox.AddActiveToGUIUpdateList(); + GUIContextMenu.AddActiveToGUIUpdateList(); if (pauseMenuOpen) { @@ -921,6 +925,7 @@ namespace Barotrauma if ((!PlayerInput.PrimaryMouseButtonHeld() && !PlayerInput.PrimaryMouseButtonClicked()) || c == prevMouseOn) { MouseOn = c; + var sakdjfnsjkd = c.MouseRect; } break; } @@ -1043,7 +1048,7 @@ namespace Barotrauma } } - if (parent != null) + if (parent != null && parent.CanBeFocused) { if (!parent.Rect.Equals(monitorRect)) { return parent.HoverCursor; } } @@ -1222,6 +1227,7 @@ namespace Barotrauma Debug.Assert(updateList.Count == updateListSet.Count); updateList.ForEach(c => c.UpdateAuto(deltaTime)); UpdateMessages(deltaTime); + UpdateSavingIndicator(deltaTime); } } @@ -1266,6 +1272,58 @@ namespace Barotrauma } + private static void UpdateSavingIndicator(float deltaTime) + { + lock (mutex) + { + if (timeUntilSavingIndicatorDisabled.HasValue) + { + timeUntilSavingIndicatorDisabled -= deltaTime; + if (timeUntilSavingIndicatorDisabled <= 0.0f) + { + isSavingIndicatorEnabled = false; + timeUntilSavingIndicatorDisabled = null; + } + } + if (isSavingIndicatorEnabled) + { + if (savingIndicatorColor == Color.Transparent) + { + savingIndicatorState = SavingIndicatorState.FadingIn; + savingIndicatorColorLerpAmount = 0.0f; + } + else if (savingIndicatorColor == Color.White) + { + savingIndicatorState = SavingIndicatorState.None; + } + } + else + { + if (savingIndicatorColor == Color.White) + { + savingIndicatorState = SavingIndicatorState.FadingOut; + savingIndicatorColorLerpAmount = 0.0f; + } + else if (savingIndicatorColor == Color.Transparent) + { + savingIndicatorState = SavingIndicatorState.None; + } + } + if (savingIndicatorState != SavingIndicatorState.None) + { + bool isFadingIn = savingIndicatorState == SavingIndicatorState.FadingIn; + Color lerpStartColor = isFadingIn ? Color.Transparent : Color.White; + Color lerpTargetColor = isFadingIn ? Color.White : Color.Transparent; + savingIndicatorColorLerpAmount += (isFadingIn ? 2.0f : 0.5f) * deltaTime; + savingIndicatorColor = Color.Lerp(lerpStartColor, lerpTargetColor, savingIndicatorColorLerpAmount); + } + if (IsSavingIndicatorVisible) + { + savingIndicatorSpriteIndex = (savingIndicatorSpriteIndex + 15.0f * deltaTime) % (Style.SavingIndicator.FrameCount + 1); + } + } + } + #region Element drawing private static List usedIndicatorAngles = new List(); @@ -1278,7 +1336,7 @@ namespace Barotrauma Vector2 diff = worldPosition - cam.WorldViewCenter; float dist = diff.Length(); - float symbolScale = Math.Min(64.0f / sprite.size.X, 1.0f) * scaleMultiplier; + float symbolScale = Math.Min(64.0f / sprite.size.X, 1.0f) * scaleMultiplier * Scale; if (overrideAlpha.HasValue || dist > hideDist) { @@ -1336,9 +1394,9 @@ namespace Barotrauma } } - public static void DrawLine(SpriteBatch sb, Vector2 start, Vector2 end, Color clr, float depth = 0.0f, int width = 1) + public static void DrawLine(SpriteBatch sb, Vector2 start, Vector2 end, Color clr, float depth = 0.0f, float width = 1) { - DrawLine(sb, t, start, end, clr, depth, width); + DrawLine(sb, t, start, end, clr, depth, (int)width); } public static void DrawLine(SpriteBatch sb, Sprite sprite, Vector2 start, Vector2 end, Color clr, float depth = 0.0f, int width = 1) @@ -1405,7 +1463,7 @@ namespace Barotrauma font.DrawStringWithColors(sb, text, pos, color, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, depth, richTextData); } - public static void DrawRectangle(SpriteBatch sb, Vector2 start, Vector2 size, Color clr, bool isFilled = false, float depth = 0.0f, int thickness = 1) + public static void DrawRectangle(SpriteBatch sb, Vector2 start, Vector2 size, Color clr, bool isFilled = false, float depth = 0.0f, float thickness = 1) { if (size.X < 0) { @@ -1420,7 +1478,7 @@ namespace Barotrauma DrawRectangle(sb, new Rectangle((int)start.X, (int)start.Y, (int)size.X, (int)size.Y), clr, isFilled, depth, thickness); } - public static void DrawRectangle(SpriteBatch sb, Rectangle rect, Color clr, bool isFilled = false, float depth = 0.0f, int thickness = 1) + public static void DrawRectangle(SpriteBatch sb, Rectangle rect, Color clr, bool isFilled = false, float depth = 0.0f, float thickness = 1) { if (isFilled) { @@ -1428,15 +1486,15 @@ namespace Barotrauma } else { - sb.Draw(t, new Rectangle(rect.X + thickness, rect.Y, rect.Width - thickness * 2, thickness), null, clr, 0.0f, Vector2.Zero, SpriteEffects.None, depth); - sb.Draw(t, new Rectangle(rect.X + thickness, rect.Y + rect.Height - thickness, rect.Width - thickness * 2, thickness), null, clr, 0.0f, Vector2.Zero, SpriteEffects.None, depth); - - sb.Draw(t, new Rectangle(rect.X, rect.Y, thickness, rect.Height), null, clr, 0.0f, Vector2.Zero, SpriteEffects.None, depth); - sb.Draw(t, new Rectangle(rect.X + rect.Width - thickness, rect.Y, thickness, rect.Height), null, clr, 0.0f, Vector2.Zero, SpriteEffects.None, depth); + Rectangle srcRect = new Rectangle(0, 0, 1, 1); + sb.Draw(t, new Vector2(rect.X, rect.Y), srcRect, clr, 0.0f, Vector2.Zero, new Vector2(thickness, rect.Height), SpriteEffects.None, depth); + sb.Draw(t, new Vector2(rect.X + thickness, rect.Y), srcRect, clr, 0.0f, Vector2.Zero, new Vector2(rect.Width - thickness, thickness), SpriteEffects.None, depth); + sb.Draw(t, new Vector2(rect.X + thickness, rect.Bottom - thickness), srcRect, clr, 0.0f, Vector2.Zero, new Vector2(rect.Width - thickness, thickness), SpriteEffects.None, depth); + sb.Draw(t, new Vector2(rect.Right - thickness, rect.Y + thickness), srcRect, clr, 0.0f, Vector2.Zero, new Vector2(thickness, rect.Height - thickness * 2f), SpriteEffects.None, depth); } } - public static void DrawRectangle(SpriteBatch sb, Vector2 center, float width, float height, float rotation, Color clr, float depth = 0.0f, int thickness = 1) + public static void DrawRectangle(SpriteBatch sb, Vector2 center, float width, float height, float rotation, Color clr, float depth = 0.0f, float thickness = 1) { Matrix rotate = Matrix.CreateRotationZ(rotation); @@ -1453,7 +1511,7 @@ namespace Barotrauma DrawLine(sb, bottomLeft, topLeft, clr, depth, thickness); } - public static void DrawRectangle(SpriteBatch sb, Vector2[] corners, Color clr, float depth = 0.0f, int thickness = 1) + public static void DrawRectangle(SpriteBatch sb, Vector2[] corners, Color clr, float depth = 0.0f, float thickness = 1) { if (corners.Length != 4) { @@ -1590,6 +1648,14 @@ namespace Barotrauma ShapeExtensions.DrawPoint(spriteBatch, pos, color, dotSize); } } + + private static void DrawSavingIndicator(SpriteBatch spriteBatch) + { + if (!IsSavingIndicatorVisible) { return; } + var sheet = Style.SavingIndicator; + Vector2 pos = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) - new Vector2(HUDLayoutSettings.Padding) - 2 * Scale * sheet.FrameSize.ToVector2(); + sheet.Draw(spriteBatch, (int)Math.Floor(savingIndicatorSpriteIndex), pos, savingIndicatorColor, origin: Vector2.Zero, rotate: 0.0f, scale: new Vector2(Scale)); + } #endregion #region Element creation @@ -2246,7 +2312,7 @@ namespace Barotrauma OnClicked = (btn, userdata) => { if (!GameMain.Client.HasPermission(ClientPermissions.ManageRound)) { return false; } - if (GameMain.GameSession.GameMode is CampaignMode || (!Submarine.MainSub.AtStartPosition && !Submarine.MainSub.AtEndPosition)) + if (GameMain.GameSession.GameMode is CampaignMode || (!Submarine.MainSub.AtStartExit && !Submarine.MainSub.AtEndExit)) { var msgBox = new GUIMessageBox("", TextManager.Get(GameMain.GameSession.GameMode is CampaignMode ? "PauseMenuReturnToServerLobbyVerification" : "EndRoundSubNotAtLevelEnd"), @@ -2343,6 +2409,21 @@ namespace Barotrauma float aspectRatio = HorizontalAspectRatio; return aspectRatio > 1.3f && aspectRatio < 1.4f; } + + public static void SetSavingIndicatorState(bool enabled) + { + if (enabled) + { + timeUntilSavingIndicatorDisabled = null; + } + isSavingIndicatorEnabled = enabled; + } + + public static void DisableSavingIndicatorDelayed(float delay = 3.0f) + { + if (!isSavingIndicatorEnabled) { return; } + timeUntilSavingIndicatorDisabled = delay; + } #endregion } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index 31233c164..ecc75885a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -77,6 +77,12 @@ namespace Barotrauma return RectTransform.IsParentOf(component.RectTransform, recursive); } + public bool IsChildOf(GUIComponent component, bool recursive = true) + { + if (component == null) { return false; } + return RectTransform.IsChildOf(component.RectTransform, recursive); + } + public virtual void RemoveChild(GUIComponent child) { if (child == null) { return; } @@ -705,6 +711,29 @@ namespace Barotrauma if (!Visible) return; DrawToolTip(spriteBatch, ToolTip, GUI.MouseOn.Rect, TooltipRichTextData); } + + public static void DrawToolTip(SpriteBatch spriteBatch, string toolTip, Vector2 pos, List richTextData = null) + { + if (Tutorials.Tutorial.ContentRunning) { return; } + + int width = (int)(400 * GUI.Scale); + int height = (int)(18 * GUI.Scale); + Point padding = new Point((int)(10 * GUI.Scale)); + + if (toolTipBlock == null || (string)toolTipBlock.userData != toolTip) + { + toolTipBlock = new GUITextBlock(new RectTransform(new Point(width, height), null), richTextData, toolTip, font: GUI.SmallFont, wrap: true, style: "GUIToolTip"); + toolTipBlock.RectTransform.NonScaledSize = new Point( + (int)(GUI.SmallFont.MeasureString(toolTipBlock.WrappedText).X + padding.X + toolTipBlock.Padding.X + toolTipBlock.Padding.Z), + (int)(GUI.SmallFont.MeasureString(toolTipBlock.WrappedText).Y + padding.Y + toolTipBlock.Padding.Y + toolTipBlock.Padding.W)); + toolTipBlock.userData = toolTip; + } + + toolTipBlock.RectTransform.AbsoluteOffset = pos.ToPoint(); + toolTipBlock.SetTextPos(); + + toolTipBlock.DrawManually(spriteBatch); + } public static void DrawToolTip(SpriteBatch spriteBatch, string toolTip, Rectangle targetElement, List richTextData = null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs new file mode 100644 index 000000000..f752af7c2 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs @@ -0,0 +1,292 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + struct ContextMenuOption + { + public string Label; + public Action OnSelected; + public ContextMenuOption[]? SubOptions; + public bool IsEnabled; + public string Tooltip; + + // Creates a regular context menu + public ContextMenuOption(string label, bool isEnabled, Action onSelected) + { + Label = TextManager.Get(label, returnNull: true) ?? label; + OnSelected = onSelected; + IsEnabled = isEnabled; + SubOptions = null; + Tooltip = string.Empty; + } + + // Creates a option with a sub context menu + public ContextMenuOption(string label, bool isEnabled, params ContextMenuOption[] options): this(label, isEnabled, () => { }) + { + SubOptions = options; + } + } + + internal class GUIContextMenu : GUIComponent + { + public static GUIContextMenu? CurrentContextMenu; + + private readonly Dictionary Options = new Dictionary(); + private GUIContextMenu? SubMenu; + public readonly GUITextBlock? HeaderLabel; + public GUITextBlock? ParentOption; + + /// + /// Creates a context menu. This constructor does not make the context menu active. + /// Use to make right click context menus. + /// + /// Position at which to create the context menu + /// Header text + /// Background style + /// list of context menu options + public GUIContextMenu(Vector2? position, string header, string style, params ContextMenuOption[] options) : base(style, new RectTransform(Point.Zero, GUI.Canvas)) + { + Vector2 pos = position ?? PlayerInput.MousePosition; + ScalableFont headerFont = GUI.SubHeadingFont; + ScalableFont font = GUI.SmallFont; // font the context menu options use + Vector4 padding = new Vector4(4), headerPadding = new Vector4(8); + int horizontalPadding = (int) (padding.X + padding.Z), verticalPadding = (int) (padding.Y + padding.W); + bool hasHeader = !string.IsNullOrWhiteSpace(header); + + //---------------------------------------------------------------------------------- + // Estimate the size of the context menu + //---------------------------------------------------------------------------------- + + Dictionary optionsAndSizes = new Dictionary(); + + // estimate how big the context menu needs to be + Point estimatedSize = new Point(horizontalPadding, verticalPadding); + + if (hasHeader) + { + InflateSize(ref estimatedSize, header, headerFont); + } + + foreach (ContextMenuOption option in options) + { + Vector2 optionSize = InflateSize(ref estimatedSize, option.Label, font); + optionsAndSizes.Add(option, optionSize); + } + + // it's better to overestimate the size since it's going to be cropped anyways + estimatedSize = estimatedSize.Multiply(1.2f); + + RectTransform.NonScaledSize = estimatedSize; + RectTransform.AbsoluteOffset = pos.ToPoint(); + + //---------------------------------------------------------------------------------- + // Construct the GUI elements + //---------------------------------------------------------------------------------- + + GUILayoutGroup background = new GUILayoutGroup(new RectTransform(Vector2.One, RectTransform, Anchor.Center)); + + if (hasHeader) + { + HeaderLabel = new GUITextBlock(new RectTransform(new Vector2(1f, 0.2f), background.RectTransform), header, font: headerFont) { Padding = headerPadding }; + } + + GUIListBox optionList = new GUIListBox(new RectTransform(new Vector2(1f, hasHeader ? 0.8f : 1f), background.RectTransform), style: null) + { + AutoHideScrollBar = false, + ScrollBarVisible = false, + Padding = hasHeader ? new Vector4(4, 0, 4, 4) : padding + }; + + foreach (var (option, size) in optionsAndSizes) + { + GUITextBlock optionElement = new GUITextBlock(new RectTransform(size.ToPoint(), optionList.Content.RectTransform), option.Label, font: font) + { + UserData = option, + Enabled = option.IsEnabled + }; + Options.Add(option, optionElement); + + if (!string.IsNullOrWhiteSpace(option.Tooltip) && optionElement.Enabled) + { + optionElement.ToolTip = option.Tooltip; + } + + if (!option.IsEnabled) + { + optionElement.TextColor *= 0.5f; + } + } + + //---------------------------------------------------------------------------------- + // Positioning and cropping the context menu + //---------------------------------------------------------------------------------- + + List children = optionList.Content.Children.ToList(); + + // Resize all children to the size of their text + foreach (GUITextBlock block in children.Where(c => c is GUITextBlock).Cast()) + { + block.RectTransform.NonScaledSize = new Point((int) (block.TextSize.X + (block.Padding.X + block.Padding.Z)), (int) (18 * GUI.Scale)); + } + + int largestWidth = children.Max(c => c.Rect.Width + horizontalPadding); + + // if the header is bigger than any of the options then overwrite + if (HeaderLabel != null) + { + RectTransform headerTransform = HeaderLabel.RectTransform; + headerTransform.MinSize = new Point((int) (HeaderLabel.TextSize.X + (headerPadding.X + headerPadding.Z)), headerTransform.NonScaledSize.Y); + if (largestWidth < headerTransform.MinSize.X) + { + largestWidth = headerTransform.MinSize.X; + } + } + + // resize all children to the size of the longest element + foreach (GUIComponent c in children) + { + c.RectTransform.MinSize = new Point(largestWidth, c.Rect.Height); + } + + // the cropped size of the option list + Point newSize = new Point(largestWidth, children.Sum(c => c.Rect.Height) + verticalPadding); + // resize the menu itself taking into account the option menus relative Y size + RectTransform.NonScaledSize = new Point(newSize.X, (int) (newSize.Y / optionList.RectTransform.RelativeSize.Y)); + optionList.RectTransform.NonScaledSize = newSize; + + // move the context menu if it would go outside of screen + if (RectTransform.Rect.Bottom > GameMain.GraphicsHeight) + { + Rectangle rect = RectTransform.Rect; + RectTransform.AbsoluteOffset = new Point(rect.X, rect.Y - rect.Height); + } + + if (RectTransform.Rect.Right > GameMain.GraphicsWidth) + { + Rectangle rect = RectTransform.Rect; + RectTransform.AbsoluteOffset = new Point(rect.X - rect.Width, rect.Y); + } + + background.Recalculate(); + + optionList.OnSelected = OnSelected; + } + + public static GUIContextMenu CreateContextMenu(params ContextMenuOption[] options) => CreateContextMenu(PlayerInput.MousePosition, string.Empty, null, options); + + public static GUIContextMenu CreateContextMenu(Vector2? pos, string header, Color? headerColor, params ContextMenuOption[] options) + { + GUIContextMenu menu = new GUIContextMenu(pos,header, "GUIToolTip", options); + if (headerColor != null) + { + menu.HeaderLabel?.OverrideTextColor(headerColor.Value); + } + CurrentContextMenu = menu; + return menu; + } + + private bool OnSelected(GUIComponent _, object data) + { + if (data is ContextMenuOption option && option.IsEnabled) + { + CurrentContextMenu = null; + option.OnSelected(); + return true; + } + + return false; + } + + /// + /// Inflates a point by the size of the text + /// + /// Pint to resize + /// String whose size to inflate by + /// What font to use + /// The size of the text + private Vector2 InflateSize(ref Point size, string label, ScalableFont font) + { + Vector2 textSize = font.MeasureString(label); + size.X = Math.Max((int) Math.Ceiling(textSize.X), size.X); + size.Y += (int) Math.Ceiling(textSize.Y); + return textSize; + } + + protected override void Update(float deltaTime) + { + base.Update(deltaTime); + + // keep the parent highlighted + if (ParentOption != null) + { + ParentOption.State = ComponentState.Hover; + } + + if (SubMenu != null && !SubMenu.IsMouseOver()) + { + SubMenu = null; + return; + } + + foreach (var (option, textBlock) in Options) + { + // Create a new sub context menu if hovering over an option with sub options + if (GUI.MouseOn != textBlock) { continue; } + if (option.IsEnabled && option.SubOptions is { } subOptions && subOptions.Any()) + { + Vector2 subMenuPos = new Vector2(textBlock.MouseRect.Right + 4, textBlock.MouseRect.Y); + SubMenu = new GUIContextMenu(subMenuPos, "", "GUIToolTip", subOptions) + { + ParentOption = textBlock + }; + } + } + } + + /// + /// Checks if the mouse cursor is over this context menu or any of its sub menus + /// + /// + private bool IsMouseOver() + { + Rectangle expandedRect = Rect; + expandedRect.Inflate(20, 20); + + bool isMouseOn = expandedRect.Contains(PlayerInput.MousePosition); + + if (ParentOption != null) + { + isMouseOn |= GUI.MouseOn == ParentOption; + } + + // Recursively check sub context menus + if (!isMouseOn && SubMenu != null) + { + isMouseOn = SubMenu.IsMouseOver(); + } + + return isMouseOn; + } + + public override void AddToGUIUpdateList(bool ignoreChildren = false, int order = 0) + { + base.AddToGUIUpdateList(ignoreChildren, order); + SubMenu?.AddToGUIUpdateList(); + } + + public static void AddActiveToGUIUpdateList() + { + if (CurrentContextMenu != null && !CurrentContextMenu.IsMouseOver()) + { + CurrentContextMenu = null; + } + + CurrentContextMenu?.AddToGUIUpdateList(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs index 87917f583..a6867eb60 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs @@ -194,6 +194,8 @@ namespace Barotrauma public bool ScrollBarEnabled { get; set; } = true; public bool KeepSpaceForScrollBar { get; set; } + public bool CanTakeKeyBoardFocus { get; set; } = true; + public bool ScrollBarVisible { get @@ -214,7 +216,22 @@ namespace Barotrauma public bool AutoHideScrollBar { get; set; } = true; private bool IsScrollBarOnDefaultSide { get; set; } - public bool CanDragElements { get; set; } = false; + public bool CanDragElements + { + get + { + return canDragElements; + } + set + { + if (value == false && canDragElements && draggedElement != null) + { + draggedElement = null; + } + canDragElements = value; + } + } + private bool canDragElements = false; private GUIComponent draggedElement; private Rectangle draggedReferenceRectangle; private Point draggedReferenceOffset; @@ -223,9 +240,12 @@ namespace Barotrauma private bool scheduledScroll = false; + private readonly bool isHorizontal; + /// For horizontal listbox, default side is on the bottom. For vertical, it's on the right. public GUIListBox(RectTransform rectT, bool isHorizontal = false, Color? color = null, string style = "", bool isScrollBarOnDefaultSide = true, bool useMouseDownToSelect = false) : base(style, rectT) { + this.isHorizontal = isHorizontal; HoverCursor = CursorState.Hand; CanBeFocused = true; selected = new List(); @@ -454,22 +474,42 @@ namespace Barotrauma } else { - draggedElement.RectTransform.AbsoluteOffset = draggedReferenceOffset + new Point(0, (int)PlayerInput.MousePosition.Y - draggedReferenceRectangle.Center.Y); + draggedElement.RectTransform.AbsoluteOffset = isHorizontal ? + draggedReferenceOffset + new Point((int)PlayerInput.MousePosition.X - draggedReferenceRectangle.Center.X, 0) : + draggedReferenceOffset + new Point(0, (int)PlayerInput.MousePosition.Y - draggedReferenceRectangle.Center.Y); int index = Content.RectTransform.GetChildIndex(draggedElement.RectTransform); int currIndex = index; - while (currIndex > 0 && PlayerInput.MousePosition.Y < draggedReferenceRectangle.Top) + if (isHorizontal) { - currIndex--; - draggedReferenceRectangle.Y -= draggedReferenceRectangle.Height; - draggedReferenceOffset.Y -= draggedReferenceRectangle.Height; + while (currIndex > 0 && PlayerInput.MousePosition.X < draggedReferenceRectangle.Left) + { + currIndex--; + draggedReferenceRectangle.X -= draggedReferenceRectangle.Width; + draggedReferenceOffset.X -= draggedReferenceRectangle.Width; + } + while (currIndex < Content.CountChildren - 1 && PlayerInput.MousePosition.X > draggedReferenceRectangle.Right) + { + currIndex++; + draggedReferenceRectangle.X += draggedReferenceRectangle.Width; + draggedReferenceOffset.X += draggedReferenceRectangle.Width; + } } - while (currIndex < Content.CountChildren - 1 && PlayerInput.MousePosition.Y > draggedReferenceRectangle.Bottom) + else { - currIndex++; - draggedReferenceRectangle.Y += draggedReferenceRectangle.Height; - draggedReferenceOffset.Y += draggedReferenceRectangle.Height; + while (currIndex > 0 && PlayerInput.MousePosition.Y < draggedReferenceRectangle.Top) + { + currIndex--; + draggedReferenceRectangle.Y -= draggedReferenceRectangle.Height; + draggedReferenceOffset.Y -= draggedReferenceRectangle.Height; + } + while (currIndex < Content.CountChildren - 1 && PlayerInput.MousePosition.Y > draggedReferenceRectangle.Bottom) + { + currIndex++; + draggedReferenceRectangle.Y += draggedReferenceRectangle.Height; + draggedReferenceOffset.Y += draggedReferenceRectangle.Height; + } } if (currIndex != index) @@ -853,7 +893,7 @@ namespace Barotrauma } // If one of the children is the subscriber, we don't want to register, because it will unregister the child. - if (takeKeyBoardFocus && RectTransform.GetAllChildren().None(rt => rt.GUIComponent == GUI.KeyboardDispatcher.Subscriber)) + if (takeKeyBoardFocus && CanTakeKeyBoardFocus && RectTransform.GetAllChildren().None(rt => rt.GUIComponent == GUI.KeyboardDispatcher.Subscriber)) { Selected = true; GUI.KeyboardDispatcher.Subscriber = this; @@ -943,9 +983,10 @@ namespace Barotrauma public override void RemoveChild(GUIComponent child) { - if (child == null) { return; } + if (child == null) { return; } child.RectTransform.Parent = null; - if (selected.Contains(child)) selected.Remove(child); + if (selected.Contains(child)) { selected.Remove(child); } + if (draggedElement == child) { draggedElement = null; } UpdateScrollBarSize(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs index 3ff6f35ed..0d7d27fd5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; using Barotrauma.Networking; +using Barotrauma.Extensions; namespace Barotrauma { @@ -22,11 +23,11 @@ namespace Barotrauma { Default, InGame, - Vote + Vote, + Hint } public List Buttons { get; private set; } = new List(); - //public GUIFrame BackgroundFrame { get; private set; } public GUILayoutGroup Content { get; private set; } public GUIFrame InnerFrame { get; private set; } public GUITextBlock Header { get; private set; } @@ -58,8 +59,6 @@ namespace Barotrauma public bool AutoClose; - private readonly bool alwaysVisible; - private float openState; private float iconState; private bool iconSwitching; @@ -77,10 +76,17 @@ namespace Barotrauma this.Buttons[0].OnClicked = Close; } - public GUIMessageBox(string headerText, string text, string[] buttons, Vector2? relativeSize = null, Point? minSize = null, Alignment textAlignment = Alignment.TopLeft, Type type = Type.Default, string tag = "", Sprite icon = null, string iconStyle = "", Sprite backgroundIcon = null) + public GUIMessageBox(string headerText, string text, string[] buttons, Vector2? relativeSize = null, Point? minSize = null, Alignment textAlignment = Alignment.TopLeft, Type type = Type.Default, string tag = "", Sprite icon = null, string iconStyle = "", Sprite backgroundIcon = null, bool parseRichText = false) : base(new RectTransform(GUI.Canvas.RelativeSize, GUI.Canvas, Anchor.Center), style: GUI.Style.GetComponentStyle("GUIMessageBox." + type) != null ? "GUIMessageBox." + type : "GUIMessageBox") { - int width = (int)(DefaultWidth * (type == Type.Default ? 1.0f : 1.5f)), height = 0; + int width = (int)(DefaultWidth * type switch + { + Type.Default => 1.0f, + Type.Hint => 1.25f, + _ => 1.5f + }); + int height = 0; + if (relativeSize.HasValue) { width = (int)(GameMain.GraphicsWidth * relativeSize.Value.X); @@ -107,6 +113,7 @@ namespace Barotrauma Anchor anchor = type switch { Type.InGame => Anchor.TopCenter, + Type.Hint => Anchor.TopRight, Type.Vote => Anchor.TopRight, _ => Anchor.Center }; @@ -127,13 +134,13 @@ namespace Barotrauma Content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.85f), InnerFrame.RectTransform, Anchor.Center)) { AbsoluteSpacing = 5 }; Header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), - headerText, font: GUI.SubHeadingFont, textAlignment: Alignment.Center, wrap: true); + headerText, font: GUI.SubHeadingFont, textAlignment: Alignment.Center, wrap: true, parseRichText: parseRichText); GUI.Style.Apply(Header, "", this); Header.RectTransform.MinSize = new Point(0, Header.Rect.Height); if (!string.IsNullOrWhiteSpace(text)) { - Text = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), text, textAlignment: textAlignment, wrap: true); + Text = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), text, textAlignment: textAlignment, wrap: true, parseRichText: parseRichText); GUI.Style.Apply(Text, "", this); Text.RectTransform.NonScaledSize = Text.RectTransform.MinSize = Text.RectTransform.MaxSize = new Point(Text.Rect.Width, Text.Rect.Height); @@ -180,7 +187,6 @@ namespace Barotrauma else if (type == Type.InGame) { InnerFrame.RectTransform.AbsoluteOffset = new Point(0, GameMain.GraphicsHeight); - alwaysVisible = true; CanBeFocused = false; AutoClose = true; GUI.Style.Apply(InnerFrame, "", this); @@ -235,13 +241,13 @@ namespace Barotrauma }; } - Header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), headerText, wrap: true); + Header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), headerText, wrap: true, parseRichText: parseRichText); GUI.Style.Apply(Header, "", this); Header.RectTransform.MinSize = new Point(0, Header.Rect.Height); if (!string.IsNullOrWhiteSpace(text)) { - Text = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), text, textAlignment: textAlignment, wrap: true); + Text = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), text, textAlignment: textAlignment, wrap: true, parseRichText: parseRichText); GUI.Style.Apply(Text, "", this); Content.Recalculate(); Text.RectTransform.NonScaledSize = Text.RectTransform.MinSize = Text.RectTransform.MaxSize = @@ -266,30 +272,184 @@ namespace Barotrauma } Buttons[0].RectTransform.MaxSize = new Point((int)(0.4f * Buttons[0].Rect.Y), Buttons[0].Rect.Y); } - + else if (type == Type.Hint) + { + CanBeFocused = false; + GUI.Style.Apply(InnerFrame, "", this); + + Point absoluteSpacing = GUIStyle.ItemFrameMargin.Multiply(1.0f / 5.0f); + var verticalLayoutGroup = new GUILayoutGroup(new RectTransform(GetVerticalLayoutGroupSize(), parent: InnerFrame.RectTransform, anchor: Anchor.Center), childAnchor: Anchor.TopCenter) + { + AbsoluteSpacing = absoluteSpacing.Y, + Stretch = true + }; + + var topHorizontalLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.7f), verticalLayoutGroup.RectTransform), + isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true, + RelativeSpacing = 0.02f + }; + + int iconMaxHeight = 0; + if (icon != null) + { + Icon = new GUIImage(new RectTransform(new Vector2(0.15f, 0.95f), topHorizontalLayoutGroup.RectTransform), icon, scaleToFit: true); + iconMaxHeight = (int)Icon.Sprite.size.Y; + } + else + { + bool iconStyleDefined = !string.IsNullOrEmpty(iconStyle); + Icon = new GUIImage(new RectTransform(new Vector2(0.15f, 0.95f), topHorizontalLayoutGroup.RectTransform), + iconStyleDefined ? iconStyle : "GUIButtonInfo", scaleToFit: true); + if (!iconStyleDefined) + { + Icon.Color = Color.Orange; + } + iconMaxHeight = (int)(Icon.Style.GetDefaultSprite()?.size.Y ?? GUI.yScale * 40); + } + + iconMaxHeight = Math.Min((int)(GUI.yScale * 40), iconMaxHeight); + int iconMinHeight = Math.Min((int)(GUI.yScale * 40), iconMaxHeight); + Icon.RectTransform.MinSize = new Point(Icon.Rect.Width, iconMinHeight); + Icon.RectTransform.MaxSize = new Point(Icon.Rect.Width, iconMaxHeight); + + Content = new GUILayoutGroup(new RectTransform(new Vector2(Icon != null ? 0.85f : 1.0f, 1.0f), topHorizontalLayoutGroup.RectTransform)) + { + AbsoluteSpacing = absoluteSpacing.Y, + }; + + var bottomContainer = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.3f), verticalLayoutGroup.RectTransform), style: null); + + var tickBoxLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.67f, 1.0f), bottomContainer.RectTransform, anchor: Anchor.CenterLeft), + isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true, + RelativeSpacing = 0.02f + }; + + var dontShowAgainTickBox = new GUITickBox(new RectTransform(new Vector2(0.5f, 1.0f), tickBoxLayoutGroup.RectTransform), + TextManager.Get("hintmessagebox.dontshowagain")) + { + ToolTip = TextManager.Get("hintmessagebox.dontshowagaintooltip"), + UserData = "dontshowagain" + }; + + //var disableHintsTickBox = new GUITickBox(new RectTransform(new Vector2(0.33f, 1.0f), tickBoxLayoutGroup.RectTransform), + // TextManager.Get("hintmessagebox.disablehints")) + //{ + // ToolTip = TextManager.Get("hintmessagebox.disablehintstooltip"), + // UserData = "disablehints" + //}; + + Buttons = new List(1) + { + new GUIButton(new RectTransform(new Vector2(0.33f, 1.0f), bottomContainer.RectTransform, Anchor.CenterRight), + text: TextManager.Get("hintmessagebox.dismiss"), style: "GUIButtonSmall") + { + OnClicked = Close + } + }; + + Header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), headerText, wrap: true); + GUI.Style.Apply(Header, "", this); + Header.RectTransform.MinSize = new Point(0, Header.Rect.Height); + + if (!string.IsNullOrWhiteSpace(text)) + { + Text = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), text, textAlignment: textAlignment, wrap: true); + GUI.Style.Apply(Text, "", this); + Content.Recalculate(); + Text.RectTransform.NonScaledSize = Text.RectTransform.MinSize = Text.RectTransform.MaxSize = + new Point(Text.Rect.Width, Text.Rect.Height); + Text.RectTransform.IsFixedSize = true; + if (string.IsNullOrWhiteSpace(headerText)) + { + Header.RectTransform.Parent = null; + Content.ChildAnchor = Anchor.Center; + } + } + + if (height == 0) + { + height = absoluteSpacing.Y; + int upperContainerHeight = absoluteSpacing.Y; + if (Header.Rect.Height > 0) { upperContainerHeight += Header.Rect.Height + Content.AbsoluteSpacing; } + if (Text != null) { upperContainerHeight += Text.Rect.Height + Content.AbsoluteSpacing; } + upperContainerHeight = Math.Max(upperContainerHeight, Icon.Rect.Height); + height += upperContainerHeight; + height += absoluteSpacing.Y; + height += (int)((bottomContainer.RectTransform.RelativeSize.Y / topHorizontalLayoutGroup.RectTransform.RelativeSize.Y) * upperContainerHeight); + height += absoluteSpacing.Y; + if (minSize.HasValue) { height = Math.Max(height, minSize.Value.Y); } + + InnerFrame.RectTransform.NonScaledSize = new Point(InnerFrame.Rect.Width, height); + verticalLayoutGroup.RectTransform.NonScaledSize = GetVerticalLayoutGroupSize(); + verticalLayoutGroup.Recalculate(); + topHorizontalLayoutGroup.Recalculate(); + Content.Recalculate(); + tickBoxLayoutGroup.Recalculate(); + } + + InnerFrame.RectTransform.AbsoluteOffset = new Point(GUI.IntScale(64), -InnerFrame.Rect.Height); + + Point GetVerticalLayoutGroupSize() + { + return InnerFrame.Rect.Size - absoluteSpacing.Multiply(2); + } + } + MessageBoxes.Add(this); } + /// + /// Use to create a message box of Hint type + /// + public GUIMessageBox(string hintIdentifier, string text, Sprite icon) : this("", text, new string[0], textAlignment: Alignment.CenterLeft, type: Type.Hint, icon: icon) + { + if (InnerFrame.FindChild("dontshowagain", recursive: true) is GUITickBox dontShowAgainTickBox) + { + dontShowAgainTickBox.OnSelected = HintManager.OnDontShowAgain; + dontShowAgainTickBox.UserData = hintIdentifier; + } + if (InnerFrame.FindChild("disablehints", recursive: true) is GUITickBox disableHintsTickBox) + { + disableHintsTickBox.OnSelected = HintManager.OnDisableHints; + disableHintsTickBox.UserData = hintIdentifier; + } + } + + private static Type[] messageBoxTypes; + public static void AddActiveToGUIUpdateList() { - for (int i = 0; i < MessageBoxes.Count; i++) + messageBoxTypes ??= (Type[])Enum.GetValues(typeof(Type)); + + foreach (var type in messageBoxTypes) { - if (MessageBoxes[i] is GUIMessageBox alwaysVisibleMsgBox && alwaysVisibleMsgBox.alwaysVisible) + // Don't display hints when HUD is disabled + if (type == Type.Hint && GUI.DisableHUD) { continue; } + + for (int i = 0; i < MessageBoxes.Count; i++) { - alwaysVisibleMsgBox.AddToGUIUpdateList(); - break; - } - } - for (int i = MessageBoxes.Count - 1; i >= 0; i--) - { - if (MessageBoxes[i].UserData as string == "verificationprompt" || - MessageBoxes[i].UserData as string == "bugreporter") - { - continue; - } - if (!(MessageBoxes[i] is GUIMessageBox msgBox) || !msgBox.alwaysVisible) - { - MessageBoxes[i].AddToGUIUpdateList(); + if (MessageBoxes[i] == null) { continue; } + if (!(MessageBoxes[i] is GUIMessageBox messageBox)) + { + if (type == Type.Default) + { + // Message box not of type GUIMessageBox is likely the round summary + MessageBoxes[i].AddToGUIUpdateList(); + break; + } + continue; + } + if (messageBox.type != type) { continue; } + + // These are handled separately in GUI.HandlePersistingElements() + if (MessageBoxes[i].UserData as string == "verificationprompt") { continue; } + if (MessageBoxes[i].UserData as string == "bugreporter") { continue; } + + messageBox.AddToGUIUpdateList(); break; } } @@ -337,11 +497,21 @@ namespace Barotrauma } } - if (type == Type.InGame) + if (type == Type.InGame || type == Type.Hint) { - Vector2 initialPos = new Vector2(0.0f, GameMain.GraphicsHeight); - Vector2 defaultPos = new Vector2(0.0f, HUDLayoutSettings.InventoryAreaLower.Y - InnerFrame.Rect.Height - 20 * GUI.Scale); - Vector2 endPos = new Vector2(GameMain.GraphicsWidth, defaultPos.Y); + Vector2 initialPos, defaultPos, endPos; + if (type == Type.InGame) + { + initialPos = new Vector2(0.0f, GameMain.GraphicsHeight); + defaultPos = new Vector2(0.0f, HUDLayoutSettings.InventoryAreaLower.Y - InnerFrame.Rect.Height - 20 * GUI.Scale); + endPos = new Vector2(GameMain.GraphicsWidth, defaultPos.Y); + } + else + { + initialPos = new Vector2(GUI.IntScale(64), -InnerFrame.Rect.Height); + defaultPos = new Vector2(initialPos.X, HUDLayoutSettings.ButtonAreaTop.Height + GUI.IntScale(64)); + endPos = new Vector2(-InnerFrame.Rect.Width, defaultPos.Y); + } if (!closing) { @@ -428,7 +598,7 @@ namespace Barotrauma public void Close() { - if (type == Type.InGame) + if (type == Type.InGame || type == Type.Hint) { closing = true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs index 58fa65ebb..169b4d758 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs @@ -34,6 +34,11 @@ namespace Barotrauma public readonly Sprite[] CursorSprite = new Sprite[7]; + public UISprite RadiationSprite { get; private set; } + public SpriteSheet RadiationAnimSpriteSheet { get; private set; } + + public SpriteSheet SavingIndicator { get; private set; } + public UISprite UIGlow { get; private set; } public UISprite UIGlowCircular { get; private set; } @@ -77,6 +82,12 @@ namespace Barotrauma public Color TextColorDark { get; private set; } = Color.Black * 0.9f; public Color TextColorDim { get; private set; } = Color.White * 0.6f; + public Color ColorReputationVeryLow { get; private set; } = Color.Red; + public Color ColorReputationLow { get; private set; } = Color.Orange; + public Color ColorReputationNeutral { get; private set; } = Color.White * 0.8f; + public Color ColorReputationHigh { get; private set; } = Color.LightBlue; + public Color ColorReputationVeryHigh { get; private set; } = Color.Blue; + // Inventory public Color EquipmentSlotIconColor { get; private set; } = new Color(99, 70, 64); @@ -167,6 +178,21 @@ namespace Barotrauma case "textcolor": TextColor = subElement.GetAttributeColor("color", TextColor); break; + case "colorreputationverylow": + ColorReputationVeryLow = subElement.GetAttributeColor("color", TextColor); + break; + case "colorreputationlow": + ColorReputationLow = subElement.GetAttributeColor("color", TextColor); + break; + case "colorreputationneutral": + ColorReputationNeutral = subElement.GetAttributeColor("color", TextColor); + break; + case "colorreputationhigh": + ColorReputationHigh = subElement.GetAttributeColor("color", TextColor); + break; + case "colorreputationveryhigh": + ColorReputationVeryHigh = subElement.GetAttributeColor("color", TextColor); + break; case "equipmentsloticoncolor": EquipmentSlotIconColor = subElement.GetAttributeColor("color", EquipmentSlotIconColor); break; @@ -209,6 +235,12 @@ namespace Barotrauma case "uiglow": UIGlow = new UISprite(subElement); break; + case "radiation": + RadiationSprite = new UISprite(subElement); + break; + case "radiationanimspritesheet": + RadiationAnimSpriteSheet = new SpriteSheet(subElement); + break; case "uiglowcircular": UIGlowCircular = new UISprite(subElement); break; @@ -218,6 +250,9 @@ namespace Barotrauma case "focusindicator": FocusIndicator = new SpriteSheet(subElement); break; + case "savingindicator": + SavingIndicator = new SpriteSheet(subElement); + break; case "font": Font = LoadFont(subElement, graphicsDevice); ForceFontUpperCase[Font] = subElement.GetAttributeBool("forceuppercase", false); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs index 523dc49b9..952cded4d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs @@ -259,9 +259,11 @@ namespace Barotrauma public StrikethroughSettings Strikethrough = null; - private readonly List richTextData = null; + public readonly List RichTextData = null; - private readonly bool hasColorHighlight = false; + public bool HasColorHighlight => RichTextData != null; + + public bool OverrideRichTextDataAlpha = true; public struct ClickableArea { @@ -279,7 +281,8 @@ namespace Barotrauma /// If the rectT height is set 0, the height is calculated from the text. /// public GUITextBlock(RectTransform rectT, string text, Color? textColor = null, ScalableFont font = null, - Alignment textAlignment = Alignment.Left, bool wrap = false, string style = "", Color? color = null, bool playerInput = false) + Alignment textAlignment = Alignment.Left, bool wrap = false, string style = "", Color? color = null, + bool playerInput = false, bool parseRichText = false) : base(style, rectT) { if (color.HasValue) @@ -289,7 +292,12 @@ namespace Barotrauma if (textColor.HasValue) { OverrideTextColor(textColor.Value); - } + } + + if (parseRichText) + { + RichTextData = Barotrauma.RichTextData.GetRichTextData(text, out text); + } //if the text is in chinese/korean/japanese and we're not using a CJK-compatible font, //use the default CJK font as a fallback @@ -318,8 +326,7 @@ namespace Barotrauma public GUITextBlock(RectTransform rectT, List richTextData, string text, Color? textColor = null, ScalableFont font = null, Alignment textAlignment = Alignment.Left, bool wrap = false, string style = "", Color? color = null, bool playerInput = false) : this(rectT, text, textColor, font, textAlignment, wrap, style, color, playerInput) { - this.richTextData = richTextData; - hasColorHighlight = richTextData != null; + this.RichTextData = richTextData; } public void CalculateHeightFromText(int padding = 0, bool removeExtraSpacing = false) @@ -568,8 +575,9 @@ namespace Barotrauma { base.Update(deltaTime); - if (ClickableAreas.Any() && (GUI.MouseOn?.IsParentOf(this) ?? true) && Rect.Contains(PlayerInput.MousePosition)) + if (ClickableAreas.Any() && (GUI.MouseOn?.IsParentOf(this) ?? true)) { + if (!Rect.Contains(PlayerInput.MousePosition)) { return; } int index = GetCaretIndexFromScreenPos(PlayerInput.MousePosition); foreach (ClickableArea clickableArea in ClickableAreas) { @@ -627,7 +635,7 @@ namespace Barotrauma currentTextColor = selectedTextColor; } - if (!hasColorHighlight) + if (!HasColorHighlight) { string textToShow = Censor ? censoredText : (Wrap ? wrappedText : text); Color colorToShow = currentTextColor * (currentTextColor.A / 255.0f); @@ -639,12 +647,15 @@ namespace Barotrauma } Font.DrawString(spriteBatch, textToShow, pos, colorToShow, 0.0f, origin, TextScale, SpriteEffects.None, textDepth); - } else { + if (OverrideRichTextDataAlpha) + { + RichTextData.ForEach(rt => rt.Alpha = currentTextColor.A / 255.0f); + } Font.DrawStringWithColors(spriteBatch, Censor ? censoredText : (Wrap ? wrappedText : text), pos, - currentTextColor * (currentTextColor.A / 255.0f), 0.0f, origin, TextScale, SpriteEffects.None, textDepth, richTextData); + currentTextColor * (currentTextColor.A / 255.0f), 0.0f, origin, TextScale, SpriteEffects.None, textDepth, RichTextData); } if (Strikethrough != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index 3c709beab..865184d16 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -626,6 +626,9 @@ namespace Barotrauma { if (Text == null) Text = ""; + // Prevent alt gr from triggering any of these as that combination is often needed for special characters + if (PlayerInput.IsAltDown()) return; + switch (command) { case '\b' when !Readonly: //backspace @@ -667,7 +670,10 @@ namespace Barotrauma } break; case (char)0x1: // ctrl-a - SelectAll(); + if (PlayerInput.IsCtrlDown()) + { + SelectAll(); + } break; case (char)0x1A when !Readonly && !SubEditorScreen.IsSubEditor(): // ctrl-z text = memento.Undo(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs index 4dd955f01..407236e08 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs @@ -128,7 +128,7 @@ namespace Barotrauma int messageAreaWidth = GameMain.GraphicsWidth / 3; - MessageAreaTop = new Rectangle((GameMain.GraphicsWidth - messageAreaWidth) / 2, ButtonAreaTop.Bottom, messageAreaWidth, ButtonAreaTop.Height); + MessageAreaTop = new Rectangle((GameMain.GraphicsWidth - messageAreaWidth) / 2, ButtonAreaTop.Bottom + ButtonAreaTop.Height, messageAreaWidth, ButtonAreaTop.Height); bool isFourByThree = GUI.IsFourByThree(); int chatBoxWidth = !isFourByThree ? (int)(475 * GUI.Scale) : (int)(375 * GUI.Scale); @@ -139,7 +139,9 @@ namespace Barotrauma int objectiveAnchorOffsetY = (int)(150 * GUI.Scale); ObjectiveAnchor = new Rectangle(Padding, ChatBoxArea.Y - objectiveAnchorOffsetY, objectiveAnchorWidth, 0); - CrewArea = new Rectangle(Padding, Padding, (int)Math.Max(400 * GUI.Scale, 220), ObjectiveAnchor.Top - Padding * 2); + int crewAreaY = ButtonAreaTop.Bottom + Padding; + int crewAreaHeight = ObjectiveAnchor.Top - Padding - crewAreaY; + CrewArea = new Rectangle(Padding, crewAreaY, (int)Math.Max(400 * GUI.Scale, 220), crewAreaHeight); InventoryAreaLower = new Rectangle(ChatBoxArea.Right + Padding * 7, inventoryTopY, GameMain.GraphicsWidth - Padding * 9 - ChatBoxArea.Width, GameMain.GraphicsHeight - inventoryTopY); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs index ba3c56caf..23b0f9036 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs @@ -70,6 +70,14 @@ namespace Barotrauma } private string selectedTip; + private List selectedTipRichTextData; + private bool selectedTipRichTextUnparsed; + private void SetSelectedTip(string tip) + { + selectedTip = tip; + selectedTipRichTextData = null; + selectedTipRichTextUnparsed = true; + } private readonly object loadMutex = new object(); private float? loadState; @@ -115,7 +123,7 @@ namespace Barotrauma overlay = TextureLoader.FromFile("Content/UI/LoadingScreenOverlay.png"); noiseSprite = new Sprite("Content/UI/noise.png", Vector2.Zero); DrawLoadingText = true; - selectedTip = TextManager.Get("LoadingScreenTip", true); + SetSelectedTip(TextManager.Get("LoadingScreenTip", true)); } public void Draw(SpriteBatch spriteBatch, GraphicsDevice graphics, float deltaTime) @@ -215,14 +223,34 @@ namespace Barotrauma if (GUI.Font != null && selectedTip != null) { + if (selectedTipRichTextUnparsed) + { + selectedTipRichTextData = RichTextData.GetRichTextData(selectedTip, out selectedTip); + selectedTipRichTextUnparsed = false; + } + string wrappedTip = ToolBox.WrapText(selectedTip, GameMain.GraphicsWidth * 0.5f, GUI.Font); string[] lines = wrappedTip.Split('\n'); float lineHeight = GUI.Font.MeasureString(selectedTip).Y; - for (int i = 0; i < lines.Length; i++) + if (selectedTipRichTextData != null) { - GUI.Font.DrawString(spriteBatch, lines[i], - new Vector2((int)(GameMain.GraphicsWidth / 2.0f - GUI.Font.MeasureString(lines[i]).X / 2.0f), (int)(GameMain.GraphicsHeight * 0.8f + i * lineHeight)), Color.White); + int rtdOffset = 0; + for (int i = 0; i < lines.Length; i++) + { + GUI.Font.DrawStringWithColors(spriteBatch, lines[i], + new Vector2((int)(GameMain.GraphicsWidth / 2.0f - GUI.Font.MeasureString(lines[i]).X / 2.0f), (int)(GameMain.GraphicsHeight * 0.8f + i * lineHeight)), Color.White, + 0f, Vector2.Zero, 1f, SpriteEffects.None, 0f, selectedTipRichTextData, rtdOffset); + rtdOffset += lines[i].Length; + } + } + else + { + for (int i = 0; i < lines.Length; i++) + { + GUI.Font.DrawString(spriteBatch, lines[i], + new Vector2((int)(GameMain.GraphicsWidth / 2.0f - GUI.Font.MeasureString(lines[i]).X / 2.0f), (int)(GameMain.GraphicsHeight * 0.8f + i * lineHeight)), Color.White); + } } } @@ -302,7 +330,7 @@ namespace Barotrauma { GameMain.Config.Language = language; //reload tip in the selected language - selectedTip = TextManager.Get("LoadingScreenTip", true); + SetSelectedTip(TextManager.Get("LoadingScreenTip", true)); GameMain.Config.SetDefaultBindings(legacy: false); GameMain.Config.CheckBindings(useDefaults: true); WaitForLanguageSelection = false; @@ -364,7 +392,7 @@ namespace Barotrauma { drawn = false; LoadState = null; - selectedTip = TextManager.Get("LoadingScreenTip", true); + SetSelectedTip(TextManager.Get("LoadingScreenTip", true)); currentBackgroundTexture = LocationType.List.GetRandom()?.GetPortrait(Rand.Int(int.MaxValue))?.Texture; while (!drawn) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs index 7355cdbc9..a00a820a9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs @@ -94,7 +94,7 @@ namespace Barotrauma } } - private static Point maxPoint = new Point(int.MaxValue, int.MaxValue); + public readonly static Point MaxPoint = new Point(int.MaxValue, int.MaxValue); private Point? maxSize; /// @@ -103,7 +103,7 @@ namespace Barotrauma /// public Point MaxSize { - get { return maxSize ?? maxPoint; } + get { return maxSize ?? MaxPoint; } set { if (maxSize == value) { return; } @@ -640,6 +640,12 @@ namespace Barotrauma return children.Contains(rectT) || (recursive && children.Any(c => c.IsParentOf(rectT))); } + public bool IsChildOf(RectTransform rectT, bool recursive = true) + { + if (Parent == null) { return false; } + return Parent == rectT || (recursive && Parent.IsChildOf(rectT)); + } + public void ClearChildren() { children.ForEachMod(c => c.Parent = null); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index b1aed13d8..3272d4940 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -24,7 +24,6 @@ namespace Barotrauma private int buyTotal, sellTotal; private GUITextBlock merchantBalanceBlock; - private GUILayoutGroup valueChangeGroup; private GUITextBlock currentSellValueBlock, newSellValueBlock; private GUIImage sellValueChangeArrow; private GUIDropDown sortingDropDown; @@ -158,7 +157,7 @@ namespace Barotrauma }; // Store header ------------------------------------------------ - var headerGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.75f / 14.0f), storeContent.RectTransform), isHorizontal: true) + var headerGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.95f / 14.0f), storeContent.RectTransform), isHorizontal: true) { RelativeSpacing = 0.005f }; @@ -209,7 +208,7 @@ namespace Barotrauma // Item sell value ------------------------------------------------ var sellValueContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), balanceAndValueGroup.RectTransform)) { - CanBeFocused = false, + CanBeFocused = true, RelativeSpacing = 0.005f }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), sellValueContainer.RectTransform), @@ -220,9 +219,9 @@ namespace Barotrauma ForceUpperCase = true }; - valueChangeGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), sellValueContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + var valueChangeGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), sellValueContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { - CanBeFocused = true, + CanBeFocused = false, RelativeSpacing = 0.02f }; float blockWidth = GUI.IsFourByThree() ? 0.32f : 0.28f; @@ -247,7 +246,7 @@ namespace Barotrauma { string tooltipTag = newStatus.SellPriceModifier > CurrentLocation.ActiveStoreBalanceStatus.SellPriceModifier ? "campaingstore.valueincreasetooltip" : "campaingstore.valuedecreasetooltip"; - valueChangeGroup.ToolTip = TextManager.Get(tooltipTag); + sellValueContainer.ToolTip = TextManager.Get(tooltipTag); currentSellValueBlock.TextColor = newStatus.Color; sellValueChangeArrow.Color = newStatus.Color; sellValueChangeArrow.Visible = true; @@ -256,7 +255,7 @@ namespace Barotrauma return $"{(CurrentLocation.ActiveStoreBalanceStatus.SellPriceModifier * 100).FormatZeroDecimal()} %"; } } - valueChangeGroup.ToolTip = null; + sellValueContainer.ToolTip = TextManager.Get("campaignstore.sellvaluetooltip"); currentSellValueBlock.TextColor = CurrentLocation.BalanceColor; sellValueChangeArrow.Visible = false; newSellValueBlock.Text = null; @@ -264,7 +263,7 @@ namespace Barotrauma } else { - valueChangeGroup.ToolTip = null; + sellValueContainer.ToolTip = null; sellValueChangeArrow.Visible = false; newSellValueBlock.Text = null; return null; @@ -293,7 +292,7 @@ namespace Barotrauma newSellValueBlock.Padding = newPadding; // Store mode buttons ------------------------------------------------ - var modeButtonFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.6f / 14.0f), storeContent.RectTransform), style: null); + var modeButtonFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.4f / 14.0f), storeContent.RectTransform), style: null); var modeButtonContainer = new GUILayoutGroup(new RectTransform(Vector2.One, modeButtonFrame.RectTransform), isHorizontal: true); var tabs = Enum.GetValues(typeof(StoreTab)); @@ -676,6 +675,7 @@ namespace Barotrauma (itemFrame.UserData as PurchasedItem).Quantity = quantity; SetQuantityLabelText(StoreTab.Buy, itemFrame); SetOwnedLabelText(itemFrame); + SetPriceGetters(itemFrame, true); } SetItemFrameStatus(itemFrame, hasPermissions && quantity > 0); existingItemFrames.Add(itemFrame); @@ -750,6 +750,7 @@ namespace Barotrauma (itemFrame.UserData as PurchasedItem).Quantity = itemQuantity; SetQuantityLabelText(StoreTab.Sell, itemFrame); SetOwnedLabelText(itemFrame); + SetPriceGetters(itemFrame, false); } SetItemFrameStatus(itemFrame, hasPermissions && itemQuantity > 0); if (itemQuantity < 1 && !isRequestedGood) @@ -772,6 +773,37 @@ namespace Barotrauma shoppingCrateSellList.BarScroll = prevShoppingCrateScroll; } + private void SetPriceGetters(GUIComponent itemFrame, bool buying) + { + if (itemFrame == null || !(itemFrame.UserData is PurchasedItem pi)) { return; } + + if (itemFrame.FindChild("undiscountedprice", recursive: true) is GUITextBlock undiscountedPriceBlock) + { + if (buying) + { + undiscountedPriceBlock.TextGetter = () => GetCurrencyFormatted( + CurrentLocation?.GetAdjustedItemBuyPrice(pi.ItemPrefab, considerDailySpecials: false) ?? 0); + } + else + { + undiscountedPriceBlock.TextGetter = () => GetCurrencyFormatted( + CurrentLocation?.GetAdjustedItemSellPrice(pi.ItemPrefab, considerRequestedGoods: false) ?? 0); + } + } + + if (itemFrame.FindChild("price", recursive: true) is GUITextBlock priceBlock) + { + if (buying) + { + priceBlock.TextGetter = () => GetCurrencyFormatted(CurrentLocation?.GetAdjustedItemBuyPrice(pi.ItemPrefab) ?? 0); + } + else + { + priceBlock.TextGetter = () => GetCurrencyFormatted(CurrentLocation?.GetAdjustedItemSellPrice(pi.ItemPrefab) ?? 0); + } + } + } + public void RefreshItemsToSell() { itemsToSell.Clear(); @@ -1188,14 +1220,6 @@ namespace Barotrauma }; priceBlock.Color *= (forceDisable ? 0.5f : 1.0f); priceBlock.CalculateHeightFromText(); - if (isSellingRelatedList) - { - priceBlock.TextGetter = () => GetCurrencyFormatted(CurrentLocation?.GetAdjustedItemSellPrice(pi.ItemPrefab, priceInfo: priceInfo) ?? 0); - } - else - { - priceBlock.TextGetter = () => GetCurrencyFormatted(CurrentLocation?.GetAdjustedItemBuyPrice(pi.ItemPrefab, priceInfo: priceInfo) ?? 0); - } if (locationHasDealOnItem) { var undiscounterPriceBlock = new GUITextBlock( @@ -1209,17 +1233,8 @@ namespace Barotrauma TextColor = priceBlock.TextColor, UserData = "undiscountedprice" }; - if (isSellingRelatedList) - { - undiscounterPriceBlock.TextGetter = () => GetCurrencyFormatted( - CurrentLocation?.GetAdjustedItemSellPrice(pi.ItemPrefab, priceInfo: priceInfo, considerRequestedGoods: false) ?? 0); - } - else - { - undiscounterPriceBlock.TextGetter = () => GetCurrencyFormatted( - CurrentLocation?.GetAdjustedItemBuyPrice(pi.ItemPrefab, priceInfo: priceInfo, considerDailySpecials: false) ?? 0); - } } + SetPriceGetters(frame, !isSellingRelatedList); if (isParentOnLeftSideOfInterface) { @@ -1268,7 +1283,8 @@ namespace Barotrauma // Add items on the sub(s) Submarine.MainSub?.GetItems(true) .Where(i => i.Components.All(c => !(c is Holdable h) || !h.Attachable || !h.Attached) && - i.Components.All(c => !(c is Wire w) || w.Connections.All(c => c == null))) + i.Components.All(c => !(c is Wire w) || w.Connections.All(c => c == null)) && + ItemAndAllContainersInteractable(i)) .ForEach(i => AddToOwnedItems(i.Prefab)); // Add items in character inventories @@ -1286,6 +1302,16 @@ namespace Barotrauma ownedItemsUpdateTimer = 0.0f; + static bool ItemAndAllContainersInteractable(Item item) + { + do + { + if (!item.IsPlayerTeamInteractable) { return false; } + item = item.Container; + } while (item != null); + return true; + } + void AddToOwnedItems(ItemPrefab itemPrefab, int amount = 1) { if (OwnedItems.ContainsKey(itemPrefab)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs index 836c62458..838f4c534 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -60,6 +60,7 @@ namespace Barotrauma public GUITextBlock submarineFee; public GUIButton selectSubmarineButton; public GUITextBlock middleTextBlock; + public GUIButton previewButton; } public SubmarineSelection(bool transfer, Action closeAction, RectTransform parent) @@ -191,6 +192,12 @@ namespace Barotrauma submarineDisplayElement.submarineClass = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding + (int)GUI.Font.MeasureString(submarineDisplayElement.submarineName.Text).Y) }, string.Empty, textAlignment: Alignment.Center); submarineDisplayElement.submarineFee = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.BottomCenter, Pivot.BottomCenter) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding) }, string.Empty, textAlignment: Alignment.Center, font: GUI.SubHeadingFont); submarineDisplayElement.selectSubmarineButton = new GUIButton(new RectTransform(Vector2.One, submarineDisplayElement.background.RectTransform), style: null); + submarineDisplayElement.previewButton = new GUIButton(new RectTransform(Vector2.One * 0.12f, submarineDisplayElement.background.RectTransform, anchor: Anchor.BottomRight, pivot: Pivot.BottomRight, scaleBasis: ScaleBasis.BothHeight) { AbsoluteOffset = new Point((int)(0.03f * background.Rect.Height)) }, style: "ExpandButton") + { + Color = Color.White, + HoverColor = Color.White, + PressedColor = Color.White + }; submarineDisplays[i] = submarineDisplayElement; } @@ -299,6 +306,7 @@ namespace Barotrauma submarineDisplays[i].selectSubmarineButton.OnClicked = null; submarineDisplays[i].displayedSubmarine = null; submarineDisplays[i].middleTextBlock.AutoDraw = false; + submarineDisplays[i].previewButton.Visible = false; } else { @@ -369,6 +377,13 @@ namespace Barotrauma { SelectSubmarine(subToDisplay, submarineDisplays[i].background.Rect); } + + submarineDisplays[i].previewButton.Visible = true; + submarineDisplays[i].previewButton.OnClicked = (btn, obj) => + { + SubmarinePreview.Create(subToDisplay); + return false; + }; } submarineIndex++; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 2e159ce12..3b0679933 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -5,6 +5,7 @@ using Microsoft.Xna.Framework.Graphics; using System.Linq; using Barotrauma.Networking; using System.Globalization; +using Barotrauma.Extensions; namespace Barotrauma { @@ -17,7 +18,7 @@ namespace Barotrauma private static UISprite spectateIcon, disconnectedIcon; private static Sprite ownerIcon, moderatorIcon; - private enum InfoFrameTab { Crew, Mission, MyCharacter, Traitor }; + private enum InfoFrameTab { Crew, Mission, Reputation, MyCharacter, Traitor, Submarine }; private static InfoFrameTab selectedTab; private GUIFrame infoFrame, contentFrame; @@ -40,11 +41,11 @@ namespace Barotrauma private const ushort mediumPingThreshold = 200; private ushort currentPing; - private Client client; - private Character character; - private bool hasCharacter; - private GUITextBlock textBlock; - private GUIFrame frame; + private readonly Client client; + private readonly Character character; + private readonly bool hasCharacter; + private readonly GUITextBlock textBlock; + private readonly GUIFrame frame; public LinkedGUI(Client client, GUIFrame frame, bool hasCharacter, GUITextBlock textBlock) { @@ -180,60 +181,79 @@ namespace Barotrauma infoFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null); new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, infoFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); + //this used to be a switch expression but i changed it because it killed enc :( + Vector2 contentFrameSize; switch (selectedTab) { - case InfoFrameTab.Crew: - case InfoFrameTab.Mission: - case InfoFrameTab.Traitor: - default: - contentFrame = new GUIFrame(new RectTransform(new Vector2(0.33f, 0.667f), infoFrame.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { /*MinSize = new Point(width, height),*/ RelativeOffset = new Vector2(0.025f, 0.12f) }); - break; case InfoFrameTab.MyCharacter: - contentFrame = new GUIFrame(new RectTransform(new Vector2(0.33f, 0.5f), infoFrame.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { /*MinSize = new Point(width, height),*/ RelativeOffset = new Vector2(0.025f, 0.12f) }); + contentFrameSize = new Vector2(0.45f, 0.5f); + break; + default: + contentFrameSize = new Vector2(0.45f, 0.667f); break; } + contentFrame = new GUIFrame(new RectTransform(contentFrameSize, infoFrame.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.12f) }); - var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.958f, 0.943f), contentFrame.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { AbsoluteOffset = new Point(0, GUI.IntScale(17.5f)) }, style: null); - var buttonArea = new GUILayoutGroup(new RectTransform(new Point(innerFrame.Rect.Width, GUI.IntScale(25f)), innerFrame.RectTransform) { AbsoluteOffset = new Point(2, 0) }, isHorizontal: true) + var horizontalLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.958f, 0.943f), contentFrame.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { AbsoluteOffset = new Point(0, GUI.IntScale(25f)) }, isHorizontal: true) { RelativeSpacing = 0.01f }; - infoFrameHolder = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.926f), innerFrame.RectTransform, Anchor.BottomCenter, Pivot.BottomCenter), style: null); - - var crewButton = new GUIButton(new RectTransform(new Vector2(0.245f, 1.0f), buttonArea.RectTransform), TextManager.Get("Crew"), style: "GUITabButton") + var buttonArea = new GUILayoutGroup(new RectTransform(new Vector2(0.07f, 1f), parent: horizontalLayoutGroup.RectTransform), isHorizontal: false) { - UserData = InfoFrameTab.Crew, - OnClicked = SelectInfoFrameTab + AbsoluteSpacing = GUI.IntScale(5f) }; - tabButtons.Add(crewButton); - - var missionButton = new GUIButton(new RectTransform(new Vector2(0.245f, 1.0f), buttonArea.RectTransform), TextManager.Get("Mission"), style: "GUITabButton") + var innerLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.92f, 1f), horizontalLayoutGroup.RectTransform)) { - UserData = InfoFrameTab.Mission, - OnClicked = SelectInfoFrameTab + RelativeSpacing = 0.01f, + Stretch = true }; - tabButtons.Add(missionButton); - bool isTraitor = GameMain.Client?.Character?.IsTraitor ?? false; - if (isTraitor && GameMain.Client.TraitorMission != null) + float absoluteSpacing = innerLayoutGroup.RelativeSpacing * innerLayoutGroup.Rect.Height; + int multiplier = GameMain.GameSession?.GameMode is CampaignMode ? 2 : 1; + int infoFrameHolderHeight = Math.Min((int)(0.97f * innerLayoutGroup.Rect.Height), (int)(innerLayoutGroup.Rect.Height - multiplier * (GUI.IntScale(15f) + absoluteSpacing))); + infoFrameHolder = new GUIFrame(new RectTransform(new Point(innerLayoutGroup.Rect.Width, infoFrameHolderHeight), parent: innerLayoutGroup.RectTransform), style: null); + + GUIButton createTabButton(InfoFrameTab tab, string textTag) { - var traitorButton = new GUIButton(new RectTransform(new Vector2(0.245f, 1.0f), buttonArea.RectTransform), TextManager.Get("tabmenu.traitor"), style: "GUITabButton") + var newButton = new GUIButton(new RectTransform(Vector2.One, buttonArea.RectTransform, scaleBasis: ScaleBasis.BothWidth), style: $"InfoFrameTabButton.{tab}") { - UserData = InfoFrameTab.Traitor, + UserData = tab, + ToolTip = TextManager.Get(textTag), OnClicked = SelectInfoFrameTab }; - tabButtons.Add(traitorButton); + tabButtons.Add(newButton); + return newButton; } + var crewButton = createTabButton(InfoFrameTab.Crew, "crew"); + + var missionButton = createTabButton(InfoFrameTab.Mission, "mission"); + + if (GameMain.GameSession?.GameMode is CampaignMode campaignMode) + { + var reputationButton = createTabButton(InfoFrameTab.Reputation, "reputation"); + + var balanceFrame = new GUIFrame(new RectTransform(new Point(innerLayoutGroup.Rect.Width, innerLayoutGroup.Rect.Height - infoFrameHolderHeight), parent: innerLayoutGroup.RectTransform), style: "InnerFrame"); + new GUITextBlock(new RectTransform(Vector2.One, balanceFrame.RectTransform), "", textAlignment: Alignment.Right, parseRichText: true) + { + TextGetter = () => TextManager.GetWithVariable("campaignmoney", "[money]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", campaignMode.Money)) + }; + } + else + { + bool isTraitor = GameMain.Client?.Character?.IsTraitor ?? false; + if (isTraitor && GameMain.Client.TraitorMission != null) + { + var traitorButton = createTabButton(InfoFrameTab.Traitor, "tabmenu.traitor"); + } + } + + var submarineButton = createTabButton(InfoFrameTab.Submarine, "submarine"); + if (GameMain.NetworkMember != null) { - var myCharacterButton = new GUIButton(new RectTransform(new Vector2(0.245f, 1.0f), buttonArea.RectTransform), TextManager.Get("tabmenu.character"), style: "GUITabButton") - { - UserData = InfoFrameTab.MyCharacter, - OnClicked = SelectInfoFrameTab - }; - tabButtons.Add(myCharacterButton); + var myCharacterButton = createTabButton(InfoFrameTab.MyCharacter, "tabmenu.character"); } } @@ -252,6 +272,14 @@ namespace Barotrauma case InfoFrameTab.Mission: CreateMissionInfo(infoFrameHolder); break; + case InfoFrameTab.Reputation: + if (GameMain.GameSession.RoundSummary != null && GameMain.GameSession.GameMode is CampaignMode campaignMode) + { + infoFrameHolder.ClearChildren(); + GUIFrame reputationFrame = new GUIFrame(new RectTransform(Vector2.One, infoFrameHolder.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox"); + GameMain.GameSession.RoundSummary.CreateReputationInfoPanel(reputationFrame, campaignMode); + } + break; case InfoFrameTab.Traitor: TraitorMissionPrefab traitorMission = GameMain.Client.TraitorMission; Character traitor = GameMain.Client.Character; @@ -262,6 +290,9 @@ namespace Barotrauma if (GameMain.NetworkMember == null) { return false; } GameMain.NetLobbyScreen.CreatePlayerFrame(infoFrameHolder); break; + case InfoFrameTab.Submarine: + CreateSubmarineInfo(infoFrameHolder, Submarine.MainSub); + break; } return true; @@ -356,6 +387,21 @@ namespace Barotrauma crewFrame.RectTransform.AbsoluteOffset = new Point(0, (int)(headerFrames[0].Rect.Height * headerFrames.Length) - (teamIDs.Count > 1 ? GUI.IntScale(10f) : 0)); + float totalRelativeHeight = 0.0f; + if (teamIDs.Count > 1) { totalRelativeHeight += teamIDs.Count * nameHeight; } + headerFrames.ForEach(f => totalRelativeHeight += f.RectTransform.RelativeSize.Y); + crewListArray.ForEach(f => totalRelativeHeight += f.RectTransform.RelativeSize.Y); + if (totalRelativeHeight > 1.0f) + { + float heightOverflow = totalRelativeHeight - 1.0f; + float heightToReduce = heightOverflow / crewListArray.Length; + crewListArray.ForEach(l => + { + l.RectTransform.Resize(l.RectTransform.RelativeSize - new Vector2(0.0f, heightToReduce)); + l.UpdateDimensions(); + }); + } + if (GameMain.IsMultiplayer) { CreateMultiPlayerList(false); @@ -676,7 +722,7 @@ namespace Barotrauma GUIComponent existingPreview = infoFrameHolder.FindChild("SelectedCharacter"); if (existingPreview != null) infoFrameHolder.RemoveChild(existingPreview); - GUIFrame background = new GUIFrame(new RectTransform(new Vector2(0.543f, 0.717f), infoFrameHolder.RectTransform, Anchor.TopLeft, Pivot.TopRight) { RelativeOffset = new Vector2(-0.061f, 0) }) + GUIFrame background = new GUIFrame(new RectTransform(new Vector2(0.543f, 0.717f), infoFrameHolder.RectTransform, Anchor.TopLeft, Pivot.TopRight) { RelativeOffset = new Vector2(-0.145f, 0) }) { UserData = "SelectedCharacter" }; @@ -831,12 +877,24 @@ namespace Barotrauma if (logList != null) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), logList.Content.RectTransform), line, wrap: true, font: GUI.SmallFont) + var textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), logList.Content.RectTransform), line, wrap: true, font: GUI.SmallFont, parseRichText: true) { TextColor = textColor, CanBeFocused = false, UserData = line - }.CalculateHeightFromText(); + }; + textBlock.CalculateHeightFromText(); + if (textBlock.HasColorHighlight) + { + foreach (var data in textBlock.RichTextData) + { + textBlock.ClickableAreas.Add(new GUITextBlock.ClickableArea() + { + Data = data, + OnClick = GameMain.NetLobbyScreen.SelectPlayer + }); + } + } } } @@ -857,54 +915,109 @@ namespace Barotrauma int locationInfoYOffset = locationNameText.Rect.Height + locationTypeText.Rect.Height + padding * 2; - GUIFrame missionDescriptionHolder; + GUIListBox missionList; if (hasPortrait) { - GUIFrame missionImageHolder = new GUIFrame(new RectTransform(new Point(contentWidth, (int)(missionFrame.Rect.Height * 0.588f)), missionFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, locationInfoYOffset) }); + GUIFrame portraitHolder = new GUIFrame(new RectTransform(new Point(contentWidth, (int)(missionFrame.Rect.Height * 0.588f)), missionFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, locationInfoYOffset) }); float portraitAspectRatio = portrait.SourceRect.Width / portrait.SourceRect.Height; - GUIImage portraitImage = new GUIImage(new RectTransform(new Vector2(1.0f, 1f), missionImageHolder.RectTransform), portrait, scaleToFit: true); - missionImageHolder.RectTransform.NonScaledSize = new Point(portraitImage.Rect.Size.X, (int)(portraitImage.Rect.Size.X / portraitAspectRatio)); - missionDescriptionHolder = new GUIFrame(new RectTransform(new Point(contentWidth, 0), missionFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, missionImageHolder.RectTransform.AbsoluteOffset.Y + missionImageHolder.Rect.Height + padding) }, style: null); + GUIImage portraitImage = new GUIImage(new RectTransform(new Vector2(1.0f, 1f), portraitHolder.RectTransform), portrait, scaleToFit: true); + portraitHolder.RectTransform.NonScaledSize = new Point(portraitImage.Rect.Size.X, (int)(portraitImage.Rect.Size.X / portraitAspectRatio)); + + missionList = new GUIListBox(new RectTransform(new Point(contentWidth, missionFrame.Rect.Bottom - portraitHolder.Rect.Bottom - padding), missionFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, portraitHolder.RectTransform.AbsoluteOffset.Y + portraitHolder.Rect.Height + padding) }); } else { - missionDescriptionHolder = new GUIFrame(new RectTransform(new Point(contentWidth, 0), missionFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, locationInfoYOffset) }, style: null); - } + missionList = new GUIListBox(new RectTransform(new Point(contentWidth, missionFrame.Rect.Height - locationInfoYOffset - padding), missionFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, locationInfoYOffset) }); + } + missionList.ContentBackground.Color = Color.Transparent; + missionList.Spacing = GUI.IntScale(15); - Mission mission = GameMain.GameSession?.Mission; - if (mission != null) + if (GameMain.GameSession?.Missions != null) { - GUILayoutGroup missionTextGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.744f, 0f), missionDescriptionHolder.RectTransform, Anchor.CenterLeft) { RelativeOffset = new Vector2(0.225f, 0f) }, false, childAnchor: Anchor.TopLeft); - - string missionNameString = ToolBox.WrapText(mission.Name, missionTextGroup.Rect.Width, GUI.LargeFont); - string missionDescriptionString = ToolBox.WrapText(mission.Description, missionTextGroup.Rect.Width, GUI.Font); - string rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", mission.Reward)); - string missionRewardString = ToolBox.WrapText(TextManager.GetWithVariable("MissionReward", "[reward]", rewardText), missionTextGroup.Rect.Width, GUI.Font); - - Vector2 missionNameSize = GUI.LargeFont.MeasureString(missionNameString); - Vector2 missionDescriptionSize = GUI.Font.MeasureString(missionDescriptionString); - Vector2 missionRewardSize = GUI.Font.MeasureString(missionRewardString); - - missionDescriptionHolder.RectTransform.NonScaledSize = new Point(missionDescriptionHolder.RectTransform.NonScaledSize.X, (int)(missionNameSize.Y + missionDescriptionSize.Y + missionRewardSize.Y)); - missionTextGroup.RectTransform.NonScaledSize = new Point(missionTextGroup.RectTransform.NonScaledSize.X, missionDescriptionHolder.RectTransform.NonScaledSize.Y); - - if (mission.Prefab.Icon != null) + foreach (Mission mission in GameMain.GameSession.Missions) { - float iconAspectRatio = mission.Prefab.Icon.SourceRect.Width / mission.Prefab.Icon.SourceRect.Height; - int iconWidth = (int)(0.225f * missionDescriptionHolder.RectTransform.NonScaledSize.X); - int iconHeight = Math.Max(missionTextGroup.RectTransform.NonScaledSize.Y, (int)(iconWidth * iconAspectRatio)); - Point iconSize = new Point(iconWidth, iconHeight); + GUIFrame missionDescriptionHolder = new GUIFrame(new RectTransform(Vector2.One, missionList.Content.RectTransform), style: null); + GUILayoutGroup missionTextGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.744f, 0f), missionDescriptionHolder.RectTransform, Anchor.CenterLeft) { RelativeOffset = new Vector2(0.225f, 0f) }, false, childAnchor: Anchor.TopLeft) + { + AbsoluteSpacing = GUI.IntScale(5) + }; + string descriptionText = mission.Description; + foreach (string missionMessage in mission.ShownMessages) + { + descriptionText += "\n\n" + missionMessage; + } + string rewardText = mission.GetMissionRewardText(); + string reputationText = mission.GetReputationRewardText(mission.Locations[0]); - new GUIImage(new RectTransform(iconSize, missionDescriptionHolder.RectTransform), mission.Prefab.Icon, null, true) { Color = mission.Prefab.IconColor }; + var missionNameRichTextData = RichTextData.GetRichTextData(mission.Name, out string missionNameString); + var missionRewardRichTextData = RichTextData.GetRichTextData(rewardText, out string missionRewardString); + var missionReputationRichTextData = RichTextData.GetRichTextData(reputationText, out string missionReputationString); + var missionDescriptionRichTextData = RichTextData.GetRichTextData(descriptionText, out string missionDescriptionString); + + missionNameString = ToolBox.WrapText(missionNameString, missionTextGroup.Rect.Width, GUI.LargeFont); + missionRewardString = ToolBox.WrapText(missionRewardString, missionTextGroup.Rect.Width, GUI.Font); + missionReputationString = ToolBox.WrapText(missionReputationString, missionTextGroup.Rect.Width, GUI.Font); + missionDescriptionString = ToolBox.WrapText(missionDescriptionString, missionTextGroup.Rect.Width, GUI.Font); + + Vector2 missionNameSize = GUI.LargeFont.MeasureString(missionNameString); + Vector2 missionDescriptionSize = GUI.Font.MeasureString(missionDescriptionString); + Vector2 missionRewardSize = GUI.Font.MeasureString(missionRewardString); + Vector2 missionReputationSize = GUI.Font.MeasureString(missionReputationString); + + float ySize = missionNameSize.Y + missionDescriptionSize.Y + missionRewardSize.Y + missionReputationSize.Y + missionTextGroup.AbsoluteSpacing * 4; + bool displayDifficulty = mission.Difficulty.HasValue; + if (displayDifficulty) { ySize += missionRewardSize.Y; } + + missionDescriptionHolder.RectTransform.NonScaledSize = new Point(missionDescriptionHolder.RectTransform.NonScaledSize.X, (int)ySize); + missionTextGroup.RectTransform.NonScaledSize = new Point(missionTextGroup.RectTransform.NonScaledSize.X, missionDescriptionHolder.RectTransform.NonScaledSize.Y); + + if (mission.Prefab.Icon != null) + { + float iconAspectRatio = mission.Prefab.Icon.SourceRect.Width / mission.Prefab.Icon.SourceRect.Height; + int iconWidth = (int)(0.225f * missionDescriptionHolder.RectTransform.NonScaledSize.X); + int iconHeight = Math.Max(missionTextGroup.RectTransform.NonScaledSize.Y, (int)(iconWidth * iconAspectRatio)); + Point iconSize = new Point(iconWidth, iconHeight); + + new GUIImage(new RectTransform(iconSize, missionDescriptionHolder.RectTransform), mission.Prefab.Icon, null, true) + { + Color = mission.Prefab.IconColor, + HoverColor = mission.Prefab.IconColor, + SelectedColor = mission.Prefab.IconColor, + CanBeFocused = false + }; + } + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionNameRichTextData, missionNameString, font: GUI.LargeFont); + GUILayoutGroup difficultyIndicatorGroup = null; + if (displayDifficulty) + { + difficultyIndicatorGroup = new GUILayoutGroup(new RectTransform(new Point(missionTextGroup.Rect.Width, (int)missionRewardSize.Y), parent: missionTextGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + AbsoluteSpacing = 1 + }; + var difficultyColor = mission.GetDifficultyColor(); + for (int i = 0; i < mission.Difficulty.Value; i++) + { + new GUIImage(new RectTransform(Vector2.One, difficultyIndicatorGroup.RectTransform, scaleBasis: ScaleBasis.Smallest), "DifficultyIndicator", scaleToFit: true) + { + CanBeFocused = false, + Color = difficultyColor + }; + } + } + var rewardTextBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionRewardRichTextData, missionRewardString); + if (difficultyIndicatorGroup != null) + { + difficultyIndicatorGroup.RectTransform.Resize(new Point((int)(difficultyIndicatorGroup.Rect.Width - rewardTextBlock.Padding.X - rewardTextBlock.Padding.Z), difficultyIndicatorGroup.Rect.Height)); + difficultyIndicatorGroup.RectTransform.AbsoluteOffset = new Point((int)rewardTextBlock.Padding.X, 0); + } + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionReputationRichTextData, missionReputationString); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionDescriptionRichTextData, missionDescriptionString); } - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionNameString, font: GUI.LargeFont); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionRewardString); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionDescriptionString); } else { - GUILayoutGroup missionTextGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0f), missionDescriptionHolder.RectTransform, Anchor.CenterLeft), false, childAnchor: Anchor.TopLeft); + GUILayoutGroup missionTextGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0f), missionList.RectTransform, Anchor.CenterLeft), false, childAnchor: Anchor.TopLeft); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), TextManager.Get("NoMission"), font: GUI.LargeFont); } } @@ -938,5 +1051,93 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionNameString, font: GUI.LargeFont); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionDescriptionString); } + + private void CreateSubmarineInfo(GUIFrame infoFrame, Submarine sub) + { + GUIFrame subInfoFrame = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox"); + GUIFrame paddedFrame = new GUIFrame(new RectTransform(Vector2.One * 0.97f, subInfoFrame.RectTransform, Anchor.Center), style: null); + + var previewButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.43f), paddedFrame.RectTransform), style: null) + { + OnClicked = (btn, obj) => { SubmarinePreview.Create(sub.Info); return false; }, + }; + + var previewImage = sub.Info.PreviewImage ?? SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name.Equals(sub.Info.Name, StringComparison.OrdinalIgnoreCase))?.PreviewImage; + if (previewImage == null) + { + new GUITextBlock(new RectTransform(Vector2.One, previewButton.RectTransform), TextManager.Get("SubPreviewImageNotFound")); + } + else + { + var submarinePreviewBackground = new GUIFrame(new RectTransform(Vector2.One, previewButton.RectTransform), style: null) + { + Color = Color.Black, + HoverColor = Color.Black, + SelectedColor = Color.Black, + PressedColor = Color.Black, + CanBeFocused = false, + }; + new GUIImage(new RectTransform(new Vector2(0.98f), submarinePreviewBackground.RectTransform, Anchor.Center), previewImage, scaleToFit: true) { CanBeFocused = false }; + new GUIFrame(new RectTransform(Vector2.One, submarinePreviewBackground.RectTransform), "InnerGlow", color: Color.Black) { CanBeFocused = false }; + } + + new GUIFrame(new RectTransform(Vector2.One * 0.12f, previewButton.RectTransform, anchor: Anchor.BottomRight, pivot: Pivot.BottomRight, scaleBasis: ScaleBasis.BothHeight) + { + AbsoluteOffset = new Point((int)(0.03f * previewButton.Rect.Height)) + }, + "ExpandButton", Color.White) + { + Color = Color.White, + HoverColor = Color.White, + PressedColor = Color.White + }; + + var subInfoTextLayout = new GUILayoutGroup(new RectTransform(Vector2.One, paddedFrame.RectTransform)); + + string className = !sub.Info.HasTag(SubmarineTag.Shuttle) ? TextManager.Get($"submarineclass.{sub.Info.SubmarineClass}") : TextManager.Get("shuttle"); + + int nameHeight = (int)GUI.LargeFont.MeasureString(sub.Info.DisplayName, true).Y; + int classHeight = (int)GUI.SubHeadingFont.MeasureString(className).Y; + + var submarineNameText = new GUITextBlock(new RectTransform(new Point(subInfoTextLayout.Rect.Width, nameHeight + HUDLayoutSettings.Padding / 2), subInfoTextLayout.RectTransform), sub.Info.DisplayName, textAlignment: Alignment.CenterLeft, font: GUI.LargeFont) { CanBeFocused = false }; + submarineNameText.RectTransform.MinSize = new Point(0, (int)submarineNameText.TextSize.Y); + var submarineClassText = new GUITextBlock(new RectTransform(new Point(subInfoTextLayout.Rect.Width, classHeight), subInfoTextLayout.RectTransform), className, textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont) { CanBeFocused = false }; + submarineClassText.RectTransform.MinSize = new Point(0, (int)submarineClassText.TextSize.Y); + + if (GameMain.GameSession?.GameMode is CampaignMode campaign) + { + GUILayoutGroup headerLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.09f), paddedFrame.RectTransform) { RelativeOffset = new Vector2(0f, 0.43f) }, isHorizontal: true) { Stretch = true }; + GUIImage headerIcon = new GUIImage(new RectTransform(Vector2.One, headerLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "SubmarineIcon"); + new GUITextBlock(new RectTransform(Vector2.One, headerLayout.RectTransform), TextManager.Get("uicategory.upgrades"), font: GUI.LargeFont); + + var upgradeRootLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.48f), paddedFrame.RectTransform, Anchor.BottomLeft, Pivot.BottomLeft), isHorizontal: true); + + var upgradeCategoryPanel = UpgradeStore.CreateUpgradeCategoryList(new RectTransform(new Vector2(0.4f, 1f), upgradeRootLayout.RectTransform)); + upgradeCategoryPanel.HideChildrenOutsideFrame = true; + UpgradeStore.UpdateCategoryList(upgradeCategoryPanel, campaign, sub, UpgradeStore.GetApplicableCategories(sub).ToArray()); + GUIComponent[] toRemove = upgradeCategoryPanel.Content.FindChildren(c => !c.Enabled).ToArray(); + toRemove.ForEach(c => upgradeCategoryPanel.RemoveChild(c)); + + var upgradePanel = new GUIListBox(new RectTransform(new Vector2(0.6f, 1f), upgradeRootLayout.RectTransform)); + upgradeCategoryPanel.OnSelected = (component, userData) => + { + upgradePanel.ClearChildren(); + if (userData is UpgradeStore.CategoryData categoryData && Submarine.MainSub != null) + { + foreach (UpgradePrefab prefab in categoryData.Prefabs) + { + var frame = UpgradeStore.CreateUpgradeFrame(prefab, categoryData.Category, campaign, new RectTransform(new Vector2(1f, 0.3f), upgradePanel.Content.RectTransform), addBuyButton: false); + UpgradeStore.UpdateUpgradeEntry(frame, prefab, categoryData.Category, campaign); + } + } + return true; + }; + } + else + { + var specsListBox = new GUIListBox(new RectTransform(new Vector2(1f, 0.57f), paddedFrame.RectTransform, Anchor.BottomLeft, Pivot.BottomLeft)); + sub.Info.CreateSpecsWindow(specsListBox, GUI.Font, includeTitle: false, includeClass: false, includeDescription: true); + } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index c64af21de..3b3d5ceff 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -15,7 +15,7 @@ namespace Barotrauma internal class UpgradeStore { - private readonly struct CategoryData + public readonly struct CategoryData { public readonly UpgradeCategory Category; public readonly List Prefabs; @@ -125,7 +125,7 @@ namespace Barotrauma { if (component.UserData is CategoryData data) { - UpdateUpgradeEntry(component, data.SinglePrefab, data.Category); + UpdateUpgradeEntry(component, data.SinglePrefab, data.Category, Campaign); } } } @@ -133,29 +133,35 @@ namespace Barotrauma // update the small indicator icons on the list if (currentStoreLayout?.Parent != null) { - foreach (GUIComponent component in currentStoreLayout.Content.Children) - { - if (!(component.UserData is CategoryData data)) { continue; } - if (component.FindChild("indicators", true) is { } indicators) - { - UpdateCategoryIndicators(indicators, component, data.Prefabs, data.Category); - } - } + UpdateCategoryList(currentStoreLayout, Campaign, drawnSubmarine, applicableCategories); + } + } - // reset the order first - foreach (UpgradeCategory category in UpgradeCategory.Categories) + //TODO: move this somewhere else + public static void UpdateCategoryList(GUIListBox categoryList, CampaignMode campaign, Submarine drawnSubmarine, IEnumerable applicableCategories) + { + foreach (GUIComponent component in categoryList.Content.Children) + { + if (!(component.UserData is CategoryData data)) { continue; } + if (component.FindChild("indicators", true) is { } indicators) { - GUIComponent component = currentStoreLayout.Content.FindChild(c => c.UserData is CategoryData categoryData && categoryData.Category == category); - component?.SetAsLastChild(); + UpdateCategoryIndicators(indicators, component, data.Prefabs, data.Category, campaign, drawnSubmarine, applicableCategories); } + } - // send the disabled components to the bottom - List lastChilds = currentStoreLayout.Content.Children.Where(component => !component.Enabled).ToList(); + // reset the order first + foreach (UpgradeCategory category in UpgradeCategory.Categories) + { + GUIComponent component = categoryList.Content.FindChild(c => c.UserData is CategoryData categoryData && categoryData.Category == category); + component?.SetAsLastChild(); + } - foreach (var lastChild in lastChilds) - { - lastChild.SetAsLastChild(); - } + // send the disabled components to the bottom + List lastChilds = categoryList.Content.Children.Where(component => !component.Enabled).ToList(); + + foreach (var lastChild in lastChilds) + { + lastChild.SetAsLastChild(); } } @@ -511,9 +517,10 @@ namespace Barotrauma } } - private void CreateUpgradeTab() + //TODO: put this somewhere else + public static GUIListBox CreateUpgradeCategoryList(RectTransform rectTransform) { - currentStoreLayout = new GUIListBox(rectT(1.0f, 1.5f, storeLayout), style: null) + var upgradeCategoryList = new GUIListBox(rectTransform, style: null) { AutoHideScrollBar = false, ScrollBarVisible = false, @@ -548,7 +555,7 @@ namespace Barotrauma foreach (var (category, prefabs) in upgrades) { - var frameChild = new GUIFrame(rectT(1, 0.15f, currentStoreLayout.Content), style: "UpgradeUIFrame") + var frameChild = new GUIFrame(rectT(1, 0.15f, upgradeCategoryList.Content), style: "UpgradeUIFrame") { UserData = new CategoryData(category, prefabs), GlowOnSelect = true @@ -565,9 +572,9 @@ namespace Barotrauma * |-----------------------------|--------------------------| */ GUILayoutGroup contentLayout = new GUILayoutGroup(rectT(0.9f, 0.85f, frameChild, Anchor.Center)); - var itemCategoryLabel = new GUITextBlock(rectT(1, 1, contentLayout), category.Name, font: GUI.SubHeadingFont) { CanBeFocused = false }; - GUILayoutGroup indicatorLayout = new GUILayoutGroup(rectT(0.5f, 0.25f, contentLayout, Anchor.BottomRight), isHorizontal: true, childAnchor: Anchor.TopRight) { UserData = "indicators", IgnoreLayoutGroups = true, RelativeSpacing = 0.01f }; - + var itemCategoryLabel = new GUITextBlock(rectT(1, 1, contentLayout), category.Name, font: GUI.SubHeadingFont) { CanBeFocused = false }; + GUILayoutGroup indicatorLayout = new GUILayoutGroup(rectT(0.5f, 0.25f, contentLayout, Anchor.BottomRight), isHorizontal: true, childAnchor: Anchor.TopRight) { UserData = "indicators", IgnoreLayoutGroups = true, RelativeSpacing = 0.01f }; + foreach (var prefab in prefabs) { GUIImage upgradeIndicator = new GUIImage(rectT(0.1f, 1f, indicatorLayout), style: "UpgradeIndicator", scaleToFit: true) { UserData = prefab, CanBeFocused = false }; @@ -582,6 +589,13 @@ namespace Barotrauma indicatorLayout.Recalculate(); } + return upgradeCategoryList; + } + + private void CreateUpgradeTab() + { + currentStoreLayout = CreateUpgradeCategoryList(rectT(1.0f, 1.5f, storeLayout)); + selectedUpgradeCategoryLayout = new GUIFrame(rectT(GUI.IsFourByThree() ? 0.3f : 0.25f, 1, mainStoreLayout), style: null) { CanBeFocused = false }; RefreshUpgradeList(); @@ -637,7 +651,7 @@ namespace Barotrauma } } - private void CreateUpgradeEntry(UpgradePrefab prefab, UpgradeCategory category, GUIComponent parent, List itemsOnSubmarine) + public static GUIFrame CreateUpgradeFrame(UpgradePrefab prefab, UpgradeCategory category, CampaignMode campaign, RectTransform rectTransform, bool addBuyButton = true) { /* UPGRADE PREFAB ENTRY * |------------------------------------------------------------------| @@ -648,8 +662,8 @@ namespace Barotrauma * | | progress bar | x / y | | * |------------------------------------------------------------------| */ - GUIFrame prefabFrame = new GUIFrame(rectT(1f, 0.25f, parent), style: "ListBoxElement") { SelectedColor = Color.Transparent, UserData = new CategoryData(category, prefab) }; - GUILayoutGroup prefabLayout = new GUILayoutGroup(rectT(0.98f,0.95f, prefabFrame, Anchor.Center), isHorizontal: true); + GUIFrame prefabFrame = new GUIFrame(rectTransform, style: "ListBoxElement") { SelectedColor = Color.Transparent, UserData = new CategoryData(category, prefab) }; + GUILayoutGroup prefabLayout = new GUILayoutGroup(rectT(0.98f, 0.95f, prefabFrame, Anchor.Center), isHorizontal: true) { Stretch = true }; GUILayoutGroup imageLayout = new GUILayoutGroup(rectT(new Point(prefabLayout.Rect.Height, prefabLayout.Rect.Height), prefabLayout), childAnchor: Anchor.Center); var icon = new GUIImage(rectT(0.9f, 0.9f, imageLayout), prefab.Sprite, scaleToFit: true) { CanBeFocused = false }; GUILayoutGroup textLayout = new GUILayoutGroup(rectT(0.8f - imageLayout.RectTransform.RelativeSize.X, 1, prefabLayout)); @@ -659,9 +673,13 @@ namespace Barotrauma GUILayoutGroup progressLayout = new GUILayoutGroup(rectT(1, 0.25f, textLayout), isHorizontal: true, childAnchor: Anchor.CenterLeft) { UserData = "progressbar" }; new GUIProgressBar(rectT(0.8f, 0.75f, progressLayout), 0.0f, GUI.Style.Orange); new GUITextBlock(rectT(0.2f, 1, progressLayout), string.Empty, font: GUI.SmallFont, textAlignment: Alignment.Center) { Padding = Vector4.Zero }; - GUILayoutGroup buyButtonLayout = new GUILayoutGroup(rectT(0.2f, 1, prefabLayout), childAnchor: Anchor.TopCenter) { UserData = "buybutton" }; - new GUITextBlock(rectT(1, 0.4f, buyButtonLayout), FormatCurrency(prefab.Price.GetBuyprice(Campaign.UpgradeManager.GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation)), textAlignment: Alignment.Center) { Padding = Vector4.Zero }; + GUILayoutGroup buyButtonLayout = null; + if (addBuyButton) + { + buyButtonLayout = new GUILayoutGroup(rectT(0.2f, 1, prefabLayout), childAnchor: Anchor.TopCenter) { UserData = "buybutton" }; + new GUITextBlock(rectT(1, 0.4f, buyButtonLayout), FormatCurrency(prefab.Price.GetBuyprice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation)), textAlignment: Alignment.Center) { Padding = Vector4.Zero }; var buyButton = new GUIButton(rectT(0.7f, 0.5f, buyButtonLayout), string.Empty, style: "UpgradeBuyButton") { Enabled = false }; + } description.CalculateHeightFromText(); // cut the description if it overflows and add a tooltip to it @@ -677,14 +695,33 @@ namespace Barotrauma } // Recalculate everything to prevent jumping - if (parent is GUILayoutGroup group) { group.Recalculate(); } + if (rectTransform.Parent.GUIComponent is GUILayoutGroup group) { group.Recalculate(); } descriptionLayout.Recalculate(); prefabLayout.Recalculate(); imageLayout.Recalculate(); textLayout.Recalculate(); progressLayout.Recalculate(); - buyButtonLayout.Recalculate(); + buyButtonLayout?.Recalculate(); + + return prefabFrame; + } + + private void CreateUpgradeEntry(UpgradePrefab prefab, UpgradeCategory category, GUIComponent parent, List itemsOnSubmarine) + { + GUIFrame prefabFrame = CreateUpgradeFrame(prefab, category, Campaign, rectT(1f, 0.25f, parent)); + var prefabLayout = prefabFrame.GetChild(); + GUILayoutGroup[] childLayouts = prefabLayout.GetAllChildren().ToArray(); + var imageLayout = childLayouts[0]; + var icon = imageLayout.GetChild(); + var textLayout = childLayouts[1]; + var name = textLayout.GetChild(); + GUILayoutGroup[] textChildLayouts = textLayout.GetAllChildren().ToArray(); + var descriptionLayout = textChildLayouts[0]; + var description = descriptionLayout.GetChild(); + var progressLayout = textChildLayouts[1]; + var buyButtonLayout = childLayouts[2]; + var buyButton = buyButtonLayout.GetChild(); if (!HasPermission || itemsOnSubmarine != null && !itemsOnSubmarine.Any(it => category.CanBeApplied(it, prefab))) { @@ -713,7 +750,7 @@ namespace Barotrauma return true; }; - UpdateUpgradeEntry(prefabFrame, prefab, category); + UpdateUpgradeEntry(prefabFrame, prefab, category, Campaign); } private void CreateItemTooltip(MapEntity entity) @@ -778,6 +815,18 @@ namespace Barotrauma static string CreateListEntry(string name, int level) => TextManager.GetWithVariables("upgradeuitooltip.upgradelistelement", new[] { "[upgradename]", "[level]" }, new[] { name, $"{level}" }); } + public static IEnumerable GetApplicableCategories(Submarine drawnSubmarine) + { + Item[] entitiesOnSub = drawnSubmarine.GetItems(true).Where(i => drawnSubmarine.IsEntityFoundOnThisSub(i, true)).ToArray(); + foreach (UpgradeCategory category in UpgradeCategory.Categories) + { + if (entitiesOnSub.Any(item => category.CanBeApplied(item, null))) + { + yield return category; + } + } + } + private void UpdateSubmarinePreview(float deltaTime, GUICustomComponent parent) { if (!parent.Children.Any() || Submarine.MainSub != null && Submarine.MainSub != drawnSubmarine || GameMain.GraphicsWidth != screenResolution.X || GameMain.GraphicsHeight != screenResolution.Y) @@ -789,16 +838,8 @@ namespace Barotrauma CreateSubmarinePreview(drawnSubmarine, parent); CreateHullBorderVerticies(drawnSubmarine, parent); - List entitiesOnSub = drawnSubmarine.GetItems(true).Where(i => drawnSubmarine.IsEntityFoundOnThisSub(i, true)).ToList(); applicableCategories.Clear(); - - foreach (UpgradeCategory category in UpgradeCategory.Categories) - { - if (entitiesOnSub.Any(item => category.CanBeApplied(item, null))) - { - applicableCategories.Add(category); - } - } + applicableCategories.AddRange(GetApplicableCategories(drawnSubmarine)); } screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); @@ -1002,9 +1043,9 @@ namespace Barotrauma } } - private void UpdateUpgradeEntry(GUIComponent prefabFrame, UpgradePrefab prefab, UpgradeCategory category) + public static void UpdateUpgradeEntry(GUIComponent prefabFrame, UpgradePrefab prefab, UpgradeCategory category, CampaignMode campaign) { - int currentLevel = Campaign.UpgradeManager.GetUpgradeLevel(prefab, category); + int currentLevel = campaign.UpgradeManager.GetUpgradeLevel(prefab, category); string progressText = TextManager.GetWithVariables("upgrades.progressformat", new[] { "[level]", "[maxlevel]" }, new[] { currentLevel.ToString(), prefab.MaxLevel.ToString() }); if (prefabFrame.FindChild("progressbar", true) is { } progressParent) @@ -1023,7 +1064,7 @@ namespace Barotrauma if (prefabFrame.FindChild("buybutton", true) is { } buttonParent) { GUITextBlock priceLabel = buttonParent.GetChild(); - int price = prefab.Price.GetBuyprice(Campaign.UpgradeManager.GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation); + int price = prefab.Price.GetBuyprice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation); if (priceLabel != null && !WaitForServerUpdate) { @@ -1038,7 +1079,7 @@ namespace Barotrauma if (button != null) { button.Enabled = currentLevel < prefab.MaxLevel; - if (WaitForServerUpdate || !HasPermission || price > Campaign.Money) + if (WaitForServerUpdate || !campaign.AllowedToManageCampaign() || price > campaign.Money) { button.Enabled = false; } @@ -1046,7 +1087,14 @@ namespace Barotrauma } } - private void UpdateCategoryIndicators(GUIComponent indicators, GUIComponent parent, List prefabs, UpgradeCategory category) + private static void UpdateCategoryIndicators( + GUIComponent indicators, + GUIComponent parent, + List prefabs, + UpgradeCategory category, + CampaignMode campaign, + Submarine drawnSubmarine, + IEnumerable applicableCategories) { // Disables the parent and only re-enables if the submarine contains valid items if (!category.IsWallUpgrade && drawnSubmarine != null) @@ -1078,13 +1126,13 @@ namespace Barotrauma GUIComponentStyle dimStyle = styles["upgradeindicatordim"]; GUIComponentStyle offStyle = styles["upgradeindicatoroff"]; - if (Campaign.UpgradeManager.GetUpgradeLevel(prefab, category) >= prefab.MaxLevel) + if (campaign.UpgradeManager.GetUpgradeLevel(prefab, category) >= prefab.MaxLevel) { // we check this to avoid flickering from re-applying the same style if (image.Style == onStyle) { continue; } image.ApplyStyle(onStyle); } - else if (Campaign.UpgradeManager.GetUpgradeLevel(prefab, category) > 0) + else if (campaign.UpgradeManager.GetUpgradeLevel(prefab, category) > 0) { if (image.Style == dimStyle) { continue; } image.ApplyStyle(dimStyle); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 5624a6820..53096b921 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -77,10 +77,6 @@ namespace Barotrauma set { if (gameSession == value) { return; } - if (value == null && Screen.Selected == GameScreen && gameSession.GameMode is CampaignMode) - { - DebugConsole.AddWarning("GameSession set to null while in the game screen\n" + Environment.StackTrace.CleanupStackTrace()); - } if (gameSession?.GameMode != null && gameSession.GameMode != value?.GameMode) { gameSession.GameMode.Remove(); @@ -465,7 +461,28 @@ namespace Barotrauma while (Config.WaitingForAutoUpdate) { yield return CoroutineStatus.Running; } } - + +#if DEBUG + if (Config.ModBreakerMode) + { + Config.SelectCorePackage(ContentPackage.CorePackages.GetRandom()); + foreach (var regularPackage in ContentPackage.RegularPackages) + { + if (Rand.Range(0.0, 1.0) <= 0.5) + { + Config.EnableRegularPackage(regularPackage); + } + else + { + Config.DisableRegularPackage(regularPackage); + } + } + ContentPackage.SortContentPackages(p => + { + return Rand.Int(int.MaxValue); + }); + } +#endif if (Config.AllEnabledPackages.None()) { @@ -535,6 +552,7 @@ namespace Barotrauma Order.Init(); EventManagerSettings.Init(); BallastFloraPrefab.LoadAll(GetFilesOfType(ContentType.MapCreature)); + HintManager.Init(); TitleScreen.LoadState = 50.0f; yield return CoroutineStatus.Running; @@ -904,7 +922,9 @@ namespace Barotrauma } #if !DEBUG - if (NetworkMember == null && !WindowActive && !Paused && true && Screen.Selected != MainMenuScreen && Config.PauseOnFocusLost) + if (NetworkMember == null && !WindowActive && !Paused && true && Config.PauseOnFocusLost && + Screen.Selected != MainMenuScreen && Screen.Selected != ServerListScreen && Screen.Selected != NetLobbyScreen && + Screen.Selected != SubEditorScreen && Screen.Selected != LevelEditorScreen) { GUI.TogglePauseMenu(); Paused = true; @@ -918,6 +938,8 @@ namespace Barotrauma Client.AddToGUIUpdateList(); } + SubmarinePreview.AddToGUIUpdateList(); + FileSelection.AddToGUIUpdateList(); DebugConsole.AddToGUIUpdateList(); @@ -1044,6 +1066,8 @@ namespace Barotrauma { if (save) { + GUI.SetSavingIndicatorState(true); + if (GameSession.Submarine != null && !GameSession.Submarine.Removed) { GameSession.SubmarineInfo = new SubmarineInfo(GameSession.Submarine); @@ -1074,13 +1098,6 @@ namespace Barotrauma { ((TutorialMode)GameSession.GameMode).Tutorial?.Stop(); } - - if (GameSettings.SendUserStatistics) - { - Mission mission = GameSession.Mission; - GameAnalyticsManager.AddDesignEvent("QuitRound:" + (save ? "Save" : "NoSave")); - GameAnalyticsManager.AddDesignEvent("EndRound:" + (mission == null ? "NoMission" : (mission.Completed ? "MissionCompleted" : "MissionFailed"))); - } } GUIMessageBox.CloseAll(); MainMenuScreen.Select(); @@ -1214,13 +1231,19 @@ namespace Barotrauma base.OnExiting(sender, args); } - public void ShowOpenUrlInWebBrowserPrompt(string url) + public void ShowOpenUrlInWebBrowserPrompt(string url, string promptExtensionTag = null) { if (string.IsNullOrEmpty(url)) { return; } if (GUIMessageBox.VisibleBox?.UserData as string == "verificationprompt") { return; } - var msgBox = new GUIMessageBox("", TextManager.GetWithVariable("openlinkinbrowserprompt", "[link]", url), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }) + string text = TextManager.GetWithVariable("openlinkinbrowserprompt", "[link]", url); + string extensionText = TextManager.Get(promptExtensionTag, returnNull: true, useEnglishAsFallBack: false); + if (!string.IsNullOrEmpty(extensionText)) + { + text += $"\n\n{extensionText}"; + } + + var msgBox = new GUIMessageBox("", text, new string[] { TextManager.Get("Yes"), TextManager.Get("No") }) { UserData = "verificationprompt" }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index dc1e16330..f2d72b36f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -22,17 +22,12 @@ namespace Barotrauma public GUIComponent ReportButtonFrame { get; set; } private GUIFrame guiFrame; - private GUIComponent crewAreaWithButtons; private GUIFrame crewArea; private GUIListBox crewList; - private GUIButton commandButton, toggleCrewButton; private float crewListOpenState; private bool _isCrewMenuOpen = true; private Point crewListEntrySize; - private GUIFrame contextMenu; - private GUIListBox subContextMenu; - /// /// Present only in single player games. In multiplayer. The chatbox is found from GameSession.Client. /// @@ -68,8 +63,6 @@ namespace Barotrauma private Sprite jobIndicatorBackground, previousOrderArrow, cancelIcon; - private const int MaxOrderIcons = 3; - #endregion #region Constructors @@ -89,49 +82,13 @@ namespace Barotrauma #region Crew Area - crewAreaWithButtons = new GUIFrame( - HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.CrewArea, guiFrame.RectTransform), - style: null, - color: Color.Transparent) + crewArea = new GUIFrame(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.CrewArea, guiFrame.RectTransform), style: null, color: Color.Transparent) { CanBeFocused = false }; - var commandButtonHeight = (int)(GUI.Scale * 40); - var buttonSize = new Point((int)(182f / 99f * commandButtonHeight), commandButtonHeight); - var crewListToggleButtonHeight = (int)(64f * buttonSize.X / 175f); - - crewArea = new GUIFrame( - new RectTransform( - new Point(crewAreaWithButtons.Rect.Width, crewAreaWithButtons.Rect.Height - commandButtonHeight - crewListToggleButtonHeight - 2 * HUDLayoutSettings.Padding), - crewAreaWithButtons.RectTransform, - Anchor.BottomLeft), - style: null, - color: Color.Transparent) - { - CanBeFocused = false - }; - - commandButton = new GUIButton( - new RectTransform(buttonSize, parent: crewAreaWithButtons.RectTransform), - style: "CommandButton") - { - // TODO: Update keybind if it's changed - ToolTip = TextManager.Get("inputtype.command") + " (" + GameMain.Config.KeyBindText(InputType.Command) + ")", - OnClicked = (button, userData) => - { - ToggleCommandUI(); - return true; - } - }; - // AbsoluteOffset is set in UpdateProjectSpecific based on crewListOpenState - crewList = new GUIListBox( - new RectTransform( - Vector2.One, - crewArea.RectTransform), - style: null, - isScrollBarOnDefaultSide: false) + crewList = new GUIListBox(new RectTransform(Vector2.One, crewArea.RectTransform), style: null, isScrollBarOnDefaultSide: false) { AutoHideScrollBar = false, CanBeFocused = false, @@ -140,21 +97,6 @@ namespace Barotrauma Spacing = (int)(GUI.Scale * 10) }; - buttonSize.Y = crewListToggleButtonHeight; - toggleCrewButton = new GUIButton( - new RectTransform(buttonSize, parent: crewAreaWithButtons.RectTransform) - { - AbsoluteOffset = new Point(0, commandButtonHeight + HUDLayoutSettings.Padding) - }, - style: "CrewListToggleButton") - { - OnClicked = (GUIButton btn, object userdata) => - { - IsCrewMenuOpen = !IsCrewMenuOpen; - return true; - } - }; - jobIndicatorBackground = new Sprite("Content/UI/CommandUIAtlas.png", new Rectangle(0, 512, 128, 128)); previousOrderArrow = new Sprite("Content/UI/CommandUIAtlas.png", new Rectangle(128, 512, 128, 128)); cancelIcon = new Sprite("Content/UI/CommandUIAtlas.png", new Rectangle(512, 384, 128, 128)); @@ -200,7 +142,8 @@ namespace Barotrauma var headset = GetHeadset(Character.Controlled, true); if (headset != null && headset.CanTransmit()) { - headset.TransmitSignal(stepsTaken: 0, signal: msg, source: headset.Item, sender: Character.Controlled, sentFromChat: true); + Signal s = new Signal(msg, sender: Character.Controlled, source: headset.Item); + headset.TransmitSignal(s, sentFromChat: true); } } textbox.Deselect(); @@ -223,7 +166,11 @@ namespace Barotrauma var chatBox = ChatBox ?? GameMain.Client?.ChatBox; if (chatBox != null) { - chatBox.ToggleButton = new GUIButton(new RectTransform(new Point((int)(182f * GUI.Scale * 0.4f), (int)(99f * GUI.Scale * 0.4f)), chatBox.GUIFrame.Parent.RectTransform), style: "ChatToggleButton"); + chatBox.ToggleButton = new GUIButton(new RectTransform(new Point((int)(182f * GUI.Scale * 0.4f), (int)(99f * GUI.Scale * 0.4f)), chatBox.GUIFrame.Parent.RectTransform), style: "ChatToggleButton") + { + ToolTip = TextManager.Get("chat"), + ClampMouseRectToParent = false + }; chatBox.ToggleButton.RectTransform.AbsoluteOffset = new Point(0, HUDLayoutSettings.ChatBoxArea.Height - chatBox.ToggleButton.Rect.Height); chatBox.ToggleButton.OnClicked += (GUIButton btn, object userdata) => { @@ -261,12 +208,13 @@ namespace Barotrauma if (!CanIssueOrders) { return false; } var sub = Character.Controlled.Submarine; if (sub == null || sub.TeamID != Character.Controlled.TeamID || sub.Info.IsWreck) { return false; } - SetCharacterOrder(null, order, null, Character.Controlled); + SetCharacterOrder(null, order, null, CharacterInfo.HighestManualOrderPriority, Character.Controlled); if (IsSinglePlayer) { HumanAIController.ReportProblem(Character.Controlled, order); } return true; }, UserData = order, - ToolTip = order.Name + ToolTip = order.Name, + ClampMouseRectToParent = false }; new GUIFrame(new RectTransform(new Vector2(1.5f), btn.RectTransform, Anchor.Center), "OuterGlowCircular") @@ -331,7 +279,10 @@ namespace Barotrauma if (removeInfo) { characterInfos.Remove(character.Info); } } - private void AddCharacterToCrewList(Character character) + /// + /// Add character to the list without actually adding it to the crew + /// + public void AddCharacterToCrewList(Character character) { if (character == null) { return; } @@ -339,19 +290,7 @@ namespace Barotrauma new RectTransform(crewListEntrySize, parent: crewList.Content.RectTransform, anchor: Anchor.TopRight), style: "CrewListBackground") { - UserData = character, - OnSecondaryClicked = (comp, data) => - { - if (data == null) { return false; } - - var client = GameMain.NetworkMember?.ConnectedClients?.Find(c => c.Character == data); - if (client != null) - { - CreateModerationContextMenu(PlayerInput.MousePosition.ToPoint(), client); - return true; - } - return false; - } + UserData = character }; var iconRelativeWidth = (float)crewListEntrySize.Y / background.Rect.Width; @@ -413,13 +352,14 @@ namespace Barotrauma layoutGroup.RectTransform) { MaxSize = new Point(150, background.Rect.Height) - }, - ToolBox.LimitString(character.Name, font, (int)(nameRelativeWidth * layoutGroup.Rect.Width)), + }, "", font: font, textColor: character.Info?.Job?.Prefab?.UIColor) { - CanBeFocused = false + CanBeFocused = false, + UserData = "name" }; + nameBlock.Text = ToolBox.LimitString(character.Name, font, (int)nameBlock.Rect.Width); var nameActualRealtiveWidth = Math.Min(nameRelativeWidth * background.Rect.Width, 150) / background.Rect.Width; var characterButton = new GUIButton( @@ -428,24 +368,19 @@ namespace Barotrauma background.RectTransform), style: null) { - UserData = character - }; - - // Only create a tooltip if the name doesn't fit the name block - if (nameBlock.Text.EndsWith("...")) - { - var characterTooltip = character.Name; - if (character.Info?.Job?.Name != null) { characterTooltip += " (" + character.Info.Job.Name + ")"; }; - characterButton.ToolTip = characterTooltip; - if (character.Info?.Job?.Prefab != null) + UserData = character, + OnSecondaryClicked = (comp, data) => { - characterButton.TooltipRichTextData = new List() { new RichTextData() + if (data == null) { return false; } + if (GameMain.NetworkMember?.ConnectedClients?.Find(c => c.Character == data) is Client client) { - Color = character.Info.Job.Prefab.UIColor, - EndIndex = characterTooltip.Length - 1 - }}; + CreateModerationContextMenu(PlayerInput.MousePosition.ToPoint(), client); + return true; + } + return false; } - } + }; + SetCharacterButtonTooltip(characterButton); if (IsSinglePlayer) { @@ -453,7 +388,6 @@ namespace Barotrauma } else { - characterButton.CanBeFocused = false; characterButton.CanBeSelected = false; } @@ -464,6 +398,41 @@ namespace Barotrauma CanBeFocused = false }; + var orderGroup = new GUILayoutGroup(new RectTransform(new Vector2(3 * 0.8f * iconRelativeWidth, 0.8f), parent: layoutGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + CanBeFocused = false, + Stretch = true + }; + + // Current orders + var currentOrderList = new GUIListBox(new RectTransform(new Vector2(0.0f, 1.0f), parent: orderGroup.RectTransform), isHorizontal: true, style: null) + { + AllowMouseWheelScroll = false, + CanDragElements = true, + HideChildrenOutsideFrame = false, + KeepSpaceForScrollBar = false, + OnRearranged = OnOrdersRearranged, + ScrollBarVisible = false, + Spacing = 2, + UserData = character + }; + currentOrderList.RectTransform.IsFixedSize = true; + currentOrderList.OnAddedToGUIUpdateList += (component) => + { + if (component is GUIListBox list) + { + list.CanBeFocused = CanIssueOrders; + list.CanDragElements = CanIssueOrders && list.Content.CountChildren > 1; + } + }; + + // Previous orders + new GUILayoutGroup(new RectTransform(Vector2.One, parent: orderGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + CanBeFocused = false, + Stretch = false + }; + var soundIcons = new GUIFrame(new RectTransform(new Vector2(0.8f * iconRelativeWidth, 0.8f), layoutGroup.RectTransform), style: null) { CanBeFocused = false, @@ -500,14 +469,26 @@ namespace Barotrauma }; } + private void SetCharacterButtonTooltip(GUIButton characterButton) + { + var character = (Character)characterButton.UserData; + if (character?.Info?.Job?.Prefab == null) { return; } + string color = XMLExtensions.ColorToString(character.Info.Job.Prefab.UIColor); + string tooltip = $"‖color:{color}‖{character.Name} ({character.Info.Job.Name})‖color:end‖"; + var richTextData = RichTextData.GetRichTextData(tooltip, out string sanitizedTooltip); + characterButton.ToolTip = sanitizedTooltip; + characterButton.TooltipRichTextData = richTextData; + } + /// /// Sets which character is selected in the crew UI (highlight effect etc) /// public bool CharacterClicked(GUIComponent component, object selection) { if (!AllowCharacterSwitch) { return false; } - Character character = selection as Character; - if (character == null || character.IsDead || character.IsUnconscious) { return false; } + if (!(selection is Character character) || character.IsDead || character.IsUnconscious) { return false; } + if (!character.IsOnPlayerTeam) { return false; } + SelectCharacter(character); if (GUI.KeyboardDispatcher.Subscriber == crewList) { GUI.KeyboardDispatcher.Subscriber = null; } return true; @@ -562,6 +543,15 @@ namespace Barotrauma yield return CoroutineStatus.Success; } + partial void RenameCharacterProjSpecific(CharacterInfo characterInfo) + { + if (!(crewList.Content.GetChildByUserData(characterInfo?.Character) is GUIComponent characterComponent)) { return; } + if (!(characterComponent.FindChild("name", recursive: true) is GUITextBlock nameBlock)) { return; } + nameBlock.Text = ToolBox.LimitString(characterInfo.Name, nameBlock.Font, nameBlock.Rect.Width); + if (!(characterComponent.FindChild(c => c is GUIButton && c.UserData == characterInfo?.Character) is GUIButton characterButton)) { return; } + SetCharacterButtonTooltip(characterButton); + } + #endregion #region Dialog @@ -668,13 +658,11 @@ namespace Barotrauma #region Crew List Order Displayment - // TODO: CHECK ALL THE ORDER CONSTUCTOR CALLS - /// /// Sets the character's current order (if it's close enough to receive messages from orderGiver) and /// displays the order in the crew UI /// - public void SetCharacterOrder(Character character, Order order, string option, Character orderGiver) + public void SetCharacterOrder(Character character, Order order, string option, int priority, Character orderGiver) { if (order != null && order.TargetAllCharacters) { @@ -728,7 +716,7 @@ namespace Barotrauma } else { - OrderChatMessage msg = new OrderChatMessage(order, "", order.IsReport ? hull : order.TargetEntity, null, orderGiver); + OrderChatMessage msg = new OrderChatMessage(order, "", priority, order.IsReport ? hull : order.TargetEntity, null, orderGiver); GameMain.Client?.SendChatMessage(msg); } } @@ -739,12 +727,12 @@ namespace Barotrauma if (IsSinglePlayer) { - character.SetOrder(order, option, orderGiver, speak: orderGiver != character); + character.SetOrder(order, option, priority, orderGiver, speak: orderGiver != character); orderGiver?.Speak(order?.GetChatMessage(character.Name, orderGiver.CurrentHull?.DisplayName, givingOrderToSelf: character == orderGiver, orderOption: option)); } else if (orderGiver != null) { - OrderChatMessage msg = new OrderChatMessage(order, option, order?.TargetSpatialEntity ?? order?.TargetItemComponent?.Item as ISpatialEntity, character, orderGiver); + OrderChatMessage msg = new OrderChatMessage(order, option, priority, order?.TargetSpatialEntity ?? order?.TargetItemComponent?.Item as ISpatialEntity, character, orderGiver); GameMain.Client?.SendChatMessage(msg); } } @@ -753,125 +741,199 @@ namespace Barotrauma /// /// Displays the specified order in the crew UI next to the character. /// - public void AddCurrentOrderIcon(Character character, Order order, string option) + public void AddCurrentOrderIcon(Character character, Order order, string option, int priority) { if (character == null) { return; } - var characterFrame = crewList.Content.GetChildByUserData(character); + var characterComponent = crewList.Content.GetChildByUserData(character); - if (characterFrame == null) { return; } + if (characterComponent == null) { return; } - GUILayoutGroup layoutGroup = (GUILayoutGroup)characterFrame.FindChild(c => c is GUILayoutGroup); + var currentOrderIconList = GetCurrentOrderIconList(characterComponent); + var currentOrderIcons = currentOrderIconList.Content.Children; + var iconsToRemove = new List(); + var newPreviousOrders = new List(); + bool updatedExistingIcon = false; - // Get the OrderInfo from the current order icon - var currentOrderIcon = GetCurrentOrderIcon(layoutGroup); - OrderInfo? currentOrderInfo = null; - if (currentOrderIcon?.UserData is OrderInfo) + foreach (var icon in currentOrderIcons) { - currentOrderInfo = (OrderInfo)currentOrderIcon.UserData; - // No need to recreate icons if the current order matches the new order - if (currentOrderInfo.Value.MatchesOrder(order, option)) + var orderInfo = (OrderInfo)icon.UserData; + var matchingOrder = character.GetCurrentOrder(orderInfo.Order, orderInfo.OrderOption); + if (!matchingOrder.HasValue) { - currentOrderIcon.UserData = new OrderInfo(order, option); - if (currentOrderIcon.FindChild(c => (string)c.UserData == "colorsource") is GUIImage image) + iconsToRemove.Add(icon); + newPreviousOrders.Add(orderInfo); + } + else if (orderInfo.MatchesOrder(order, option)) + { + icon.UserData = new OrderInfo(order, option, priority); + if (icon is GUIImage image) { image.Sprite = GetOrderIconSprite(order, option); - image.ToolTip = CreateOrderTooltip(order, option); } - return; + updatedExistingIcon = true; } } - - // Remove the current order icon - layoutGroup.RemoveChild(currentOrderIcon); + iconsToRemove.ForEach(c => currentOrderIconList.RemoveChild(c)); // Remove a previous order icon if it matches the new order // We don't want the same order as both a current order and a previous order - foreach (GUIComponent icon in GetPreviousOrderIcons(layoutGroup)) + var previousOrderIconGroup = GetPreviousOrderIconGroup(characterComponent); + var previousOrderIcons = previousOrderIconGroup.Children; + foreach (var icon in previousOrderIcons) { - if (icon?.UserData is OrderInfo info && info.MatchesOrder(order, option)) + var orderInfo = (OrderInfo)icon.UserData; + if (orderInfo.MatchesOrder(order, option)) { - layoutGroup.RemoveChild(icon); + previousOrderIconGroup.RemoveChild(icon); break; } } - // Create a new previous order icon from the current order icon's OrderInfo - if (currentOrderInfo.HasValue) + // Rearrange the icons before adding anything + if (updatedExistingIcon) { - AddPreviousOrderIcon(character, layoutGroup, currentOrderInfo.Value); + RearrangeIcons(); } - if (order == null || order.Identifier == dismissedOrderPrefab.Identifier) { return; } - - if (GetPreviousOrderIcons(layoutGroup).Count() >= MaxOrderIcons) + for (int i = newPreviousOrders.Count - 1; i >= 0; i--) { - RemoveLastPreviousOrderIcon(layoutGroup); + AddPreviousOrderIcon(character, characterComponent, newPreviousOrders[i]); } - var orderFrame = new GUIButton( - new RectTransform( - layoutGroup.GetChildByUserData("job").RectTransform.RelativeSize, - layoutGroup.RectTransform), - style: null) + if (order == null || order.Identifier == dismissedOrderPrefab.Identifier || updatedExistingIcon) { - UserData = new OrderInfo(order, option), - OnClicked = (button, userData) => - { - if (!CanIssueOrders) { return false; } - SetCharacterOrder(character, dismissedOrderPrefab, null, Character.Controlled); - return true; - } + RearrangeIcons(); + return; + } + + int orderIconCount = currentOrderIconList.Content.CountChildren + previousOrderIconGroup.CountChildren; + if (orderIconCount >= CharacterInfo.MaxCurrentOrders) + { + RemoveLastOrderIcon(characterComponent); + } + + float nodeWidth = ((1.0f / CharacterInfo.MaxCurrentOrders) * currentOrderIconList.Parent.Rect.Width) - ((CharacterInfo.MaxCurrentOrders - 1) * currentOrderIconList.Spacing); + Point size = new Point((int)nodeWidth, currentOrderIconList.RectTransform.NonScaledSize.Y); + var nodeIcon = CreateNodeIcon(size, currentOrderIconList.Content.RectTransform, GetOrderIconSprite(order, option), order.Color, tooltip: CreateOrderTooltip(order, option)); + nodeIcon.UserData = new OrderInfo(order, option, priority); + nodeIcon.OnSecondaryClicked = (image, userData) => + { + if (!CanIssueOrders) { return false; } + var orderInfo = (OrderInfo)userData; + SetCharacterOrder(character, dismissedOrderPrefab, Order.GetDismissOrderOption(orderInfo), + character.GetCurrentOrder(orderInfo.Order, orderInfo.OrderOption)?.ManualPriority ?? 0, + Character.Controlled); + return true; }; - CreateNodeIcon(orderFrame.RectTransform, - GetOrderIconSprite(order, option), - order.Color, - tooltip: CreateOrderTooltip(order, option)); - - new GUIImage(new RectTransform(Vector2.One, orderFrame.RectTransform), cancelIcon, scaleToFit: true) + new GUIFrame(new RectTransform(new Point((int)(1.5f * nodeWidth)), parent: nodeIcon.RectTransform, Anchor.Center), "OuterGlowCircular") { CanBeFocused = false, - UserData = "cancel", + Color = order.Color, + UserData = "glow", Visible = false }; - orderFrame.RectTransform.RepositionChildInHierarchy(4); + int hierarchyIndex = GetOrderIconHierarchyIndex(priority); + if (hierarchyIndex != currentOrderIconList.Content.GetChildIndex(nodeIcon)) + { + nodeIcon.RectTransform.RepositionChildInHierarchy(hierarchyIndex); + } + + RearrangeIcons(); + + void RearrangeIcons() + { + if (character.CurrentOrders != null) + { + // Make sure priority values are up-to-date + foreach (var currentOrderInfo in character.CurrentOrders) + { + var component = currentOrderIconList.Content.FindChild(c => c?.UserData is OrderInfo componentOrderInfo && + componentOrderInfo.MatchesOrder(currentOrderInfo)); + if (component == null) { continue; } + var componentOrderInfo = (OrderInfo)component.UserData; + int newPriority = currentOrderInfo.ManualPriority; + if (componentOrderInfo.ManualPriority != newPriority) + { + component.UserData = new OrderInfo(componentOrderInfo, newPriority); + } + } + + currentOrderIconList.Content.RectTransform.SortChildren((x, y) => + { + var xOrder = (OrderInfo)x.GUIComponent.UserData; + var yOrder = (OrderInfo)y.GUIComponent.UserData; + return yOrder.ManualPriority.CompareTo(xOrder.ManualPriority); + }); + + if (currentOrderIconList.Parent is GUILayoutGroup parentGroup) + { + int iconCount = currentOrderIconList.Content.CountChildren; + float nonScaledWidth = ((float)iconCount / CharacterInfo.MaxCurrentOrders) * parentGroup.Rect.Width + (iconCount * currentOrderIconList.Spacing); + currentOrderIconList.RectTransform.NonScaledSize = new Point((int)nonScaledWidth, currentOrderIconList.RectTransform.NonScaledSize.Y); + parentGroup.Recalculate(); + previousOrderIconGroup.Recalculate(); + } + } + } + + static int GetOrderIconHierarchyIndex(int priority) + { + return CharacterInfo.HighestManualOrderPriority - priority; + } } - private void AddPreviousOrderIcon(Character character, GUILayoutGroup characterComponent, OrderInfo orderInfo) + public void AddCurrentOrderIcon(Character character, OrderInfo? orderInfo) + { + AddCurrentOrderIcon(character, orderInfo?.Order, orderInfo?.OrderOption, orderInfo?.ManualPriority ?? 0); + } + + private void AddPreviousOrderIcon(Character character, GUIComponent characterComponent, OrderInfo orderInfo) { if (orderInfo.Order == null || orderInfo.Order.Identifier == dismissedOrderPrefab.Identifier) { return; } - var maxPreviousOrderIcons = GetCurrentOrderIcon(characterComponent) != null ? MaxOrderIcons - 1 : MaxOrderIcons; - if (GetPreviousOrderIcons(characterComponent).Count() >= maxPreviousOrderIcons) + var currentOrderIconList = GetCurrentOrderIconList(characterComponent); + int maxPreviousOrderIcons = CharacterInfo.MaxCurrentOrders - currentOrderIconList.Content.CountChildren; + + if (maxPreviousOrderIcons < 1) { return; } + + var previousOrderIconGroup = GetPreviousOrderIconGroup(characterComponent); + if (previousOrderIconGroup.CountChildren >= maxPreviousOrderIcons) { - RemoveLastPreviousOrderIcon(characterComponent); + RemoveLastPreviousOrderIcon(previousOrderIconGroup); } - var previousOrderInfo = new OrderInfo(orderInfo); - - var prevOrderFrame = new GUIButton( - new RectTransform( - characterComponent.GetChildByUserData("job").RectTransform.RelativeSize, - characterComponent.RectTransform), - style: null) + float nodeWidth = ((1.0f / CharacterInfo.MaxCurrentOrders) * previousOrderIconGroup.Parent.Rect.Width) - ((CharacterInfo.MaxCurrentOrders - 1) * currentOrderIconList.Spacing); + Point size = new Point((int)nodeWidth, previousOrderIconGroup.Rect.Height); + var previousOrderInfo = new OrderInfo(orderInfo, OrderInfo.OrderType.Previous); + var prevOrderFrame = new GUIButton(new RectTransform(size, parent: previousOrderIconGroup.RectTransform), style: null) { - UserData = previousOrderInfo, + UserData = previousOrderInfo, OnClicked = (button, userData) => { if (!CanIssueOrders) { return false; } var orderInfo = (OrderInfo)userData; - SetCharacterOrder(character, orderInfo.Order, orderInfo.OrderOption, Character.Controlled); + SetCharacterOrder(character, orderInfo.Order, orderInfo.OrderOption, CharacterInfo.HighestManualOrderPriority, Character.Controlled); + return true; + }, + OnSecondaryClicked = (button, userData) => + { + if (previousOrderIconGroup == null) { return false; } + previousOrderIconGroup.RemoveChild(button); + previousOrderIconGroup.Recalculate(); return true; } }; + prevOrderFrame.RectTransform.IsFixedSize = true; var prevOrderIconFrame = new GUIFrame( new RectTransform(new Vector2(0.8f), prevOrderFrame.RectTransform, anchor: Anchor.BottomLeft), style: null); - CreateNodeIcon(prevOrderIconFrame.RectTransform, + CreateNodeIcon(Vector2.One, + prevOrderIconFrame.RectTransform, GetOrderIconSprite(previousOrderInfo), previousOrderInfo.Order.Color, tooltip: CreateOrderTooltip(previousOrderInfo)); @@ -891,22 +953,20 @@ namespace Barotrauma CanBeFocused = false }; - var positionInHierarchy = GetCurrentOrderIcon(characterComponent) != null ? 5 : 4; - prevOrderFrame.RectTransform.RepositionChildInHierarchy(positionInHierarchy); + prevOrderFrame.SetAsFirstChild(); } - private void AddOldPreviousOrderIcons(Character character, GUILayoutGroup oldCharacterComponent) + private void AddOldPreviousOrderIcons(Character character, GUIComponent oldCharacterComponent) { - var prevOrderIcons = GetPreviousOrderIcons(oldCharacterComponent).ToList(); - if (prevOrderIcons.None()) { return; } - if (prevOrderIcons.Count() > 1) + var oldPrevOrderIcons = GetPreviousOrderIconGroup(oldCharacterComponent).Children; + if (oldPrevOrderIcons.None()) { return; } + if (oldPrevOrderIcons.Count() > 1) { - prevOrderIcons.Sort((x, y) => oldCharacterComponent.GetChildIndex(x).CompareTo(oldCharacterComponent.GetChildIndex(y))); - prevOrderIcons.Reverse(); + oldPrevOrderIcons = oldPrevOrderIcons.Reverse(); } - if (crewList.Content.Children.FirstOrDefault(c => c?.UserData == character)?.GetChild() is GUILayoutGroup newCharacterComponent) + if (crewList.Content.Children.FirstOrDefault(c => c.UserData == character) is GUIComponent newCharacterComponent) { - foreach (GUIComponent icon in prevOrderIcons) + foreach (GUIComponent icon in oldPrevOrderIcons) { if (icon.UserData is OrderInfo orderInfo) { @@ -916,36 +976,49 @@ namespace Barotrauma } } - private void RemoveLastPreviousOrderIcon(GUILayoutGroup characterComponent) + private void RemoveLastOrderIcon(GUIComponent characterComponent) { - var prevOrderIcons = GetPreviousOrderIcons(characterComponent); - if (prevOrderIcons.None()) { return; } - if (prevOrderIcons.Count() == 1) + var previousOrderIconGroup = GetPreviousOrderIconGroup(characterComponent); + if (RemoveLastPreviousOrderIcon(previousOrderIconGroup)) { - characterComponent.RemoveChild(prevOrderIcons.First()); + return; } - else + var currentOrderIconList = GetCurrentOrderIconList(characterComponent); + if (currentOrderIconList.Content.CountChildren > 0) { - int highestIndex = 0; - GUIComponent oldestPreviousOrderIcon = null; - foreach (GUIComponent icon in prevOrderIcons) - { - int i = characterComponent.GetChildIndex(icon); - if (i > highestIndex || oldestPreviousOrderIcon == null) - { - highestIndex = i; - oldestPreviousOrderIcon = icon; - } - } - characterComponent.RemoveChild(oldestPreviousOrderIcon); + var iconToRemove = currentOrderIconList.Content.Children.Last(); + currentOrderIconList.RemoveChild(iconToRemove); + return; } } - private GUIComponent GetCurrentOrderIcon(GUILayoutGroup characterComponent) => - characterComponent?.FindChild(c => c?.UserData is OrderInfo orderInfo && orderInfo.ComponentIdentifier == "currentorder"); + private bool RemoveLastPreviousOrderIcon(GUILayoutGroup iconGroup) + { + if (iconGroup.CountChildren > 0) + { + var iconToRemove = iconGroup.Children.Last(); + iconGroup.RemoveChild(iconToRemove); + return true; + } + return false; + } - private IEnumerable GetPreviousOrderIcons(GUILayoutGroup characterComponent) => - characterComponent?.FindChildren(c => c?.UserData is OrderInfo orderInfo && orderInfo.ComponentIdentifier == "previousorder"); + private GUIListBox GetCurrentOrderIconList(GUIComponent characterComponent) => + characterComponent?.GetChild().GetChild().GetChild(); + + private GUILayoutGroup GetPreviousOrderIconGroup(GUIComponent characterComponent) => + characterComponent?.GetChild().GetChild().GetChild(); + + private void OnOrdersRearranged(GUIListBox orderList, object userData) + { + var orderComponent = orderList.Content.GetChildByUserData(userData); + if (orderComponent == null) { return; } + var orderInfo = (OrderInfo)userData; + var priority = Math.Max(CharacterInfo.HighestManualOrderPriority - orderList.Content.GetChildIndex(orderComponent), 1); + if (orderInfo.ManualPriority == priority) { return; } + var character = (Character)orderList.UserData; + SetCharacterOrder(character, orderInfo.Order, orderInfo.OrderOption, priority, Character.Controlled); + } private string CreateOrderTooltip(Order order, string option) { @@ -1024,22 +1097,9 @@ namespace Barotrauma public void CreateModerationContextMenu(Point mousePos, Client client) { - if (IsSinglePlayer || client == null || (!GameMain.Client?.PreviouslyConnectedClients?.Contains(client) ?? true)) { return; } + if (GUIContextMenu.CurrentContextMenu != null) { return; } + if (IsSinglePlayer || client == null || ((!GameMain.Client?.PreviouslyConnectedClients?.Contains(client)) ?? true)) { return; } - contextMenu = new GUIFrame(new RectTransform(new Vector2(0.1f, 0.15f), GUI.Canvas) { ScreenSpaceOffset = mousePos }, style: "GUIToolTip") { UserData = client }; - - var nameLabel = new GUITextBlock(new RectTransform(new Vector2(1f, 0.2f), contextMenu.RectTransform), client.Name, font: GUI.SubHeadingFont) - { - Padding = new Vector4(8), - TextColor = client.Character?.Info?.Job.Prefab.UIColor ?? Color.White - }; - - var optionsList = new GUIListBox(new RectTransform(new Vector2(1f, 0.8f), contextMenu.RectTransform, Anchor.BottomLeft), style: null) - { - Padding = new Vector4(4, 0, 4, 4), - AutoHideScrollBar = false, - ScrollBarVisible = false - }; bool hasSteam = client.SteamID > 0 && SteamManager.IsInitialized, canKick = GameMain.Client.HasPermission(ClientPermissions.Kick), @@ -1052,196 +1112,82 @@ namespace Barotrauma canKick = canBan = canPromo = false; } - RectTransform parent = optionsList.Content.RectTransform; - new GUITextBlock(new RectTransform(Point.Zero, parent), TextManager.Get("viewsteamprofile"), font: GUI.SmallFont) - { - Padding = new Vector4(4), - Enabled = hasSteam, - UserData = "steam" - }; - - new GUITextBlock(new RectTransform(Point.Zero, parent), TextManager.Get("moderationmenu.userdetails"), font: GUI.SmallFont) - { - Padding = new Vector4(4), - Enabled = true, - UserData = "user" - }; - - new GUITextBlock(new RectTransform(Point.Zero, parent), TextManager.Get("permissions"), font: GUI.SmallFont) - { - Padding = new Vector4(4), - Enabled = canPromo, - UserData = "promote" - }; - - if (GameMain.Client.ConnectedClients.Contains(client)) - { - new GUITextBlock(new RectTransform(Point.Zero, parent), TextManager.Get(client.MutedLocally ? "unmute" : "mute"), font: GUI.SmallFont) - { - Padding = new Vector4(4), - Enabled = client.ID != GameMain.Client?.ID, - UserData = "mute" - }; - - new GUITextBlock(new RectTransform(Point.Zero, parent), TextManager.Get(canKick ? "kick" : "votetokick"), font: GUI.SmallFont) - { - Padding = new Vector4(4), - Enabled = client.ID != GameMain.Client?.ID && client.AllowKicking, - UserData = canKick ? "kick" : "votekick" - }; - } - - new GUITextBlock(new RectTransform(Point.Zero, parent), TextManager.Get("ban"), font: GUI.SmallFont) - { - Padding = new Vector4(4), - Enabled = canBan, - UserData = "ban" - }; - - foreach (GUIComponent c in optionsList.Content.Children) - { - if (c is GUITextBlock child && !child.Enabled) - { - child.TextColor *= 0.5f; - } - } - - var children = optionsList.Content.Children.ToList(); - - // Resize all children to the size of their text - foreach (GUITextBlock block in children.Where(c => c is GUITextBlock).Cast()) - { - block.RectTransform.NonScaledSize = new Point((int) (block.TextSize.X + (block.Padding.X + block.Padding.Z)), (int)(18 * GUI.Scale)); - } - - int horizontalPadding = (int)(optionsList.Padding.X + optionsList.Padding.Z); - int verticalPadding = (int)(optionsList.Padding.Y + optionsList.Padding.W); - int largestWidth = children.Max(c => c.Rect.Width + horizontalPadding); - - // If the name is bigger than any of the options then overwrite - nameLabel.RectTransform.MinSize = new Point((int)(nameLabel.TextSize.X + (nameLabel.Padding.X + nameLabel.Padding.Z)), nameLabel.RectTransform.NonScaledSize.Y); - if (largestWidth < nameLabel.RectTransform.MinSize.X) { largestWidth = nameLabel.RectTransform.MinSize.X; } - - // Resize all children to the size of the longest element - foreach (GUIComponent c in children) { c.RectTransform.MinSize = new Point(largestWidth, c.Rect.Height); } + List options = new List(); - // crop the context menu - contextMenu.RectTransform.NonScaledSize = new Point(largestWidth, (children.Sum(c => c.Rect.Height) + verticalPadding) + nameLabel.Rect.Height); + options.Add(new ContextMenuOption("ViewSteamProfile", isEnabled: hasSteam, onSelected: delegate + { + Steamworks.SteamFriends.OpenWebOverlay($"https://steamcommunity.com/profiles/{client.SteamID}"); + })); - // if the menu would go off the screen then move it up - if (contextMenu.Rect.Bottom > GameMain.GraphicsHeight) + options.Add(new ContextMenuOption("ModerationMenu.UserDetails", isEnabled: true, onSelected: delegate { - contextMenu.RectTransform.ScreenSpaceOffset = new Point(mousePos.X, mousePos.Y - contextMenu.Rect.Height); - } - - optionsList.OnSelected = (component, obj) => + GameMain.NetLobbyScreen?.SelectPlayer(client); + })); + + + // Creates sub context menu options for all the ranks + List permissionOptions = new List(); + foreach (PermissionPreset rank in PermissionPreset.List) { - if (component.Enabled) + permissionOptions.Add(new ContextMenuOption(rank.Name, isEnabled: true, onSelected: () => { - switch (obj) + string label = TextManager.GetWithVariables(rank.Permissions == ClientPermissions.None ? "clearrankprompt" : "giverankprompt", new []{ "[user]", "[rank]" }, new []{ client.Name, rank.Name }); + GUIMessageBox msgBox = new GUIMessageBox(string.Empty, label, new[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); + + msgBox.Buttons[0].OnClicked = delegate { - case "steam": - Steamworks.SteamFriends.OpenWebOverlay($"https://steamcommunity.com/profiles/{client.SteamID}"); - break; - case "mute": - client.MutedLocally = !client.MutedLocally; - break; - case "kick": - GameMain.Client?.CreateKickReasonPrompt(client.Name, false); - break; - case "votekick": - GameMain.Client?.VoteForKick(client); - break; - case "ban": - GameMain.Client?.CreateKickReasonPrompt(client.Name, true); - break; - case "user": - GameMain.NetLobbyScreen?.SelectPlayer(client); - break; - } - contextMenu = null; - return true; - } - return false; - }; - } - - private void CreatePromoteSubMenu(Point pos, Client client) - { - if (client == null ) { return; } - - subContextMenu = new GUIListBox(new RectTransform(new Vector2(0.1f, 0.1f), GUI.Canvas) { ScreenSpaceOffset = pos }, style: "GUIToolTip") - { - AutoHideScrollBar = false, - ScrollBarVisible = false - }; - - foreach (var rank in PermissionPreset.List) - { - new GUITextBlock(new RectTransform(Point.Zero, subContextMenu.Content.RectTransform), rank.Name, font: GUI.SmallFont) - { - ToolTip = rank.Description, - UserData = rank, - Padding = new Vector4(4) - }; - } - - var children = subContextMenu.Content.Children.ToList(); - - // Resize all children to the size of their text - foreach (GUITextBlock block in children.Where(c => c is GUITextBlock).Cast()) - { - block.RectTransform.NonScaledSize = new Point((int) (block.TextSize.X + (block.Padding.X + block.Padding.Z)), (int)(18 * GUI.Scale)); - } - - int horizontalPadding = (int)(subContextMenu.Padding.X + subContextMenu.Padding.Z); - int largestWidth = children.Max(c => c.Rect.Width + horizontalPadding); - - // Resize all children to the size of the longest element - foreach (GUIComponent c in children) { c.RectTransform.MinSize = new Point(largestWidth, c.Rect.Height); } - - // crop the context menu - subContextMenu.RectTransform.NonScaledSize = new Point(largestWidth, children.Sum(c => c.Rect.Height) + horizontalPadding); - - // if the menu would go off the screen then move it up - if (subContextMenu.Rect.Bottom > GameMain.GraphicsHeight) - { - subContextMenu.RectTransform.ScreenSpaceOffset = new Point(pos.X, pos.Y - subContextMenu.Rect.Height); - } - - subContextMenu.OnSelected = (component, obj) => - { - if (component.Enabled && obj is PermissionPreset preset) - { - var label = TextManager.GetWithVariables(preset.Permissions == ClientPermissions.None ? "clearrankprompt" : "giverankprompt", new []{ "[user]", "[rank]" }, new []{ client.Name, preset.Name }); - - var msgBox = new GUIMessageBox(string.Empty, label, new[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); - - msgBox.Buttons[0].OnClicked = (yesBtn, userdata) => - { - client.SetPermissions(preset.Permissions, preset.PermittedCommands); + client.SetPermissions(rank.Permissions, rank.PermittedCommands); GameMain.Client.UpdateClientPermissions(client); msgBox.Close(); return true; }; - msgBox.Buttons[1].OnClicked = (_, userdata) => + msgBox.Buttons[1].OnClicked = delegate { msgBox.Close(); return true; }; - contextMenu = null; - subContextMenu = null; - return true; - } - return false; - }; - } + }) { Tooltip = rank.Description }); + } - private static bool IsMouseOnContextMenu(Rectangle rect) - { - Rectangle expandedRect = rect; - expandedRect.Inflate(20, 20); - return expandedRect.Contains(PlayerInput.MousePosition); + options.Add(new ContextMenuOption("Permissions", isEnabled: canPromo, options: permissionOptions.ToArray())); + + Color clientColor = client.Character?.Info?.Job.Prefab.UIColor ?? Color.White; + + if (GameMain.Client.ConnectedClients.Contains(client)) + { + options.Add(new ContextMenuOption(client.MutedLocally ? "Unmute" : "Mute", isEnabled: client.ID != GameMain.Client?.ID, onSelected: delegate + { + client.MutedLocally = !client.MutedLocally; + })); + + bool kickEnabled = client.ID != GameMain.Client?.ID && client.AllowKicking; + + // if the user can kick create a kick option else create the votekick option + ContextMenuOption kickOption; + if (canKick) + { + kickOption = new ContextMenuOption("Kick", isEnabled: kickEnabled, onSelected: delegate + { + GameMain.Client?.CreateKickReasonPrompt(client.Name, false); + }); + } + else + { + kickOption = new ContextMenuOption("VoteToKick", isEnabled: kickEnabled, onSelected: delegate + { + GameMain.Client?.VoteForKick(client); + }); + } + + options.Add(kickOption); + } + + options.Add(new ContextMenuOption("Ban", isEnabled: canBan, onSelected: delegate + { + GameMain.Client?.CreateKickReasonPrompt(client.Name, true); + })); + + GUIContextMenu.CreateContextMenu(null, client.Name, headerColor: clientColor, options.ToArray()); } #endregion @@ -1257,22 +1203,20 @@ namespace Barotrauma if (GameMain.GraphicsWidth != screenResolution.X || GameMain.GraphicsHeight != screenResolution.Y || prevUIScale != GUI.Scale) { - var previousCrewList = crewList; + var oldCrewList = crewList; InitProjectSpecific(); - foreach (GUIComponent c in previousCrewList.Content.Children) + foreach (GUIComponent oldCharacterComponent in oldCrewList.Content.Children) { - if (!(c.UserData is Character character) || character.IsDead || character.Removed) { continue; } + if (!(oldCharacterComponent.UserData is Character character) || character.IsDead || character.Removed) { continue; } AddCharacter(character); - AddOldPreviousOrderIcons(character, c.GetChild()); + AddOldPreviousOrderIcons(character, oldCharacterComponent); } } - crewAreaWithButtons.Visible = !(GameMain.GameSession?.GameMode is CampaignMode campaign) || (!campaign.ForceMapUI && !campaign.ShowCampaignUI); + crewArea.Visible = !(GameMain.GameSession?.GameMode is CampaignMode campaign) || (!campaign.ForceMapUI && !campaign.ShowCampaignUI); guiFrame.AddToGUIUpdateList(); - contextMenu?.AddToGUIUpdateList(false, 1); - subContextMenu?.AddToGUIUpdateList(false, 1); } public void SelectNextCharacter() @@ -1306,6 +1250,7 @@ namespace Barotrauma } DisableCommandUI(); Character.Controlled = character; + HintManager.OnChangeCharacter(); } private int TryAdjustIndex(int amount) @@ -1338,43 +1283,6 @@ namespace Barotrauma SelectPreviousCharacter(); } } - - // context menu behavior - if (contextMenu != null) - { - var promote = contextMenu.GetChild()?.Content.GetChildByUserData("promote"); - - if (promote != null && promote.Enabled) - { - promote.ExternalHighlight = subContextMenu != null; - - if (GUI.IsMouseOn(promote)) - { - if (contextMenu.UserData is Client client && subContextMenu == null) - { - CreatePromoteSubMenu(new Point(promote.Rect.Right, promote.Rect.Y), client); - } - } - else if (subContextMenu != null && !IsMouseOnContextMenu(subContextMenu.Rect)) - { - subContextMenu = null; - } - } - else - { - subContextMenu = null; - } - - if (subContextMenu == null && !IsMouseOnContextMenu(contextMenu.Rect)) - { - contextMenu = null; - } - } - - if (contextMenu == null && subContextMenu != null) - { - subContextMenu = null; - } if (GUI.DisableHUD) { return; } @@ -1382,7 +1290,8 @@ namespace Barotrauma WasCommandInterfaceDisabledThisUpdate = false; - if (PlayerInput.KeyDown(InputType.Command) && (GUI.KeyboardDispatcher.Subscriber == null || GUI.KeyboardDispatcher.Subscriber == crewList) && + if (PlayerInput.KeyDown(InputType.Command) && + (GUI.KeyboardDispatcher.Subscriber == null || (GUI.KeyboardDispatcher.Subscriber is GUIComponent component && (component == crewList || component.IsChildOf(crewList)))) && commandFrame == null && !clicklessSelectionActive && CanIssueOrders && !(GameMain.GameSession?.Campaign?.ShowCampaignUI ?? false)) { if (PlayerInput.KeyDown(Keys.LeftShift) || PlayerInput.KeyDown(Keys.RightShift)) @@ -1539,19 +1448,6 @@ namespace Barotrauma clicklessSelectionActive = false; } - // TODO: Expand crew list to use command button's space when it's not visible - if (!IsSinglePlayer && commandButton != null) - { - if (!CanIssueOrders && commandButton.Visible) - { - commandButton.Visible = false; - } - else if (CanIssueOrders && !commandButton.Visible) - { - commandButton.Visible = true; - } - } - #endregion if (ChatBox != null) @@ -1599,27 +1495,33 @@ namespace Barotrauma { crewArea.Visible = characters.Count > 0 && CharacterHealth.OpenHealthWindow == null; - foreach (GUIComponent child in crewList.Content.Children) + foreach (GUIComponent characterComponent in crewList.Content.Children) { - if (child.UserData is Character character) + if (characterComponent.UserData is Character character) { - child.Visible = Character.Controlled == null || Character.Controlled.TeamID == character.TeamID; - if (child.Visible) + characterComponent.Visible = Character.Controlled == null || Character.Controlled.TeamID == character.TeamID; + if (character.TeamID == CharacterTeamType.FriendlyNPC && Character.Controlled != null && + (character.CurrentHull == Character.Controlled.CurrentHull || Vector2.DistanceSquared(Character.Controlled.WorldPosition, character.WorldPosition) < 500.0f * 500.0f)) { - if (character == Character.Controlled && child.State != GUIComponent.ComponentState.Selected) + characterComponent.Visible = true; + } + if (characterComponent.Visible) + { + if (character == Character.Controlled && characterComponent.State != GUIComponent.ComponentState.Selected) { crewList.Select(character, force: true); } - if (child.FindChild(c => c is GUILayoutGroup) is GUILayoutGroup layoutGroup) + if (character.AIController is HumanAIController controller) { - if (GetCurrentOrderIcon(layoutGroup) is GUIComponent orderButton && - orderButton.GetChildByUserData("colorsource") is GUIComponent orderIcon && - orderButton.GetChildByUserData("cancel") is GUIComponent cancelIcon) + OrderInfo? currentOrderInfo = controller.ObjectiveManager?.GetCurrentOrderInfo(); + if (currentOrderInfo.HasValue) { - cancelIcon.Visible = GUI.IsMouseOn(orderIcon); + SetHighlightedOrderIcon(characterComponent, currentOrderInfo.Value.Order?.Identifier, currentOrderInfo.Value.OrderOption); } - if (layoutGroup.GetChildByUserData("soundicons")? - .FindChild(c => c.UserData is Pair pair && pair.First == "soundicon") is GUIImage soundIcon) + } + if (characterComponent.GetChild().GetChildByUserData("soundicons") is GUIComponent soundIconParent) + { + if (soundIconParent.FindChild(c => c.UserData is Pair pair && pair.First == "soundicon") is GUIImage soundIcon) { VoipClient.UpdateVoiceIndicator(soundIcon, 0.0f, deltaTime); } @@ -1647,6 +1549,33 @@ namespace Barotrauma UpdateReports(); } + private void SetHighlightedOrderIcon(GUIComponent characterComponent, string orderIdentifier, string orderOption) + { + var currentOrderIconList = GetCurrentOrderIconList(characterComponent); + if (currentOrderIconList == null) { return; } + bool foundMatch = false; + foreach (var orderIcon in currentOrderIconList.Content.Children) + { + var glowComponent = orderIcon.GetChildByUserData("glow"); + if (glowComponent == null) { continue; } + if (foundMatch) + { + glowComponent.Visible = false; + continue; + } + var orderInfo = (OrderInfo)orderIcon.UserData; + foundMatch = orderInfo.MatchesOrder(orderIdentifier, orderOption); + glowComponent.Visible = foundMatch; + } + } + + public void SetHighlightedOrderIcon(Character character, string orderIdentifier, string orderOption) + { + if (crewList == null) { return; } + var characterComponent = crewList.Content.GetChildByUserData(character); + SetHighlightedOrderIcon(characterComponent, orderIdentifier, orderOption); + } + #endregion #region Command UI @@ -1703,7 +1632,7 @@ namespace Barotrauma private const int maxShortCutNodeCount = 4; private bool WasCommandInterfaceDisabledThisUpdate { get; set; } - private bool CanIssueOrders + public static bool CanIssueOrders { get { @@ -1720,7 +1649,8 @@ namespace Barotrauma #if DEBUG if (Character.Controlled == null) { return true; } #endif - return Character.Controlled != null && characters.Any(c => c != Character.Controlled && c.CanHearCharacter(Character.Controlled)); + return Character.Controlled != null && + (characters.Any(c => c != Character.Controlled && c.CanHearCharacter(Character.Controlled)) || GetOrderableFriendlyNPCs().Any(c => c != Character.Controlled && c.CanHearCharacter(Character.Controlled))); } private Entity FindEntityContext() @@ -1856,9 +1786,11 @@ namespace Barotrauma { Character.Controlled.dontFollowCursor = true; } + + HintManager.OnShowCommandInterface(); } - private void ToggleCommandUI() + public void ToggleCommandUI() { if (commandFrame == null) { @@ -2174,7 +2106,7 @@ namespace Barotrauma var tooltip = TextManager.Get("ordercategorytitle." + category.ToString().ToLower()); var categoryDescription = TextManager.Get("ordercategorydescription." + category.ToString(), true); if (!string.IsNullOrWhiteSpace(categoryDescription)) { tooltip += "\n" + categoryDescription; } - CreateNodeIcon(node.RectTransform, sprite.Item1, sprite.Item2, tooltip: tooltip); + CreateNodeIcon(Vector2.One, node.RectTransform, sprite.Item1, sprite.Item2, tooltip: tooltip); } CreateHotkeyIcon(node.RectTransform, hotkey % 10); optionNodes.Add(new Tuple(node, Keys.D0 + hotkey % 10)); @@ -2448,8 +2380,7 @@ namespace Barotrauma // Show 'dismiss' order only when there are crew members with active orders orderIdentifier = "dismissed"; - if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier)) && - characters.Any(c => c.CurrentOrder != null && !c.CurrentOrder.Identifier.Equals(orderIdentifier))) + if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier)) && characters.Any(c => !c.IsDismissed)) { contextualOrders.Add(Order.GetPrefab(orderIdentifier)); } @@ -2534,18 +2465,19 @@ namespace Barotrauma o = new Order(o.Prefab, orderTargetEntity, orderTargetEntity.Components.FirstOrDefault(ic => ic.GetType() == order.ItemComponentType), orderGiver: order.OrderGiver); } var character = !o.TargetAllCharacters ? characterContext ?? GetCharacterForQuickAssignment(o) : null; - SetCharacterOrder(character, o, null, Character.Controlled); + SetCharacterOrder(character, o, null, CharacterInfo.HighestManualOrderPriority, Character.Controlled); DisableCommandUI(); } return true; }; + if (CanOpenManualAssignment(node)) { node.OnSecondaryClicked = (button, _) => CreateAssignmentNodes(button); } var showAssignmentTooltip = !mustSetOptionOrTarget && characterContext == null && !order.MustManuallyAssign && !order.TargetAllCharacters; var orderName = GetOrderNameBasedOnContextuality(order); - var icon = CreateNodeIcon(node.RectTransform, order.SymbolSprite, order.Color, + var icon = CreateNodeIcon(Vector2.One, node.RectTransform, order.SymbolSprite, order.Color, tooltip: !showAssignmentTooltip ? orderName : orderName + "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.leftmouse") : TextManager.Get("input.rightmouse")) + ": " + TextManager.Get("commandui.quickassigntooltip") + "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.rightmouse") : TextManager.Get("input.leftmouse")) + ": " + TextManager.Get("commandui.manualassigntooltip")); @@ -2663,7 +2595,7 @@ namespace Barotrauma { if (!CanIssueOrders) { return false; } var o = userData as Tuple; - SetCharacterOrder(characterContext ?? GetCharacterForQuickAssignment(o.Item1), o.Item1, o.Item2, Character.Controlled); + SetCharacterOrder(characterContext ?? GetCharacterForQuickAssignment(o.Item1), o.Item1, o.Item2, CharacterInfo.HighestManualOrderPriority, Character.Controlled); DisableCommandUI(); return true; } @@ -2692,7 +2624,7 @@ namespace Barotrauma { if (!CanIssueOrders) { return false; } var o = userData as Tuple; - SetCharacterOrder(characterContext ?? GetCharacterForQuickAssignment(o.Item1), o.Item1, o.Item2, Character.Controlled); + SetCharacterOrder(characterContext ?? GetCharacterForQuickAssignment(o.Item1), o.Item1, o.Item2, CharacterInfo.HighestManualOrderPriority, Character.Controlled); DisableCommandUI(); return true; } @@ -2707,10 +2639,10 @@ namespace Barotrauma { icon = item.Prefab.MinimapIcon; } - var colorMultiplier = characters.Any(c => c.CurrentOrder != null && - c.CurrentOrder.Identifier == userData.Item1.Identifier && - c.CurrentOrder.TargetEntity == userData.Item1.TargetEntity) ? 0.5f : 1f; - CreateNodeIcon(optionElement.RectTransform, icon ?? order.SymbolSprite, order.Color * colorMultiplier); + var colorMultiplier = characters.Any(c => c.CurrentOrders.Any(o => o.Order != null && + o.Order.Identifier == userData.Item1.Identifier && + o.Order.TargetEntity == userData.Item1.TargetEntity)) ? 0.5f : 1f; + CreateNodeIcon(Vector2.One, optionElement.RectTransform, icon ?? order.SymbolSprite, order.Color * colorMultiplier); optionNodes.Add(new Tuple(optionElement, Keys.None)); } optionElements.Add(optionElement); @@ -2752,7 +2684,7 @@ namespace Barotrauma { if (!CanIssueOrders) { return false; } var o = userData as Tuple; - SetCharacterOrder(characterContext ?? GetCharacterForQuickAssignment(o.Item1), o.Item1, o.Item2, Character.Controlled); + SetCharacterOrder(characterContext ?? GetCharacterForQuickAssignment(o.Item1), o.Item1, o.Item2, CharacterInfo.HighestManualOrderPriority, Character.Controlled); DisableCommandUI(); return true; } @@ -2767,7 +2699,7 @@ namespace Barotrauma if (order.Prefab.OptionSprites.TryGetValue(option, out Sprite sprite)) { var showAssignmentTooltip = characterContext == null && !order.MustManuallyAssign && !order.TargetAllCharacters; - icon = CreateNodeIcon(node.RectTransform, sprite, order.Color, + icon = CreateNodeIcon(Vector2.One, node.RectTransform, sprite, order.Color, tooltip: characterContext != null ? optionName : optionName + "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.leftmouse") : TextManager.Get("input.rightmouse")) + ": " + TextManager.Get("commandui.quickassigntooltip") + "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.rightmouse") : TextManager.Get("input.leftmouse")) + ": " + TextManager.Get("commandui.manualassigntooltip")); @@ -2827,7 +2759,7 @@ namespace Barotrauma }; if (order.Item1.Prefab.OptionSprites.TryGetValue(order.Item2, out Sprite sprite)) { - CreateNodeIcon(clickedOptionNode.RectTransform, sprite, order.Item1.Color, tooltip: order.Item2); + CreateNodeIcon(Vector2.One, clickedOptionNode.RectTransform, sprite, order.Item1.Color, tooltip: order.Item2); } SetCenterNode(clickedOptionNode); node = null; @@ -2950,7 +2882,7 @@ namespace Barotrauma OnClicked = (_, userData) => { if (!CanIssueOrders) { return false; } - SetCharacterOrder(userData as Character, order.Item1, order.Item2, Character.Controlled); + SetCharacterOrder(userData as Character, order.Item1, order.Item2, CharacterInfo.HighestManualOrderPriority, Character.Controlled); DisableCommandUI(); return true; } @@ -2960,12 +2892,13 @@ namespace Barotrauma var jobColor = character.Info?.Job?.Prefab?.UIColor ?? Color.White; // Order icon + var topOrderInfo = character.GetCurrentOrderWithTopPriority(); GUIImage orderIcon; - if (!character.IsDismissed) + if (topOrderInfo.HasValue) { - orderIcon = new GUIImage(new RectTransform(new Vector2(1.2f), node.RectTransform, anchor: Anchor.Center), character.CurrentOrder.SymbolSprite, scaleToFit: true); - var tooltip = character.CurrentOrder.Name; - if (!string.IsNullOrWhiteSpace(character.CurrentOrderOption)) { tooltip += " (" + character.CurrentOrder.GetOptionName(character.CurrentOrderOption) + ")"; }; + orderIcon = new GUIImage(new RectTransform(new Vector2(1.2f), node.RectTransform, anchor: Anchor.Center), topOrderInfo.Value.Order.SymbolSprite, scaleToFit: true); + var tooltip = topOrderInfo.Value.Order.Name; + if (!string.IsNullOrWhiteSpace(topOrderInfo.Value.OrderOption)) { tooltip += " (" + topOrderInfo.Value.Order.GetOptionName(topOrderInfo.Value.OrderOption) + ")"; }; orderIcon.ToolTip = tooltip; } else @@ -3030,11 +2963,31 @@ namespace Barotrauma } } - private GUIImage CreateNodeIcon(RectTransform parent, Sprite sprite, Color color, string tooltip = null) + private GUIImage CreateNodeIcon(Vector2 relativeSize, RectTransform parent, Sprite sprite, Color color, string tooltip = null) { // Icon return new GUIImage( - new RectTransform(Vector2.One, parent), + new RectTransform(relativeSize, parent), + sprite, + scaleToFit: true) + { + Color = color * nodeColorMultiplier, + HoverColor = color, + PressedColor = color, + SelectedColor = color, + ToolTip = tooltip, + UserData = "colorsource" + }; + } + + /// + /// Create node icon with a fixed absolute size + /// + private GUIImage CreateNodeIcon(Point absoluteSize, RectTransform parent, Sprite sprite, Color color, string tooltip = null) + { + // Icon + return new GUIImage( + new RectTransform(absoluteSize, parent: parent) { IsFixedSize = true }, sprite, scaleToFit: true) { @@ -3236,18 +3189,20 @@ namespace Barotrauma #endif if (order.Identifier == dismissedOrderPrefab.Identifier) { - return characters.FindAll(c => !c.IsDismissed).OrderBy(c => c.Info.DisplayName).ToList(); + return characters.Union(GetOrderableFriendlyNPCs()).Where(c => !c.IsDismissed).OrderBy(c => c.Info.DisplayName).ToList(); } return GetCharactersSortedForOrder(order, order.Identifier != "follow").ToList(); } private IEnumerable GetCharactersSortedForOrder(Order order, bool includeSelf) { - return characters.FindAll(c => Character.Controlled == null || ((includeSelf || c != Character.Controlled) && c.TeamID == Character.Controlled.TeamID)) + return characters.Where(c => Character.Controlled == null || ((includeSelf || c != Character.Controlled) && c.TeamID == Character.Controlled.TeamID)).Union(GetOrderableFriendlyNPCs()) // 1. Prioritize those who are on the same submarine than the controlled character .OrderByDescending(c => Character.Controlled == null || c.Submarine == Character.Controlled.Submarine) - // 2. Prioritize those who are already ordered to operate the item target of the new 'operate' order, or given the same maintenance order as now issued - .ThenByDescending(c => c.CurrentOrder != null && c.CurrentOrder.Identifier == order.Identifier && (order.Category == OrderCategory.Maintenance || (order.Category == OrderCategory.Operate && c.CurrentOrder.TargetSpatialEntity == order.TargetSpatialEntity))) + // 2. Prioritize those who have been given the same maintenance or operate order as now issued + .ThenByDescending(c => c.CurrentOrders.Any(o => + o.Order != null && o.Order.Identifier == order.Identifier && + (order.Category == OrderCategory.Maintenance || order.Category == OrderCategory.Operate))) // 3. Prioritize those with the appropriate job for the order .ThenByDescending(c => order.HasAppropriateJob(c)) // 4. Prioritize bots over player controlled characters @@ -3258,6 +3213,12 @@ namespace Barotrauma .ThenByDescending(c => c.GetSkillLevel(order.AppropriateSkill)); } + private IEnumerable GetOrderableFriendlyNPCs() + { + return crewList.Content.Children.Where(c => c.UserData is Character character && character.TeamID == CharacterTeamType.FriendlyNPC).Select(c => (Character)c.UserData); + } + + #endregion #endregion @@ -3339,6 +3300,7 @@ namespace Barotrauma characters.Clear(); crewList.ClearChildren(); + GUIContextMenu.CurrentContextMenu = null; } public void Reset() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index c29550dcf..5b98cf1f0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -18,6 +18,8 @@ namespace Barotrauma protected Color overlayTextColor; protected Sprite overlaySprite; + private TransitionType prevCampaignUIAutoOpenType; + protected GUIButton endRoundButton; public GUIButton ReadyCheckButton; @@ -53,19 +55,26 @@ namespace Barotrauma { chatBox.ToggleOpen = wasChatBoxOpen; } + if (!value && CampaignUI?.SelectedTab == InteractionType.PurchaseSub) + { + SubmarinePreview.Close(); + } showCampaignUI = value; } } public override void ShowStartMessage() { - if (Mission == null) return; - - new GUIMessageBox(Mission.Name, Mission.Description, new string[0], type: GUIMessageBox.Type.InGame, icon: Mission.Prefab.Icon) + foreach (Mission mission in Missions) { - IconColor = Mission.Prefab.IconColor, - UserData = "missionstartmessage" - }; + new GUIMessageBox( + mission.Prefab.IsSideObjective ? TextManager.AddPunctuation(':', TextManager.Get("sideobjective"), mission.Name) : mission.Name, + mission.Description, new string[0], type: GUIMessageBox.Type.InGame, icon: mission.Prefab.Icon, parseRichText: true) + { + IconColor = mission.Prefab.IconColor, + UserData = "missionstartmessage" + }; + } } /// @@ -119,22 +128,22 @@ namespace Barotrauma { var backgroundSprite = GUI.Style.GetComponentStyle("CommandBackground").GetDefaultSprite(); Vector2 centerPos = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) / 2; + string wrappedText = ToolBox.WrapText(overlayText, GameMain.GraphicsWidth / 3, GUI.Font); + Vector2 textSize = GUI.Font.MeasureString(wrappedText); + Vector2 textPos = centerPos - textSize / 2; backgroundSprite.Draw(spriteBatch, centerPos, Color.White * (overlayTextColor.A / 255.0f), origin: backgroundSprite.size / 2, rotate: 0.0f, - scale: new Vector2(1.5f, 0.7f) * (GameMain.GraphicsWidth / 3 / backgroundSprite.size.X)); + scale: new Vector2(GameMain.GraphicsWidth / 2 / backgroundSprite.size.X, textSize.Y / backgroundSprite.size.Y * 1.5f)); - string wrappedText = ToolBox.WrapText(overlayText, GameMain.GraphicsWidth / 3, GUI.Font); - Vector2 textSize = GUI.Font.MeasureString(wrappedText); - Vector2 textPos = centerPos - textSize / 2; GUI.DrawString(spriteBatch, textPos + Vector2.One, wrappedText, Color.Black * (overlayTextColor.A / 255.0f)); GUI.DrawString(spriteBatch, textPos, wrappedText, overlayTextColor); if (!string.IsNullOrEmpty(overlayTextBottom)) { - Vector2 bottomTextPos = centerPos + new Vector2(0.0f, textSize.Y + 30 * GUI.Scale) - GUI.Font.MeasureString(overlayTextBottom) / 2; + Vector2 bottomTextPos = centerPos + new Vector2(0.0f, textSize.Y / 2 + 40 * GUI.Scale) - GUI.Font.MeasureString(overlayTextBottom) / 2; GUI.DrawString(spriteBatch, bottomTextPos + Vector2.One, overlayTextBottom, Color.Black * (overlayTextColor.A / 255.0f)); GUI.DrawString(spriteBatch, bottomTextPos, overlayTextBottom, overlayTextColor); } @@ -147,7 +156,7 @@ namespace Barotrauma if (ReadyCheckButton != null) { ReadyCheckButton.Visible = false; } return; } - if (Submarine.MainSub == null) { return; } + if (Submarine.MainSub == null || Level.Loaded == null) { return; } endRoundButton.Visible = false; var availableTransition = GetAvailableTransition(out _, out Submarine leavingSub); @@ -158,7 +167,8 @@ namespace Barotrauma case TransitionType.ProgressToNextEmptyLocation: if (Level.Loaded.EndOutpost == null || !Level.Loaded.EndOutpost.DockedTo.Contains(leavingSub)) { - buttonText = TextManager.GetWithVariable("EnterLocation", "[locationname]", Level.Loaded.EndLocation?.Name ?? "[ERROR]"); + string textTag = availableTransition == TransitionType.ProgressToNextLocation ? "EnterLocation" : "EnterEmptyLocation"; + buttonText = TextManager.GetWithVariable(textTag, "[locationname]", Level.Loaded.EndLocation?.Name ?? "[ERROR]"); endRoundButton.Visible = !ForceMapUI && !ShowCampaignUI; } break; @@ -170,7 +180,8 @@ namespace Barotrauma case TransitionType.ReturnToPreviousEmptyLocation: if (Level.Loaded.StartOutpost == null || !Level.Loaded.StartOutpost.DockedTo.Contains(leavingSub)) { - buttonText = TextManager.GetWithVariable("EnterLocation", "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); + string textTag = availableTransition == TransitionType.ReturnToPreviousLocation ? "EnterLocation" : "EnterEmptyLocation"; + buttonText = TextManager.GetWithVariable(textTag, "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); endRoundButton.Visible = !ForceMapUI && !ShowCampaignUI; } @@ -194,7 +205,20 @@ namespace Barotrauma if (endRoundButton.Visible) { - if (!AllowedToEndRound()) { buttonText = TextManager.Get("map"); } + if (!AllowedToEndRound()) + { + buttonText = TextManager.Get("map"); + } + else if (prevCampaignUIAutoOpenType != availableTransition && + (availableTransition == TransitionType.ProgressToNextEmptyLocation || availableTransition == TransitionType.ReturnToPreviousEmptyLocation)) + { + HintManager.OnAvailableTransition(availableTransition); + //opening the campaign map pauses the game and prevents HintManager from running -> update it manually to get the hint to show up immediately + HintManager.Update(); + Map.SelectLocation(-1); + endRoundButton.OnClicked(EndRoundButton, null); + prevCampaignUIAutoOpenType = availableTransition; + } endRoundButton.Text = ToolBox.LimitString(buttonText, endRoundButton.Font, endRoundButton.Rect.Width - 5); if (endRoundButton.Text != buttonText) { @@ -209,15 +233,20 @@ namespace Barotrauma { endRoundButton.RectTransform.ScreenSpaceOffset = new Point(0, Character.Controlled.CharacterHealth.SuicideButton.Rect.Height); } + else if (GameMain.Client != null && GameMain.Client.IsFollowSubTickBoxVisible) + { + endRoundButton.RectTransform.ScreenSpaceOffset = new Point(0, HUDLayoutSettings.Padding + GameMain.Client.FollowSubTickBox.Rect.Height); + } else { endRoundButton.RectTransform.ScreenSpaceOffset = Point.Zero; } } endRoundButton.DrawManually(spriteBatch); - if (this is MultiPlayerCampaign) + if (this is MultiPlayerCampaign && ReadyCheckButton != null) { - ReadyCheckButton?.DrawManually(spriteBatch); + ReadyCheckButton.RectTransform.ScreenSpaceOffset = endRoundButton.RectTransform.ScreenSpaceOffset; + ReadyCheckButton.DrawManually(spriteBatch); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index 6a189aa73..cd95768e9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -227,6 +227,11 @@ namespace Barotrauma float timer = 0.0f; while (timer < textDuration) { + if (GameMain.GameSession == null || Screen.Selected != GameMain.GameScreen) + { + GUI.DisableHUD = false; + yield return CoroutineStatus.Success; + } // Try to grab the controlled here to prevent inputs, assigned late on multiplayer if (Character.Controlled != null) { @@ -239,11 +244,20 @@ namespace Barotrauma timer = Math.Min(timer + CoroutineManager.DeltaTime, textDuration); yield return CoroutineStatus.Running; } + var outpost = GameMain.GameSession.Level.StartOutpost; + var borders = outpost.GetDockedBorders(); + borders.Location += outpost.WorldPosition.ToPoint(); + GameMain.GameScreen.Cam.Position = new Vector2(borders.X + borders.Width / 2, borders.Y - borders.Height / 2); + float startZoom = 0.8f / + ((float)Math.Max(borders.Width, borders.Height) / (float)GameMain.GameScreen.Cam.Resolution.X); + GameMain.GameScreen.Cam.MinZoom = Math.Min(startZoom, GameMain.GameScreen.Cam.MinZoom); var transition = new CameraTransition(prevControlled, GameMain.GameScreen.Cam, null, null, fadeOut: false, - duration: 5, - startZoom: 1.5f, endZoom: 1.0f) + losFadeIn: true, + waitDuration: 1, + panDuration: 5, + startZoom: startZoom, endZoom: 1.0f) { AllowInterrupt = true, RemoveControlFromCharacter = false @@ -274,7 +288,8 @@ namespace Barotrauma var transition = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, null, null, fadeOut: false, - duration: 5, + losFadeIn: true, + panDuration: 5, startZoom: 0.5f, endZoom: 1.0f) { AllowInterrupt = true, @@ -311,6 +326,7 @@ namespace Barotrauma Level prevLevel = Level.Loaded; bool success = CrewManager.GetCharacters().Any(c => !c.IsDead); + GUI.SetSavingIndicatorState(success); crewDead = false; var continueButton = GameMain.GameSession.RoundSummary?.ContinueButton; @@ -327,7 +343,7 @@ namespace Barotrauma var endTransition = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, null, Alignment.Center, fadeOut: false, - duration: EndTransitionDuration); + panDuration: EndTransitionDuration); GameMain.Client.EndCinematic = endTransition; Location portraitLocation = Map?.SelectedLocation ?? Map?.CurrentLocation ?? Level.Loaded?.StartLocation; @@ -335,7 +351,7 @@ namespace Barotrauma { overlaySprite = portraitLocation.Type.GetPortrait(portraitLocation.PortraitId); } - float fadeOutDuration = endTransition.Duration; + float fadeOutDuration = endTransition.PanDuration; float t = 0.0f; while (t < fadeOutDuration || endTransition.Running) { @@ -368,6 +384,7 @@ namespace Barotrauma } } + GUI.SetSavingIndicatorState(false); yield return CoroutineStatus.Success; } @@ -420,7 +437,7 @@ namespace Barotrauma { //wasn't initially docked (sub doesn't have a docking port?) // -> choose a destination when the sub is far enough from the start outpost - if (!Submarine.MainSub.AtStartPosition) + if (!Submarine.MainSub.AtStartExit) { ForceMapUI = true; if (CampaignUI == null) { InitCampaignUI(); } @@ -437,8 +454,10 @@ namespace Barotrauma { ShowCampaignUI = false; } + HintManager.OnAvailableTransition(transitionType); } } + public override void End(TransitionType transitionType = TransitionType.None) { base.End(transitionType); @@ -496,7 +515,7 @@ namespace Barotrauma var transition = new CameraTransition(endObject ?? Submarine.MainSub, GameMain.GameScreen.Cam, null, Alignment.Center, fadeOut: true, - duration: 10, + panDuration: 10, startZoom: null, endZoom: 0.2f); while (transition.Running) @@ -649,7 +668,7 @@ namespace Barotrauma { string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer); - GameMain.GameSession = new GameSession(null, savePath, GameModePreset.MultiPlayerCampaign, mapSeed); + GameMain.GameSession = new GameSession(null, savePath, GameModePreset.MultiPlayerCampaign, CampaignSettings.Unsure, mapSeed); campaign = (MultiPlayerCampaign)GameMain.GameSession.GameMode; campaign.CampaignID = campaignID; GameMain.NetLobbyScreen.ToggleCampaignMode(true); @@ -711,13 +730,20 @@ namespace Barotrauma DebugConsole.ThrowError($"Error when receiving campaign data from the server: mission prefab \"{availableMission.First}\" not found."); continue; } - if (availableMission.Second < 0 || availableMission.Second >= campaign.Map.CurrentLocation.Connections.Count) + if (availableMission.Second == 255) { - DebugConsole.ThrowError($"Error when receiving campaign data from the server: connection index for mission \"{availableMission.First}\" out of range (index: {availableMission.Second}, current location: {campaign.Map.CurrentLocation.Name}, connections: {campaign.Map.CurrentLocation.Connections.Count})."); - continue; + campaign.Map.CurrentLocation.UnlockMission(missionPrefab); + } + else + { + if (availableMission.Second < 0 || availableMission.Second >= campaign.Map.CurrentLocation.Connections.Count) + { + DebugConsole.ThrowError($"Error when receiving campaign data from the server: connection index for mission \"{availableMission.First}\" out of range (index: {availableMission.Second}, current location: {campaign.Map.CurrentLocation.Name}, connections: {campaign.Map.CurrentLocation.Connections.Count})."); + continue; + } + LocationConnection connection = campaign.Map.CurrentLocation.Connections[availableMission.Second]; + campaign.Map.CurrentLocation.UnlockMission(missionPrefab, connection); } - LocationConnection connection = campaign.Map.CurrentLocation.Connections[availableMission.Second]; - campaign.Map.CurrentLocation.UnlockMission(missionPrefab, connection); } GameMain.NetLobbyScreen.ToggleCampaignMode(true); @@ -770,16 +796,29 @@ namespace Barotrauma { pendingHires.Add(msg.ReadInt32()); } - - bool validateHires = msg.ReadBoolean(); + + ushort hiredLength = msg.ReadUInt16(); + List hiredCharacters = new List(); + for (int i = 0; i < hiredLength; i++) + { + CharacterInfo hired = CharacterInfo.ClientRead("human", msg); + hired.Salary = msg.ReadInt32(); + hiredCharacters.Add(hired); + } + + bool renameCrewMember = msg.ReadBoolean(); + if (renameCrewMember) + { + int renamedIdentifier = msg.ReadInt32(); + string newName = msg.ReadString(); + CharacterInfo renamedCharacter = CrewManager.CharacterInfos.FirstOrDefault(info => info.GetIdentifierUsingOriginalName() == renamedIdentifier); + if (renamedCharacter != null) { CrewManager.RenameCharacter(renamedCharacter, newName); } + } bool fireCharacter = msg.ReadBoolean(); - - int firedIdentifier = -1; - if (fireCharacter) { firedIdentifier = msg.ReadInt32(); } - if (fireCharacter) { + int firedIdentifier = msg.ReadInt32(); CharacterInfo firedCharacter = CrewManager.CharacterInfos.FirstOrDefault(info => info.GetIdentifier() == firedIdentifier); // this one might and is allowed to be null since the character is already fired on the original sender's game if (firedCharacter != null) { CrewManager.FireCharacter(firedCharacter); } @@ -787,10 +826,10 @@ namespace Barotrauma if (map?.CurrentLocation?.HireManager != null && CampaignUI?.CrewManagement != null) { - CampaignUI?.CrewManagement?.SetHireables(map.CurrentLocation, availableHires); - if (validateHires) { CampaignUI?.CrewManagement.ValidatePendingHires(); } - CampaignUI?.CrewManagement?.SetPendingHires(pendingHires, map?.CurrentLocation); - if (fireCharacter) { CampaignUI?.CrewManagement.UpdateCrew(); } + CampaignUI.CrewManagement.SetHireables(map.CurrentLocation, availableHires); + if (hiredCharacters.Any()) { CampaignUI.CrewManagement.ValidateHires(hiredCharacters); } + CampaignUI.CrewManagement.SetPendingHires(pendingHires, map.CurrentLocation); + if (renameCrewMember || fireCharacter) { CampaignUI.CrewManagement.UpdateCrew(); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index 71364cf6b..d3913d80f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -22,7 +22,7 @@ namespace Barotrauma { if (CoroutineManager.IsCoroutineRunning("LevelTransition") || CoroutineManager.IsCoroutineRunning("SubmarineTransition") || gameOver) { return; } - if (PlayerInput.RightButtonClicked() || + if (PlayerInput.SecondaryMouseButtonClicked() || PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Escape)) { ShowCampaignUI = false; @@ -57,11 +57,12 @@ namespace Barotrauma /// /// Instantiates a new single player campaign /// - private SinglePlayerCampaign(string mapSeed) : base(GameModePreset.SinglePlayerCampaign) + private SinglePlayerCampaign(string mapSeed, CampaignSettings settings) : base(GameModePreset.SinglePlayerCampaign) { CampaignMetadata = new CampaignMetadata(this); UpgradeManager = new UpgradeManager(this); - map = new Map(this, mapSeed); + map = new Map(this, mapSeed, settings); + Settings = settings; foreach (JobPrefab jobPrefab in JobPrefab.Prefabs) { for (int i = 0; i < jobPrefab.InitialCount; i++) @@ -85,11 +86,14 @@ namespace Barotrauma { switch (subElement.Name.ToString().ToLowerInvariant()) { + case "campaignsettings": + Settings = new CampaignSettings(subElement); + break; case "crew": GameMain.GameSession.CrewManager = new CrewManager(subElement, true); break; case "map": - map = Map.Load(this, subElement); + map = Map.Load(this, subElement, Settings); break; case "metadata": CampaignMetadata = new CampaignMetadata(this, subElement); @@ -141,9 +145,9 @@ namespace Barotrauma /// /// Start a completely new single player campaign /// - public static SinglePlayerCampaign StartNew(string mapSeed, SubmarineInfo selectedSub) + public static SinglePlayerCampaign StartNew(string mapSeed, SubmarineInfo selectedSub, CampaignSettings settings) { - var campaign = new SinglePlayerCampaign(mapSeed); + var campaign = new SinglePlayerCampaign(mapSeed, settings); return campaign; } @@ -213,6 +217,7 @@ namespace Barotrauma if (!savedOnStart) { + GUI.SetSavingIndicatorState(true); SaveUtil.SaveGame(GameMain.GameSession.SavePath); savedOnStart = true; } @@ -224,6 +229,8 @@ namespace Barotrauma { PetBehavior.LoadPets(petsElement); } + + GUI.DisableSavingIndicatorDelayed(); } protected override void LoadInitialLevel() @@ -290,15 +297,29 @@ namespace Barotrauma break; } } + if (GameMain.GameSession == null) + { + GUI.DisableHUD = false; + yield return CoroutineStatus.Success; + } overlayTextColor = Color.Lerp(Color.Transparent, Color.White, (timer - 1.0f) / fadeInDuration); timer = Math.Min(timer + CoroutineManager.DeltaTime, textDuration); yield return CoroutineStatus.Running; } + var outpost = GameMain.GameSession.Level.StartOutpost; + var borders = outpost.GetDockedBorders(); + borders.Location += outpost.WorldPosition.ToPoint(); + GameMain.GameScreen.Cam.Position = new Vector2(borders.X + borders.Width / 2, borders.Y - borders.Height / 2); + float startZoom = 0.8f / + ((float)Math.Max(borders.Width, borders.Height) / (float)GameMain.GameScreen.Cam.Resolution.X); + GameMain.GameScreen.Cam.MinZoom = Math.Min(startZoom, GameMain.GameScreen.Cam.MinZoom); var transition = new CameraTransition(prevControlled, GameMain.GameScreen.Cam, null, null, fadeOut: false, - duration: 5, - startZoom: 1.5f, endZoom: 1.0f) + losFadeIn: true, + waitDuration: 1, + panDuration: 5, + startZoom: startZoom, endZoom: 1.0f) { AllowInterrupt = true, RemoveControlFromCharacter = false @@ -323,19 +344,13 @@ namespace Barotrauma else { ISpatialEntity transitionTarget; - if (prevControlled != null) - { - transitionTarget = prevControlled; - } - else - { - transitionTarget = Submarine.MainSub; - } + transitionTarget = (ISpatialEntity)prevControlled ?? Submarine.MainSub; var transition = new CameraTransition(transitionTarget, GameMain.GameScreen.Cam, null, null, fadeOut: false, - duration: 5, + losFadeIn: prevControlled != null, + panDuration: 5, startZoom: 0.5f, endZoom: 1.0f) { AllowInterrupt = true, @@ -370,11 +385,9 @@ namespace Barotrauma bool success = CrewManager.GetCharacters().Any(c => !c.IsDead); SoundPlayer.OverrideMusicType = success ? "endround" : "crewdead"; SoundPlayer.OverrideMusicDuration = 18.0f; + GUI.SetSavingIndicatorState(success); crewDead = false; - LevelData lvlData = GameMain.GameSession.LevelData; - bool beaconActive = GameMain.GameSession.Level.CheckBeaconActive(); - GameMain.GameSession.EndRound("", traitorResults, transitionType); var continueButton = GameMain.GameSession.RoundSummary?.ContinueButton; RoundSummary roundSummary = null; @@ -408,13 +421,13 @@ namespace Barotrauma var endTransition = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, null, transitionType == TransitionType.LeaveLocation ? Alignment.BottomCenter : Alignment.Center, fadeOut: false, - duration: EndTransitionDuration); + panDuration: EndTransitionDuration); GUI.ClearMessages(); Location portraitLocation = Map.SelectedLocation ?? Map.CurrentLocation; overlaySprite = portraitLocation.Type.GetPortrait(portraitLocation.PortraitId); - float fadeOutDuration = endTransition.Duration; + float fadeOutDuration = endTransition.PanDuration; float t = 0.0f; while (t < fadeOutDuration || endTransition.Running) { @@ -459,8 +472,6 @@ namespace Barotrauma } } - lvlData.IsBeaconActive = beaconActive; - SaveUtil.SaveGame(GameMain.GameSession.SavePath); } else @@ -484,6 +495,7 @@ namespace Barotrauma overlayColor = Color.Transparent; }); + GUI.SetSavingIndicatorState(false); yield return CoroutineStatus.Success; } @@ -510,7 +522,7 @@ namespace Barotrauma var transition = new CameraTransition(endObject ?? Submarine.MainSub, GameMain.GameScreen.Cam, null, Alignment.Center, fadeOut: true, - duration: 10, + panDuration: 10, startZoom: null, endZoom: 0.2f); while (transition.Running) @@ -530,6 +542,8 @@ namespace Barotrauma if (CoroutineManager.IsCoroutineRunning("LevelTransition") || CoroutineManager.IsCoroutineRunning("SubmarineTransition") || gameOver) { return; } base.Update(deltaTime); + + Map?.Radiation?.UpdateRadiation(deltaTime); if (PlayerInput.SecondaryMouseButtonClicked() || PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Escape)) @@ -583,7 +597,7 @@ namespace Barotrauma { //wasn't initially docked (sub doesn't have a docking port?) // -> choose a destination when the sub is far enough from the start outpost - if (!Submarine.MainSub.AtStartPosition) + if (!Submarine.MainSub.AtStartExit) { ForceMapUI = true; CampaignUI.SelectTab(InteractionType.Map); @@ -611,6 +625,7 @@ namespace Barotrauma { ShowCampaignUI = false; } + HintManager.OnAvailableTransition(transitionType); } if (!crewDead) @@ -632,9 +647,9 @@ namespace Barotrauma if (nextLevel == null) { //no level selected -> force the player to select one + ForceMapUI = true; CampaignUI.SelectTab(InteractionType.Map); map.SelectLocation(-1); - ForceMapUI = true; return false; } else if (transitionType == TransitionType.ProgressToNextEmptyLocation) @@ -707,6 +722,7 @@ namespace Barotrauma new XAttribute("purchasedhullrepairs", PurchasedHullRepairs), new XAttribute("purchaseditemrepairs", PurchasedItemRepairs), new XAttribute("cheatsenabled", CheatsEnabled)); + modeElement.Add(Settings.Save()); //save and remove all items that are in someone's inventory so they don't get included in the sub file as well foreach (Character c in Character.CharacterList) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/BasicTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/BasicTutorial.cs index a98a699a6..7372def70 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/BasicTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/BasicTutorial.cs @@ -614,7 +614,7 @@ namespace Barotrauma.Tutorials GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; GameMain.LightManager.LosEnabled = false; - var cinematic = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, Alignment.CenterLeft, Alignment.CenterRight, duration: 5.0f); + var cinematic = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, Alignment.CenterLeft, Alignment.CenterRight, panDuration: 5.0f); while (cinematic.Running) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs index f757bce5c..023d89045 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs @@ -99,7 +99,7 @@ namespace Barotrauma.Tutorials captain_medicSpawnPos = Item.ItemList.Find(i => i.HasTag("captain_medicspawnpos")).WorldPosition; tutorial_submarineDoor = Item.ItemList.Find(i => i.HasTag("tutorial_submarinedoor")).GetComponent(); tutorial_submarineDoorLight = Item.ItemList.Find(i => i.HasTag("tutorial_submarinedoorlight")).GetComponent(); - var medicInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, "", JobPrefab.Get("medicaldoctor")); + var medicInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: JobPrefab.Get("medicaldoctor")); captain_medic = Character.Create(medicInfo, captain_medicSpawnPos, "medicaldoctor"); captain_medic.TeamID = CharacterTeamType.Team1; captain_medic.GiveJobItems(null); @@ -122,17 +122,17 @@ namespace Barotrauma.Tutorials SetDoorAccess(tutorial_lockedDoor_1, null, false); SetDoorAccess(tutorial_lockedDoor_2, null, false); - var mechanicInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, "", JobPrefab.Get("mechanic")); + var mechanicInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: JobPrefab.Get("mechanic")); captain_mechanic = Character.Create(mechanicInfo, WayPoint.GetRandom(SpawnType.Human, mechanicInfo.Job, Submarine.MainSub).WorldPosition, "mechanic"); captain_mechanic.TeamID = CharacterTeamType.Team1; captain_mechanic.GiveJobItems(); - var securityInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, "", JobPrefab.Get("securityofficer")); + var securityInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: JobPrefab.Get("securityofficer")); captain_security = Character.Create(securityInfo, WayPoint.GetRandom(SpawnType.Human, securityInfo.Job, Submarine.MainSub).WorldPosition, "securityofficer"); captain_security.TeamID = CharacterTeamType.Team1; captain_security.GiveJobItems(); - var engineerInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, "", JobPrefab.Get("engineer")); + var engineerInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: JobPrefab.Get("engineer")); captain_engineer = Character.Create(engineerInfo, WayPoint.GetRandom(SpawnType.Human, engineerInfo.Job, Submarine.MainSub).WorldPosition, "engineer"); captain_engineer.TeamID = CharacterTeamType.Team1; captain_engineer.GiveJobItems(); @@ -249,7 +249,7 @@ namespace Barotrauma.Tutorials { //captain_navConsoleCustomInterface.HighlightElement(0, uiHighlightColor, duration: 1.0f, pulsateAmount: 0.0f); yield return new WaitForSeconds(1.0f, false); - } while (!Submarine.MainSub.AtEndPosition || !Submarine.MainSub.DockedTo.Any()); + } while (!Submarine.MainSub.AtEndExit || !Submarine.MainSub.DockedTo.Any()); RemoveCompletedObjective(segments[6]); yield return new WaitForSeconds(3f, false); GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.GetWithVariable("Captain.Radio.Complete", "[OUTPOSTNAME]", GameMain.GameSession.EndLocation.Name), ChatMessageType.Radio, null); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs index a64f5e678..2dab531d3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs @@ -78,7 +78,7 @@ namespace Barotrauma.Tutorials var patientHull2 = WayPoint.WayPointList.Find(wp => wp.IdCardDesc == "airlock").CurrentHull; medBay = WayPoint.WayPointList.Find(wp => wp.IdCardDesc == "medbay").CurrentHull; - var assistantInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, "", JobPrefab.Get("assistant")); + var assistantInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: JobPrefab.Get("assistant")); patient1 = Character.Create(assistantInfo, patientHull1.WorldPosition, "1"); patient1.TeamID = CharacterTeamType.Team1; patient1.GiveJobItems(null); @@ -86,26 +86,26 @@ namespace Barotrauma.Tutorials patient1.AddDamage(patient1.WorldPosition, new List() { new Affliction(AfflictionPrefab.Burn, 15.0f) }, stun: 0, playSound: false); patient1.AIController.Enabled = false; - assistantInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, "", JobPrefab.Get("assistant")); + assistantInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: JobPrefab.Get("assistant")); patient2 = Character.Create(assistantInfo, patientHull2.WorldPosition, "2"); patient2.TeamID = CharacterTeamType.Team1; patient2.GiveJobItems(null); patient2.CanSpeak = false; patient2.AIController.Enabled = false; - var mechanicInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, "", JobPrefab.Get("engineer")); + var mechanicInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: JobPrefab.Get("engineer")); var subPatient1 = Character.Create(mechanicInfo, WayPoint.GetRandom(SpawnType.Human, mechanicInfo.Job, Submarine.MainSub).WorldPosition, "3"); subPatient1.TeamID = CharacterTeamType.Team1; subPatient1.AddDamage(patient1.WorldPosition, new List() { new Affliction(AfflictionPrefab.Burn, 40.0f) }, stun: 0, playSound: false); subPatients.Add(subPatient1); - var securityInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, "", JobPrefab.Get("securityofficer")); + var securityInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: JobPrefab.Get("securityofficer")); var subPatient2 = Character.Create(securityInfo, WayPoint.GetRandom(SpawnType.Human, securityInfo.Job, Submarine.MainSub).WorldPosition, "3"); subPatient2.TeamID = CharacterTeamType.Team1; subPatient2.AddDamage(patient1.WorldPosition, new List() { new Affliction(AfflictionPrefab.InternalDamage, 40.0f) }, stun: 0, playSound: false); subPatients.Add(subPatient2); - var engineerInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, "", JobPrefab.Get("engineer")); + var engineerInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: JobPrefab.Get("engineer")); var subPatient3 = Character.Create(securityInfo, WayPoint.GetRandom(SpawnType.Human, engineerInfo.Job, Submarine.MainSub).WorldPosition, "3"); subPatient3.TeamID = CharacterTeamType.Team1; subPatient3.AddDamage(patient1.WorldPosition, new List() { new Affliction(AfflictionPrefab.Burn, 20.0f) }, stun: 0, playSound: false); @@ -283,7 +283,7 @@ namespace Barotrauma.Tutorials doctor.RemoveActiveObjectiveEntity(patient1); TriggerTutorialSegment(3, GameMain.Config.KeyBindText(InputType.Command)); // Get the patient to medbay - while (patient1.CurrentOrder == null || patient1.CurrentOrder.Identifier != "follow") + while (patient1.GetCurrentOrderWithTopPriority()?.Order?.Identifier != "follow") { // TODO: Rework order highlighting for new command UI // GameMain.GameSession.CrewManager.HighlightOrderButton(patient1, "follow", highlightColor, new Vector2(5, 5)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs index e88f1e314..2a68e6611 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs @@ -317,7 +317,8 @@ namespace Barotrauma.Tutorials yield return new WaitForSeconds(2f, false); TriggerTutorialSegment(4, GameMain.Config.KeyBindText(InputType.Select), GameMain.Config.KeyBindText(InputType.Shoot), GameMain.Config.KeyBindText(InputType.Deselect)); // Kill hammerhead officer_hammerhead = SpawnMonster("hammerhead", officer_hammerheadSpawnPos); - ((EnemyAIController)officer_hammerhead.AIController).StayInsideLevel = false; + officer_hammerhead.Params.AI.AvoidAbyss = false; + officer_hammerhead.Params.AI.StayInAbyss = false; officer_hammerhead.AIController.SelectTarget(officer.AiTarget); SetHighlight(officer_coilgunPeriscope, true); float originalDistance = Vector2.Distance(officer_coilgunPeriscope.WorldPosition, officer_hammerheadSpawnPos); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs index c25686093..6f90823d7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs @@ -62,7 +62,7 @@ namespace Barotrauma.Tutorials yield return CoroutineStatus.Running; - GameMain.GameSession = new GameSession(subInfo, GameModePreset.Tutorial, missionPrefab: null); + GameMain.GameSession = new GameSession(subInfo, GameModePreset.Tutorial, missionPrefabs: null); (GameMain.GameSession.GameMode as TutorialMode).Tutorial = this; if (generationParams != null) @@ -110,7 +110,7 @@ namespace Barotrauma.Tutorials } CharacterInfo charInfo = configElement.Element("Character") == null ? - new CharacterInfo(CharacterPrefab.HumanSpeciesName, "", JobPrefab.Get("engineer")) : + new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: JobPrefab.Get("engineer")) : new CharacterInfo(configElement.Element("Character")); WayPoint wayPoint = GetSpawnPoint(charInfo); @@ -182,7 +182,8 @@ namespace Barotrauma.Tutorials protected bool HasOrder(Character character, string identifier, string option = null) { - if (character.CurrentOrder?.Identifier == identifier) + var currentOrderInfo = character.GetCurrentOrderWithTopPriority(); + if (currentOrderInfo?.Order?.Identifier == identifier) { if (option == null) { @@ -190,8 +191,7 @@ namespace Barotrauma.Tutorials } else { - HumanAIController humanAI = character.AIController as HumanAIController; - return humanAI.CurrentOrderOption == option; + return currentOrderInfo?.OrderOption == option; } } @@ -288,7 +288,7 @@ namespace Barotrauma.Tutorials yield return new WaitForSeconds(waitBeforeFade); - var endCinematic = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, null, Alignment.Center, duration: fadeOutTime); + var endCinematic = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, null, Alignment.Center, panDuration: fadeOutTime); currentTutorialCompleted = Completed = true; while (endCinematic.Running) yield return null; Stop(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs index bcb0f796d..6d880f5ef 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs @@ -535,15 +535,15 @@ namespace Barotrauma.Tutorials titleBlock.RectTransform.IsFixedSize = true; } - List richTextData = RichTextData.GetRichTextData(text, out text); + List richTextData = RichTextData.GetRichTextData(" " + text, out text); GUITextBlock textBlock; if (richTextData == null) { - textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), " " + text, wrap: true); + textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), text, wrap: true); } else { - textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), richTextData, " " + text, wrap: true); + textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), richTextData, text, wrap: true); } textBlock.RectTransform.IsFixedSize = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs index 54537e63a..90aae527a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; namespace Barotrauma { @@ -26,6 +27,7 @@ namespace Barotrauma if (tabMenu == null && GameMode is TutorialMode == false) { tabMenu = new TabMenu(); + HintManager.OnShowTabMenu(); } else { @@ -36,12 +38,106 @@ namespace Barotrauma return true; } + private GUILayoutGroup topLeftButtonGroup; + private GUIButton crewListButton, commandButton, tabMenuButton; + + private GUIComponent respawnInfoFrame, respawnButtonContainer; + private GUITextBlock respawnInfoText; + private GUITickBox respawnTickBox; + private GUILayoutGroup TopLeftButtonGroup; + private void CreateTopLeftButtons() + { + if (topLeftButtonGroup != null) + { + topLeftButtonGroup.RectTransform.Parent = null; + topLeftButtonGroup = null; + crewListButton = commandButton = tabMenuButton = null; + } + topLeftButtonGroup = new GUILayoutGroup(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.ButtonAreaTop, GUI.Canvas), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + AbsoluteSpacing = HUDLayoutSettings.Padding, + CanBeFocused = false + }; + topLeftButtonGroup.RectTransform.ParentChanged += (_) => + { + GameMain.Instance.ResolutionChanged -= CreateTopLeftButtons; + }; + int buttonHeight = GUI.IntScale(40); + Vector2 buttonSpriteSize = GUI.Style.GetComponentStyle("CrewListToggleButton").GetDefaultSprite().size; + int buttonWidth = (int)((buttonHeight / buttonSpriteSize.Y) * buttonSpriteSize.X); + Point buttonSize = new Point(buttonWidth, buttonHeight); + crewListButton = new GUIButton(new RectTransform(buttonSize, parent: topLeftButtonGroup.RectTransform), style: "CrewListToggleButton") + { + ToolTip = TextManager.GetWithVariable("hudbutton.crewlist", "[key]", GameMain.Config.KeyBindText(InputType.CrewOrders)), + OnClicked = (GUIButton btn, object userdata) => + { + if (CrewManager == null) { return false; } + CrewManager.IsCrewMenuOpen = !CrewManager.IsCrewMenuOpen; + return true; + } + }; + commandButton = new GUIButton(new RectTransform(buttonSize, parent: topLeftButtonGroup.RectTransform), style: "CommandButton") + { + ToolTip = TextManager.GetWithVariable("hudbutton.commandinterface", "[key]", GameMain.Config.KeyBindText(InputType.Command)), + OnClicked = (button, userData) => + { + if (CrewManager == null) { return false; } + CrewManager.ToggleCommandUI(); + return true; + } + }; + tabMenuButton = new GUIButton(new RectTransform(buttonSize, parent: topLeftButtonGroup.RectTransform), style: "TabMenuButton") + { + ToolTip = TextManager.GetWithVariable("hudbutton.tabmenu", "[key]", GameMain.Config.KeyBindText(InputType.InfoTab)), + OnClicked = (button, userData) => + { + return ToggleTabMenu(); + } + }; + GameMain.Instance.ResolutionChanged += CreateTopLeftButtons; + + respawnInfoFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 1.0f), parent: topLeftButtonGroup.RectTransform) + { MaxSize = new Point(HUDLayoutSettings.ButtonAreaTop.Width / 3, int.MaxValue) }, style: null) + { + Visible = false + }; + respawnInfoText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), respawnInfoFrame.RectTransform), "", wrap: true); + respawnButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), respawnInfoFrame.RectTransform, Anchor.CenterRight), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + AbsoluteSpacing = HUDLayoutSettings.Padding, + Stretch = true + }; + respawnTickBox = new GUITickBox(new RectTransform(Vector2.One * 0.9f, respawnButtonContainer.RectTransform, Anchor.Center), TextManager.Get("respawnquestionpromptrespawn")) + { + ToolTip = TextManager.Get("respawnquestionprompt"), + OnSelected = (tickbox) => + { + GameMain.Client?.SendRespawnPromptResponse(waitForNextRoundRespawn: !tickbox.Selected); + return true; + } + }; + } + public void AddToGUIUpdateList() { - if (GUI.DisableHUD) return; + if (GUI.DisableHUD) { return; } GameMode?.AddToGUIUpdateList(); tabMenu?.AddToGUIUpdateList(); + if ((!(GameMode is CampaignMode campaign) || (!campaign.ForceMapUI && !campaign.ShowCampaignUI)) && + !CoroutineManager.IsCoroutineRunning("LevelTransition") && !CoroutineManager.IsCoroutineRunning("SubmarineTransition")) + { + if (topLeftButtonGroup == null) + { + CreateTopLeftButtons(); + } + crewListButton.Selected = CrewManager != null && CrewManager.IsCrewMenuOpen; + commandButton.Selected = CrewManager.IsCommandInterfaceOpen; + commandButton.Enabled = CrewManager.CanIssueOrders; + tabMenuButton.Selected = IsTabMenuOpen; + topLeftButtonGroup.AddToGUIUpdateList(); + } + if (GameMain.NetworkMember != null) { GameMain.NetLobbyScreen?.HeadSelectionList?.AddToGUIUpdateList(); @@ -55,7 +151,7 @@ namespace Barotrauma if (tabMenu == null) { - if (PlayerInput.KeyHit(InputType.InfoTab) && GUI.KeyboardDispatcher.Subscriber is GUITextBox == false) + if (PlayerInput.KeyHit(InputType.InfoTab) && !(GUI.KeyboardDispatcher.Subscriber is GUITextBox)) { ToggleTabMenu(); } @@ -63,8 +159,8 @@ namespace Barotrauma else { tabMenu.Update(); - - if (PlayerInput.KeyHit(InputType.InfoTab) && GUI.KeyboardDispatcher.Subscriber is GUITextBox == false) + if ((PlayerInput.KeyHit(InputType.InfoTab) || PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Escape)) && + !(GUI.KeyboardDispatcher.Subscriber is GUITextBox)) { ToggleTabMenu(); } @@ -88,6 +184,19 @@ namespace Barotrauma } } } + + HintManager.Update(); + } + + public void SetRespawnInfo(bool visible, string text, Color textColor, bool buttonsVisible, bool waitForNextRoundRespawn) + { + if (topLeftButtonGroup == null) { return; } + respawnInfoFrame.Visible = visible; + if (!visible) { return; } + respawnInfoText.Text = text; + respawnInfoText.TextColor = textColor; + respawnButtonContainer.Visible = buttonsVisible; + respawnTickBox.Selected = !waitForNextRoundRespawn; } public void Draw(SpriteBatch spriteBatch) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs new file mode 100644 index 000000000..ec74f7590 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs @@ -0,0 +1,731 @@ +using Barotrauma.Extensions; +using Barotrauma.IO; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + static class HintManager + { + private const string HintManagerFile = "hintmanager.xml"; + private static HashSet HintIdentifiers { get; set; } + private static Dictionary> HintTags { get; } = new Dictionary>(); + private static Dictionary HintOrders { get; } = new Dictionary(); + /// + /// Hints that have already been shown this round and shouldn't be shown shown again until the next round + /// + private static HashSet HintsIgnoredThisRound { get; } = new HashSet(); + private static GUIMessageBox ActiveHintMessageBox { get; set; } + private static Action OnUpdate { get; set; } + private static double TimeStoppedInteracting { get; set; } + private static double TimeRoundStarted { get; set; } + /// + /// Seconds before any reminders can be shown + /// + private static int TimeBeforeReminders { get; set; } + /// + /// Seconds before another reminder can be shown + /// + private static int ReminderCooldown { get; set; } + private static double TimeReminderLastDisplayed { get; set; } + private static HashSet BallastHulls { get; } = new HashSet(); + + public static void Init() + { + if (File.Exists(HintManagerFile)) + { + var doc = XMLExtensions.TryLoadXml(HintManagerFile); + if (doc?.Root != null) + { + HintIdentifiers = new HashSet(); + foreach (var element in doc.Root.Elements()) + { + GetHintsRecursive(element, element.Name.ToString()); + } + } + else + { + DebugConsole.ThrowError($"File \"{HintManagerFile}\" is empty - cannot initialize the HintManager!"); + } + } + else + { + DebugConsole.ThrowError($"File \"{HintManagerFile}\" is missing - cannot initialize the HintManager!"); + } + + static void GetHintsRecursive(XElement element, string identifier) + { + if (!element.HasElements) + { + HintIdentifiers.Add(identifier); + if (element.GetAttributeStringArray("tags", null, convertToLowerInvariant: true) is string[] tags) + { + HintTags.TryAdd(identifier, tags.ToHashSet()); + } + if (element.GetAttributeString("order", null) is string orderIdentifier && !string.IsNullOrEmpty(orderIdentifier)) + { + string orderOption = element.GetAttributeString("orderoption", ""); + HintOrders.Add(identifier, (orderIdentifier, orderOption)); + } + return; + } + else if (element.Name.ToString().Equals("reminder")) + { + TimeBeforeReminders = element.GetAttributeInt("timebeforereminders", TimeBeforeReminders); + ReminderCooldown = element.GetAttributeInt("remindercooldown", ReminderCooldown); + } + foreach (var childElement in element.Elements()) + { + GetHintsRecursive(childElement, $"{identifier}.{childElement.Name}"); + } + } + } + + public static void Update() + { + if (HintIdentifiers == null || GameMain.Config.DisableInGameHints) { return; } + if (GameMain.GameSession == null || !GameMain.GameSession.IsRunning) { return; } + + if (ActiveHintMessageBox != null) + { + if (ActiveHintMessageBox.Closed) + { + ActiveHintMessageBox = null; + OnUpdate = null; + } + else + { + OnUpdate?.Invoke(); + return; + } + } + + CheckIsInteracting(); + CheckIfDivingGearOutOfOxygen(); + CheckHulls(); + CheckReminders(); + } + + public static void OnSetSelectedConstruction(Character character, Item oldConstruction, Item newConstruction) + { + if (oldConstruction == newConstruction) { return; } + + if (Character.Controlled != null && Character.Controlled == character && oldConstruction != null && oldConstruction.GetComponent() == null) + { + TimeStoppedInteracting = Timing.TotalTime; + } + + if (newConstruction == null) { return; } + if (newConstruction.GetComponent() != null) { return; } + if (newConstruction.GetComponent() is ConnectionPanel cp && cp.User == character) { return; } + OnStartedInteracting(character, newConstruction); + } + + private static void OnStartedInteracting(Character character, Item item) + { + if (!CanDisplayHints()) { return; } + if (character != Character.Controlled || item == null) { return; } + + string hintIdentifierBase = "onstartedinteracting"; + + // onstartedinteracting.brokenitem + if (item.Repairables.Any(r => item.ConditionPercentage < r.RepairThreshold)) + { + if (DisplayHint($"{hintIdentifierBase}.brokenitem")) { return; } + } + + // Don't display other item-related hints if the repair interface is displayed + if (item.Repairables.Any(r => r.ShouldDrawHUD(character))) { return; } + + // onstartedinteracting.lootingisstealing + if (item.Submarine?.Info?.Type == SubmarineType.Outpost && + item.ContainedItems.Any(i => !i.AllowStealing)) + { + if (DisplayHint($"{hintIdentifierBase}.lootingisstealing")) { return; } + } + + // onstartedinteracting.turretperiscope + if (item.HasTag("periscope") && + item.GetConnectedComponents().FirstOrDefault(t => t.Item.HasTag("turret")) is Turret) + { + if (DisplayHint($"{hintIdentifierBase}.turretperiscope", + variableTags: new string[] { "[shootkey]", "[deselectkey]", }, + variableValues: new string[] { GameMain.Config.KeyBindText(InputType.Shoot), GameMain.Config.KeyBindText(InputType.Deselect) })) + { return; } + } + + // onstartedinteracting.item... + hintIdentifierBase += ".item"; + foreach (string hintIdentifier in HintIdentifiers) + { + if (!hintIdentifier.StartsWith(hintIdentifierBase)) { continue; } + if (!HintTags.TryGetValue(hintIdentifier, out var hintTags)) { continue; } + if (!item.HasTag(hintTags)) { continue; } + if (DisplayHint(hintIdentifier)) { return; } + } + } + + private static void CheckIsInteracting() + { + if (!CanDisplayHints()) { return; } + if (Character.Controlled?.SelectedConstruction == null) { return; } + + if (Character.Controlled.SelectedConstruction.GetComponent() is Reactor reactor && reactor.PowerOn && + Character.Controlled.SelectedConstruction.OwnInventory?.AllItems is IEnumerable containedItems && + containedItems.Count(i => i.HasTag("reactorfuel")) > 1) + { + if (DisplayHint("onisinteracting.reactorwithextrarods")) { return; } + } + } + + public static void OnRoundStarted() + { + // Make sure everything's been reset properly, OnRoundEnded() isn't always called when exiting a game + Reset(); + TimeRoundStarted = GameMain.GameScreen.GameTime; + + var initRoundHandle = CoroutineManager.StartCoroutine(InitRound(), "HintManager.InitRound"); + if (!CanDisplayHints(requireGameScreen: false, requireControllingCharacter: false)) { return; } + CoroutineManager.StartCoroutine(DisplayRoundStartedHints(initRoundHandle), "HintManager.DisplayRoundStartedHints"); + + static IEnumerable InitRound() + { + while (Character.Controlled == null) { yield return CoroutineStatus.Running; } + // Get the ballast hulls on round start not to find them again and again later + BallastHulls.Clear(); + var sub = Submarine.MainSubs.FirstOrDefault(s => s != null && s.TeamID == Character.Controlled.TeamID); + if (sub != null) + { + foreach (var item in sub.GetItems(true)) + { + if (item.CurrentHull == null) { continue; } + if (item.GetComponent() == null) { continue; } + if (!item.HasTag("ballast")) { continue; } + BallastHulls.Add(item.CurrentHull); + } + } + yield return CoroutineStatus.Success; + } + + static IEnumerable DisplayRoundStartedHints(CoroutineHandle initRoundHandle) + { + while (GameMain.Instance.LoadingScreenOpen || Screen.Selected != GameMain.GameScreen || + CoroutineManager.IsCoroutineRunning(initRoundHandle) || + CoroutineManager.IsCoroutineRunning("LevelTransition") || + CoroutineManager.IsCoroutineRunning("SinglePlayerCampaign.DoInitialCameraTransition") || + CoroutineManager.IsCoroutineRunning("MultiPlayerCampaign.DoInitialCameraTransition") || + GUIMessageBox.VisibleBox != null || Character.Controlled == null) + { + yield return CoroutineStatus.Running; + } + + OnStartedControlling(); + + while (ActiveHintMessageBox != null) + { + yield return CoroutineStatus.Running; + } + + if (!GameMain.GameSession.GameMode.IsSinglePlayer && + GameMain.Config.VoiceSetting == GameSettings.VoiceMode.Disabled) + { + DisplayHint("onroundstarted.voipdisabled", onUpdate: () => + { + if (GameMain.Config.VoiceSetting == GameSettings.VoiceMode.Disabled) { return; } + ActiveHintMessageBox.Close(); + }); + } + + yield return CoroutineStatus.Success; + } + } + + public static void OnRoundEnded() + { + Reset(); + } + + private static void Reset() + { + CoroutineManager.StopCoroutines("HintManager.InitRound"); + CoroutineManager.StopCoroutines("HintManager.DisplayRoundStartedHints"); + if (ActiveHintMessageBox != null) + { + GUIMessageBox.MessageBoxes.Remove(ActiveHintMessageBox); + ActiveHintMessageBox = null; + } + OnUpdate = null; + HintsIgnoredThisRound.Clear(); + } + + public static void OnSonarSpottedCharacter(Item sonar, Character spottedCharacter) + { + if (!CanDisplayHints()) { return; } + if (sonar == null || sonar.Removed) { return; } + if (spottedCharacter == null || spottedCharacter.Removed || spottedCharacter.IsDead) { return; } + if (Character.Controlled.SelectedConstruction != sonar) { return; } + if (HumanAIController.IsFriendly(Character.Controlled, spottedCharacter)) { return; } + DisplayHint("onsonarspottedenemy"); + } + + public static void OnAfflictionDisplayed(Character character, List displayedAfflictions) + { + if (!CanDisplayHints()) { return; } + if (character != Character.Controlled || displayedAfflictions == null) { return; } + foreach (var affliction in displayedAfflictions) + { + if (affliction?.Prefab == null) { continue; } + if (affliction.Prefab.IsBuff) { continue; } + if (affliction.Prefab == AfflictionPrefab.OxygenLow) { continue; } + if (affliction.Prefab == AfflictionPrefab.RadiationSickness && (GameMain.GameSession.Map?.Radiation?.IsEntityRadiated(character) ?? false)) { continue; } + if (affliction.Strength < affliction.Prefab.ShowIconThreshold) { continue; } + DisplayHint("onafflictiondisplayed", + variableTags: new string[1] { "[key]" }, + variableValues: new string[1] { GameMain.Config.KeyBindText(InputType.Health) }, + icon: affliction.Prefab.Icon, + iconColor: CharacterHealth.GetAfflictionIconColor(affliction), + onUpdate: () => + { + if (CharacterHealth.OpenHealthWindow == null) { return; } + ActiveHintMessageBox.Close(); + }); + return; + } + } + + public static void OnShootWithoutAiming(Character character, Item item) + { + if (!CanDisplayHints()) { return; } + if (character != Character.Controlled) { return; } + if (character.SelectedConstruction != null || character.FocusedItem != null) { return; } + if (item == null || !item.IsShootable || !item.RequireAimToUse) { return; } + if (TimeStoppedInteracting + 1 > Timing.TotalTime) { return; } + if (GUI.MouseOn != null) { return; } + if (Character.Controlled.Inventory?.visualSlots != null && Character.Controlled.Inventory.visualSlots.Any(s => s.InteractRect.Contains(PlayerInput.MousePosition))) { return; } + string hintIdentifier = "onshootwithoutaiming"; + if (!HintTags.TryGetValue(hintIdentifier, out var tags)) { return; } + if (!item.HasTag(tags)) { return; } + DisplayHint(hintIdentifier, + variableTags: new string[1] { "[key]" }, + variableValues: new string[1] { GameMain.Config.KeyBindText(InputType.Aim) }, + onUpdate: () => + { + if (character.SelectedConstruction == null && GUI.MouseOn == null && PlayerInput.KeyDown(InputType.Aim)) + { + ActiveHintMessageBox.Close(); + } + }); + } + + public static void OnWeldingDoor(Character character, Door door) + { + if (!CanDisplayHints()) { return; } + if (character != Character.Controlled) { return; } + if (door == null || door.Stuck < 20.0f) { return; } + DisplayHint("onweldingdoor"); + } + + public static void OnTryOpenStuckDoor(Character character) + { + if (!CanDisplayHints()) { return; } + if (character != Character.Controlled) { return; } + DisplayHint("ontryopenstuckdoor"); + } + + public static void OnShowCampaignInterface(CampaignMode.InteractionType interactionType) + { + if (!CanDisplayHints()) { return; } + if (interactionType == CampaignMode.InteractionType.None) { return; } + string hintIdentifier = $"onshowcampaigninterface.{interactionType.ToString().ToLowerInvariant()}"; + DisplayHint(hintIdentifier, onUpdate: () => + { + + if (!(GameMain.GameSession?.Campaign is CampaignMode campaign) || + (!campaign.ShowCampaignUI && !campaign.ForceMapUI) || + campaign.CampaignUI?.SelectedTab != CampaignMode.InteractionType.Map) + { + ActiveHintMessageBox.Close(); + } + }); + } + + public static void OnShowCommandInterface() + { + IgnoreReminder("commandinterface"); + if (!CanDisplayHints()) { return; } + DisplayHint("onshowcommandinterface", onUpdate: () => + { + if (CrewManager.IsCommandInterfaceOpen) { return; } + ActiveHintMessageBox.Close(); + }); + } + + public static void OnShowHealthInterface() + { + if (!CanDisplayHints()) { return; } + if (CharacterHealth.OpenHealthWindow == null) { return; } + DisplayHint("onshowhealthinterface", onUpdate: () => + { + if (CharacterHealth.OpenHealthWindow != null) { return; } + ActiveHintMessageBox.Close(); + }); + } + + public static void OnShowTabMenu() + { + IgnoreReminder("tabmenu"); + } + + public static void OnStoleItem(Character character, Item item) + { + if (!CanDisplayHints()) { return; } + if (character != Character.Controlled) { return; } + if (item == null || item.AllowStealing || !item.StolenDuringRound) { return; } + DisplayHint("onstoleitem", onUpdate: () => + { + if (item == null || item.Removed || item.GetRootInventoryOwner() != character) + { + ActiveHintMessageBox.Close(); + } + }); + } + + public static void OnHandcuffed(Character character) + { + if (!CanDisplayHints()) { return; } + if (character != Character.Controlled || !character.LockHands) { return; } + DisplayHint("onhandcuffed", onUpdate: () => + { + if (character != null && !character.Removed && character.LockHands) { return; } + ActiveHintMessageBox.Close(); + }); + } + + public static void OnReactorOutOfFuel(Reactor reactor) + { + if (!CanDisplayHints()) { return; } + if (reactor == null) { return; } + if (reactor.Item.Submarine?.Info?.Type != SubmarineType.Player || reactor.Item.Submarine.TeamID != Character.Controlled.TeamID) { return; } + if (!HasValidJob("engineer")) { return; } + DisplayHint("onreactoroutoffuel", onUpdate: () => + { + if (reactor?.Item != null && !reactor.Item.Removed && reactor.AvailableFuel < 1) { return; } + ActiveHintMessageBox.Close(); + }); + } + + public static void OnAvailableTransition(CampaignMode.TransitionType transitionType) + { + if (!CanDisplayHints()) { return; } + if (transitionType == CampaignMode.TransitionType.None) { return; } + DisplayHint($"onavailabletransition.{transitionType.ToString().ToLowerInvariant()}"); + } + + public static void OnShowSubInventory(Item item) + { + if (item?.Prefab == null) { return; } + if (item.Prefab.Identifier.Equals("toolbelt", StringComparison.OrdinalIgnoreCase)) + { + IgnoreReminder("toolbelt"); + } + } + + public static void OnChangeCharacter() + { + IgnoreReminder("characterchange"); + } + + public static void OnCharacterUnconscious(Character character) + { + if (!CanDisplayHints()) { return; } + if (character != Character.Controlled) { return; } + if (character.IsDead) { return; } + if (character.CharacterHealth != null && character.Vitality < character.CharacterHealth.MinVitality) { return; } + DisplayHint("oncharacterunconscious"); + } + + public static void OnCharacterKilled(Character character) + { + if (!CanDisplayHints()) { return; } + if (character != Character.Controlled) { return; } + if (GameMain.IsMultiplayer) { return; } + if (GameMain.GameSession?.CrewManager == null) { return; } + if (GameMain.GameSession.CrewManager.GetCharacters().None(c => !c.IsDead)) { return; } + DisplayHint("oncharacterkilled"); + } + + private static void OnStartedControlling() + { + if (Level.IsLoadedOutpost) { return; } + if (Character.Controlled?.Info?.Job?.Prefab == null) { return; } + string hintIdentifier = $"onstartedcontrolling.job.{Character.Controlled.Info.Job.Prefab.Identifier}"; + DisplayHint(hintIdentifier, + icon: Character.Controlled.Info.Job.Prefab.Icon, + iconColor: Character.Controlled.Info.Job.Prefab.UIColor, + onDisplay: () => + { + if (!HintOrders.TryGetValue(hintIdentifier, out var orderInfo)) { return; } + var orderPrefab = Order.GetPrefab(orderInfo.identifier); + if (orderPrefab == null) { return; } + Item targetEntity = null; + ItemComponent targetItem = null; + if (orderPrefab.MustSetTarget) + { + targetEntity = orderPrefab.GetMatchingItems(true, interactableFor: Character.Controlled).FirstOrDefault(); + if (targetEntity == null) { return; } + targetItem = orderPrefab.GetTargetItemComponent(targetEntity); + } + var order = new Order(orderPrefab, targetEntity as Entity, targetItem, orderGiver: Character.Controlled); + GameMain.GameSession.CrewManager.SetCharacterOrder(Character.Controlled, order, orderInfo.option, CharacterInfo.HighestManualOrderPriority, Character.Controlled); + }); + } + + public static void OnAutoPilotPathUpdated(Steering steering) + { + if (!CanDisplayHints()) { return; } + if (!HasValidJob("captain")) { return; } + if (steering?.Item?.Submarine?.Info == null) { return; } + if (steering.Item.Submarine.Info.Type != SubmarineType.Player) { return; } + if (steering.Item.Submarine.TeamID != Character.Controlled.TeamID) { return; } + if (!steering.AutoPilot || steering.MaintainPos) { return; } + if (steering.SteeringPath?.CurrentNode?.Tunnel?.Type != Level.TunnelType.MainPath) { return; } + if (!steering.SteeringPath.Finished && steering.SteeringPath.NextNode != null) { return; } + if (steering.LevelStartSelected && (Level.Loaded.StartOutpost == null || !steering.Item.Submarine.AtStartExit)) { return; } + if (steering.LevelEndSelected && (Level.Loaded.EndOutpost == null || !steering.Item.Submarine.AtEndExit)) { return; } + DisplayHint("onautopilotreachedoutpost"); + } + + public static void OnStatusEffectApplied(ItemComponent component, ActionType actionType, Character character) + { + if (!CanDisplayHints()) { return; } + if (character != Character.Controlled) { return; } + // Could make this more generic if there will ever be any other status effect related hints + if (!(component is Repairable) || actionType != ActionType.OnFailure) { return; } + DisplayHint("onrepairfailed"); + } + + public static void OnActiveOrderAdded(Order order) + { + if (!CanDisplayHints()) { return; } + if (order == null) { return; } + + if (order.Identifier == "reportballastflora" && + order.TargetEntity is Hull h && + h.Submarine?.TeamID == Character.Controlled.TeamID) + { + DisplayHint("onballastflorainfected"); + } + } + + private static void CheckIfDivingGearOutOfOxygen() + { + if (!CanDisplayHints()) { return; } + var divingGear = Character.Controlled.GetEquippedItem("diving"); + if (divingGear?.OwnInventory == null) { return; } + if (divingGear.GetContainedItemConditionPercentage() > 0.0f) { return; } + DisplayHint("ondivinggearoutofoxygen", onUpdate: () => + { + if (divingGear == null || divingGear.Removed || + Character.Controlled == null || !Character.Controlled.HasEquippedItem(divingGear) || + divingGear.GetContainedItemConditionPercentage() > 0.0f) + { + ActiveHintMessageBox.Close(); + } + }); + } + + private static void CheckHulls() + { + if (!CanDisplayHints()) { return; } + if (Character.Controlled.CurrentHull == null) { return; } + if (HumanAIController.IsBallastFloraNoticeable(Character.Controlled, Character.Controlled.CurrentHull)) + { + if (DisplayHint("onballastflorainfected")) { return; } + } + foreach (var gap in Character.Controlled.CurrentHull.ConnectedGaps) + { + if (gap.ConnectedDoor == null || gap.ConnectedDoor.Impassable) { continue; } + if (Vector2.DistanceSquared(Character.Controlled.WorldPosition, gap.ConnectedDoor.Item.WorldPosition) > 400 * 400) { continue; } + if (!gap.IsRoomToRoom) + { + if (!(Character.Controlled.GetEquippedItem("deepdiving") is Item)) { continue; } + if (Character.Controlled.IsProtectedFromPressure()) { continue; } + if (DisplayHint("divingsuitwarning", extendTextTag: false)) { return; } + continue; + } + foreach (var me in gap.linkedTo) + { + if (me == Character.Controlled.CurrentHull) { continue; } + if (!(me is Hull adjacentHull)) { continue; } + if (adjacentHull.LethalPressure > 5.0f && DisplayHint("onadjacenthull.highpressure")) { return; } + if (adjacentHull.WaterPercentage > 75 && !BallastHulls.Contains(adjacentHull) && DisplayHint("onadjacenthull.highwaterpercentage")) { return; } + } + } + } + + private static void CheckReminders() + { + if (!CanDisplayHints()) { return; } + if (Level.Loaded == null) { return; } + if (GameMain.GameScreen.GameTime < TimeRoundStarted + TimeBeforeReminders) { return; } + if (GameMain.GameScreen.GameTime < TimeReminderLastDisplayed + ReminderCooldown) { return; } + + string hintIdentifierBase = "reminder"; + + if (GameMain.GameSession.GameMode.IsSinglePlayer) + { + if (DisplayHint($"{hintIdentifierBase}.characterchange")) + { + TimeReminderLastDisplayed = GameMain.GameScreen.GameTime; + return; + } + } + + if (Level.Loaded.Type != LevelData.LevelType.Outpost) + { + if (DisplayHint($"{hintIdentifierBase}.commandinterface", + variableTags: new string[] { "[commandkey]" }, + variableValues: new string[] { GameMain.Config.KeyBindText(InputType.Command) }, + onUpdate: () => + { + if (!CrewManager.IsCommandInterfaceOpen) { return; } + ActiveHintMessageBox.Close(); + })) + { + TimeReminderLastDisplayed = GameMain.GameScreen.GameTime; + return; + } + } + + if (DisplayHint($"{hintIdentifierBase}.tabmenu", + variableTags: new string[] { "[infotabkey]" }, + variableValues: new string[] { GameMain.Config.KeyBindText(InputType.InfoTab) }, + onUpdate: () => + { + if (!GameSession.IsTabMenuOpen) { return; } + ActiveHintMessageBox.Close(); + })) + { + TimeReminderLastDisplayed = GameMain.GameScreen.GameTime; + return; + } + + if (Character.Controlled.Inventory?.GetItemInLimbSlot(InvSlotType.Bag)?.Prefab?.Identifier == "toolbelt") + { + if (DisplayHint($"{hintIdentifierBase}.toolbelt")) + { + TimeReminderLastDisplayed = GameMain.GameScreen.GameTime; + return; + } + } + } + + private static bool DisplayHint(string hintIdentifier, bool extendTextTag = true, string[] variableTags = null, string[] variableValues = null, Sprite icon = null, Color? iconColor = null, Action onDisplay = null, Action onUpdate = null) + { + if (string.IsNullOrEmpty(hintIdentifier)) { return false; } + if (!HintIdentifiers.Contains(hintIdentifier)) { return false; } + if (GameMain.Config.IgnoredHints.Contains(hintIdentifier)) { return false; } + if (HintsIgnoredThisRound.Contains(hintIdentifier)) { return false; } + + string text; + string textTag = extendTextTag ? $"hint.{hintIdentifier}" : hintIdentifier; + if (variableTags != null && variableTags != null && variableTags.Length > 0 && variableTags.Length == variableValues.Length) + { + text = TextManager.GetWithVariables(textTag, variableTags, variableValues, returnNull: true); + } + else + { + text = TextManager.Get(textTag, returnNull: true); + } + + if (string.IsNullOrEmpty(text)) + { +#if DEBUG + DebugConsole.ThrowError($"No hint text found for text tag \"{textTag}\""); +#endif + return false; + } + + HintsIgnoredThisRound.Add(hintIdentifier); + + ActiveHintMessageBox = new GUIMessageBox(hintIdentifier, text, icon); + if (iconColor.HasValue) { ActiveHintMessageBox.IconColor = iconColor.Value; } + OnUpdate = onUpdate; + + SoundPlayer.PlayUISound(GUISoundType.UIMessage); + ActiveHintMessageBox.InnerFrame.Flash(color: iconColor ?? Color.Orange, flashDuration: 0.75f); + onDisplay?.Invoke(); + + return true; + } + + public static bool OnDontShowAgain(GUITickBox tickBox) + { + IgnoreHint((string)tickBox.UserData, ignore: tickBox.Selected); + return true; + } + + private static void IgnoreHint(string hintIdentifier, bool ignore = true) + { + if (string.IsNullOrEmpty(hintIdentifier)) { return; } + if (!HintIdentifiers.Contains(hintIdentifier)) + { +#if DEBUG + DebugConsole.ThrowError($"Tried to ignore a hint not defined in {HintManagerFile}: {hintIdentifier}"); +#endif + return; + } + if (ignore) + { + GameMain.Config.IgnoredHints.Add(hintIdentifier); + } + else + { + GameMain.Config.IgnoredHints.Remove(hintIdentifier); + } + } + + private static void IgnoreReminder(string reminderIdentifier) + { + HintsIgnoredThisRound.Add($"reminder.{reminderIdentifier}"); + } + + public static bool OnDisableHints(GUITickBox tickBox) + { + GameMain.Config.DisableInGameHints = tickBox.Selected; + return GameMain.Config.SaveNewPlayerConfig(); + } + + private static bool CanDisplayHints(bool requireGameScreen = true, bool requireControllingCharacter = true) + { + if (HintIdentifiers == null) { return false; } + if (GameMain.Config.DisableInGameHints) { return false; } + if (ActiveHintMessageBox != null) { return false; } + if (requireControllingCharacter && Character.Controlled == null) { return false; } + var gameMode = GameMain.GameSession?.GameMode; + if (!(gameMode is CampaignMode || gameMode is MissionMode)) { return false; } + if (requireGameScreen && Screen.Selected != GameMain.GameScreen) { return false; } + return true; + } + + private static bool HasValidJob(string jobIdentifier) + { + // In singleplayer, we can control all character so we don't care about job restrictions + if (GameMain.GameSession.GameMode.IsSinglePlayer) { return true; } + if (Character.Controlled.HasJob(jobIdentifier)) { return true; } + // In multiplayer, if there are players with the job, display the hint to all players + foreach (var c in GameMain.GameSession.CrewManager.GetCharacters()) + { + if (c == null || !c.IsRemotePlayer) { continue; } + if (c.IsUnconscious || c.IsDead || c.Removed) { continue; } + if (!c.HasJob(jobIdentifier)) { continue; } + return false; + } + return true; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs index 71ffe721d..bb01da050 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs @@ -48,6 +48,7 @@ namespace Barotrauma msgBox.Buttons[0].OnClicked = delegate { msgBox.Close(); + if (GameMain.Client == null) { return true; } SendState(ReadyStatus.Yes); CreateResultsMessage(); return true; @@ -57,6 +58,7 @@ namespace Barotrauma msgBox.Buttons[1].OnClicked = delegate { msgBox.Close(); + if (GameMain.Client == null) { return true; } SendState(ReadyStatus.No); CreateResultsMessage(); return true; @@ -65,6 +67,8 @@ namespace Barotrauma private void CreateResultsMessage() { + if (GameMain.Client == null) { return; } + Vector2 relativeSize = new Vector2(0.2f, 0.3f); Point minSize = new Point(300, 400); resultsBox = new GUIMessageBox(readyCheckHeader, string.Empty, new[] { closeButton }, relativeSize, minSize, type: GUIMessageBox.Type.Vote) { UserData = ResultData, Draggable = true }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index 5b0dd6f58..e0211d8af 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -16,7 +16,7 @@ namespace Barotrauma private int jobColumnWidth, characterColumnWidth, statusColumnWidth; private readonly SubmarineInfo sub; - private readonly Mission selectedMission; + private readonly List selectedMissions; private readonly Location startLocation, endLocation; private readonly GameMode gameMode; @@ -32,11 +32,11 @@ namespace Barotrauma - public RoundSummary(SubmarineInfo sub, GameMode gameMode, Mission selectedMission, Location startLocation, Location endLocation) + public RoundSummary(SubmarineInfo sub, GameMode gameMode, IEnumerable selectedMissions, Location startLocation, Location endLocation) { this.sub = sub; this.gameMode = gameMode; - this.selectedMission = selectedMission; + this.selectedMissions = selectedMissions.ToList(); this.startLocation = startLocation; this.endLocation = endLocation; initialLocationReputation = startLocation?.Reputation?.Value ?? 0.0f; @@ -75,7 +75,7 @@ namespace Barotrauma //crew panel ------------------------------------------------------------------------------- - GUIFrame crewFrame = new GUIFrame(new RectTransform(new Vector2(0.35f, 0.55f), background.RectTransform, Anchor.TopCenter, minSize: new Point(minWidth, minHeight))); + GUIFrame crewFrame = new GUIFrame(new RectTransform(new Vector2(0.35f, 0.45f), background.RectTransform, Anchor.TopCenter, minSize: new Point(minWidth, minHeight))); GUIFrame crewFrameInner = new GUIFrame(new RectTransform(new Point(crewFrame.Rect.Width - padding * 2, crewFrame.Rect.Height - padding * 2), crewFrame.RectTransform, Anchor.Center), style: "InnerFrame"); var crewContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), crewFrameInner.RectTransform, Anchor.Center)) @@ -90,10 +90,10 @@ namespace Barotrauma CreateCrewList(crewContent, gameSession.CrewManager.GetCharacterInfos().Where(c => c.TeamID != CharacterTeamType.Team2)); //another crew frame for the 2nd team in combat missions - if (gameSession.Mission is CombatMission) + if (gameSession.Missions.Any(m => m is CombatMission)) { crewHeader.Text = CombatMission.GetTeamName(CharacterTeamType.Team1); - GUIFrame crewFrame2 = new GUIFrame(new RectTransform(new Vector2(0.35f, 0.55f), background.RectTransform, Anchor.TopCenter, minSize: new Point(minWidth, minHeight))); + GUIFrame crewFrame2 = new GUIFrame(new RectTransform(new Vector2(0.35f, 0.45f), background.RectTransform, Anchor.TopCenter, minSize: new Point(minWidth, minHeight))); rightPanels.Add(crewFrame2); GUIFrame crewFrameInner2 = new GUIFrame(new RectTransform(new Point(crewFrame2.Rect.Width - padding * 2, crewFrame2.Rect.Height - padding * 2), crewFrame2.RectTransform, Anchor.Center), style: "InnerFrame"); var crewContent2 = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), crewFrameInner2.RectTransform, Anchor.Center)) @@ -183,7 +183,8 @@ namespace Barotrauma //reputation panel ------------------------------------------------------------------------------- - if (gameMode is CampaignMode campaignMode) + var campaignMode = gameMode as CampaignMode; + if (campaignMode != null) { GUIFrame reputationframe = new GUIFrame(new RectTransform(crewFrame.RectTransform.RelativeSize, background.RectTransform, Anchor.TopCenter, minSize: crewFrame.RectTransform.MinSize)); rightPanels.Add(reputationframe); @@ -198,158 +199,154 @@ namespace Barotrauma TextManager.Get("reputation"), textAlignment: Alignment.TopLeft, font: GUI.SubHeadingFont); reputationHeader.RectTransform.MinSize = new Point(0, GUI.IntScale(reputationHeader.Rect.Height * 2.0f)); - GUIListBox reputationList = new GUIListBox(new RectTransform(Vector2.One, reputationContent.RectTransform)) - { - Padding = new Vector4(2, 5, 0, 0) - }; - reputationList.ContentBackground.Color = Color.Transparent; - - if (startLocation.Type.HasOutpost && startLocation.Reputation != null) - { - var iconStyle = GUI.Style.GetComponentStyle("LocationReputationIcon"); - CreateReputationElement( - reputationList.Content, - startLocation.Name, - startLocation.Reputation.Value, startLocation.Reputation.NormalizedValue, initialLocationReputation, - startLocation.Type.Name, "", - iconStyle?.GetDefaultSprite(), startLocation.Type.GetPortrait(0), iconStyle?.Color ?? Color.White); - } - - foreach (Faction faction in campaignMode.Factions) - { - float initialReputation = faction.Reputation.Value; - if (initialFactionReputations.ContainsKey(faction)) - { - initialReputation = initialFactionReputations[faction]; - } - else - { - DebugConsole.AddWarning($"Could not determine reputation change for faction \"{faction.Prefab.Name}\" (faction was not present at the start of the round)."); - } - CreateReputationElement( - reputationList.Content, - faction.Prefab.Name, - faction.Reputation.Value, faction.Reputation.NormalizedValue, initialReputation, - faction.Prefab.ShortDescription, faction.Prefab.Description, - faction.Prefab.Icon, faction.Prefab.BackgroundPortrait, faction.Prefab.IconColor); - } - - float otherElementHeight = 0.0f; - float maxDescriptionHeight = 0.0f; - foreach (GUIComponent child in reputationList.Content.Children) - { - var descriptionElement = child.FindChild("description", recursive: true) as GUITextBlock; - maxDescriptionHeight = Math.Max(maxDescriptionHeight, descriptionElement.TextSize.Y * 1.1f); - otherElementHeight = Math.Max(otherElementHeight, descriptionElement.Parent.Rect.Height - descriptionElement.TextSize.Y); - } - foreach (GUIComponent child in reputationList.Content.Children) - { - var descriptionElement = child.FindChild("description", recursive: true) as GUITextBlock; - descriptionElement.RectTransform.MaxSize = new Point(int.MaxValue, (int)(maxDescriptionHeight)); - child.RectTransform.MaxSize = new Point(int.MaxValue, (int)((maxDescriptionHeight + otherElementHeight) * 1.2f)); - (descriptionElement?.Parent as GUILayoutGroup).Recalculate(); - } + CreateReputationInfoPanel(reputationContent, campaignMode); } //mission panel ------------------------------------------------------------------------------- - GUIFrame missionframe = new GUIFrame(new RectTransform(new Vector2(0.39f, 0.22f), background.RectTransform, Anchor.TopCenter, minSize: new Point(minWidth, minHeight / 4))); - GUIFrame missionframeInner = new GUIFrame(new RectTransform(new Point(missionframe.Rect.Width - padding * 2, missionframe.Rect.Height - padding * 2), missionframe.RectTransform, Anchor.Center), style: "InnerFrame"); - - var missionContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), missionframeInner.RectTransform, Anchor.Center)) + GUIFrame missionframe = new GUIFrame(new RectTransform(new Vector2(0.39f, 0.3f), background.RectTransform, Anchor.TopCenter, minSize: new Point(minWidth, minHeight / 4))); + GUILayoutGroup missionFrameContent = new GUILayoutGroup(new RectTransform(new Point(missionframe.Rect.Width - padding * 2, missionframe.Rect.Height - padding * 2), missionframe.RectTransform, Anchor.Center)) + { + Stretch = true, + RelativeSpacing = 0.05f + }; + GUIFrame missionframeInner = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.9f), missionFrameContent.RectTransform, Anchor.Center), style: "InnerFrame"); + + var missionContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.93f), missionframeInner.RectTransform, Anchor.Center)) { - RelativeSpacing = 0.05f, Stretch = true }; + List missionsToDisplay = new List(selectedMissions); + if (!selectedMissions.Any() && startLocation?.SelectedMission != null) + { + if (startLocation.SelectedMission.Locations[0] == startLocation.SelectedMission.Locations[1] || + startLocation.SelectedMission.Locations.Contains(campaignMode?.Map.SelectedLocation)) + { + missionsToDisplay.Add(startLocation.SelectedMission); + } + } + + if (missionsToDisplay.Any()) + { + var missionHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionContent.RectTransform), + TextManager.Get(missionsToDisplay.Count > 1 ? "Missions" : "Mission"), textAlignment: Alignment.TopLeft, font: GUI.SubHeadingFont); + missionHeader.RectTransform.MinSize = new Point(0, (int)(missionHeader.Rect.Height * 1.2f)); + } + + GUIListBox missionList = new GUIListBox(new RectTransform(Vector2.One, missionContent.RectTransform, Anchor.Center)) + { + Padding = new Vector4(4, 10, 0, 0) * GUI.Scale + }; + missionList.ContentBackground.Color = Color.Transparent; + + ButtonArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), missionFrameContent.RectTransform, Anchor.BottomCenter), isHorizontal: true, childAnchor: Anchor.BottomRight) + { + RelativeSpacing = 0.025f + }; + + missionFrameContent.Recalculate(); + missionContent.Recalculate(); + if (!string.IsNullOrWhiteSpace(endMessage)) { - var endText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionContent.RectTransform), - TextManager.GetServerMessage(endMessage), wrap: true); + var endText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionList.Content.RectTransform), + TextManager.GetServerMessage(endMessage), wrap: true) + { + CanBeFocused = false + }; endText.RectTransform.MinSize = new Point(0, endText.Rect.Height); - var line = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.1f), missionContent.RectTransform), style: "HorizontalLine"); + var line = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.1f), missionList.Content.RectTransform), style: "HorizontalLine"); line.RectTransform.NonScaledSize = new Point(line.Rect.Width, GUI.IntScale(5.0f)); } - var missionContentHorizontal = new GUILayoutGroup(new RectTransform(Vector2.One, missionContent.RectTransform), childAnchor: Anchor.TopLeft, isHorizontal: true) + foreach (Mission displayedMission in missionsToDisplay) { - RelativeSpacing = 0.025f, - Stretch = true - }; - - Mission displayedMission = selectedMission ?? startLocation.SelectedMission; - string missionMessage = ""; - GUIImage missionIcon; - if (displayedMission != null) - { - missionMessage = - displayedMission == selectedMission ? - displayedMission.Completed ? displayedMission.SuccessMessage : displayedMission.FailureMessage : - displayedMission.Description; - missionIcon = new GUIImage(new RectTransform(new Point(missionContentHorizontal.Rect.Height), missionContentHorizontal.RectTransform), displayedMission.Prefab.Icon, scaleToFit: true) + var missionContentHorizontal = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.8f), missionList.Content.RectTransform), childAnchor: Anchor.CenterLeft, isHorizontal: true) { - Color = displayedMission.Prefab.IconColor + RelativeSpacing = 0.025f, + Stretch = true }; - if (displayedMission == selectedMission) - { - new GUIImage(new RectTransform(Vector2.One, missionIcon.RectTransform), displayedMission.Completed ? "MissionCompletedIcon" : "MissionFailedIcon", scaleToFit: true); - } - } - else - { - missionIcon = new GUIImage(new RectTransform(new Point(missionContentHorizontal.Rect.Height), missionContentHorizontal.RectTransform), style: "NoMissionIcon", scaleToFit: true); - } - var missionTextContent = new GUILayoutGroup(new RectTransform(Vector2.One, missionContentHorizontal.RectTransform)) - { - RelativeSpacing = 0.05f - }; - missionContentHorizontal.Recalculate(); - missionContent.Recalculate(); - missionIcon.RectTransform.MinSize = new Point(0, missionContentHorizontal.Rect.Height); - missionTextContent.RectTransform.MaxSize = new Point(int.MaxValue, missionIcon.Rect.Width); - GUITextBlock missionDescription = null; - if (displayedMission == null) - { + string missionMessage = + selectedMissions.Contains(displayedMission) ? + displayedMission.Completed ? displayedMission.SuccessMessage : displayedMission.FailureMessage : + displayedMission.Description; + GUIImage missionIcon = new GUIImage(new RectTransform(new Point((int)(missionContentHorizontal.Rect.Height)), missionContentHorizontal.RectTransform), displayedMission.Prefab.Icon, scaleToFit: true) + { + Color = displayedMission.Prefab.IconColor, + HoverColor = displayedMission.Prefab.IconColor, + SelectedColor = displayedMission.Prefab.IconColor + }; + missionIcon.RectTransform.MinSize = new Point((int)(missionContentHorizontal.Rect.Height * 0.9f)); + if (selectedMissions.Contains(displayedMission)) + { + new GUIImage(new RectTransform(Vector2.One, missionIcon.RectTransform), displayedMission.Completed ? "MissionCompletedIcon" : "MissionFailedIcon", scaleToFit: true); + } + + var missionTextContent = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.8f), missionContentHorizontal.RectTransform)) + { + RelativeSpacing = 0.05f + }; + var missionNameTextBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), + displayedMission.Name, font: GUI.SubHeadingFont); + if (displayedMission.Difficulty.HasValue) + { + var groupSize = missionNameTextBlock.Rect.Size; + groupSize.X -= (int)(missionNameTextBlock.Padding.X + missionNameTextBlock.Padding.Z); + var indicatorGroup = new GUILayoutGroup(new RectTransform(groupSize, missionTextContent.RectTransform) { AbsoluteOffset = new Point((int)missionNameTextBlock.Padding.X, 0) }, + isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + AbsoluteSpacing = 1 + }; + var difficultyColor = displayedMission.GetDifficultyColor(); + for (int i = 0; i < displayedMission.Difficulty; i++) + { + new GUIImage(new RectTransform(Vector2.One, indicatorGroup.RectTransform, scaleBasis: ScaleBasis.Smallest) { IsFixedSize = true }, "DifficultyIndicator", scaleToFit: true) + { + CanBeFocused = false, + Color = difficultyColor + }; + } + } new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), - TextManager.Get("nomission"), font: GUI.LargeFont); - } - else - { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), - TextManager.AddPunctuation(':', TextManager.Get("Mission"), displayedMission.Name), font: GUI.SubHeadingFont); - missionDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), - missionMessage, wrap: true); - if (displayedMission == selectedMission && displayedMission.Completed) + missionMessage, wrap: true, parseRichText: true); + if (selectedMissions.Contains(displayedMission) && displayedMission.Completed && displayedMission.Reward > 0) { string rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", displayedMission.Reward)); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), - TextManager.GetWithVariable("MissionReward", "[reward]", rewardText)); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), displayedMission.GetMissionRewardText(), parseRichText: true); + } + + if (displayedMission != missionsToDisplay.Last()) + { + var spacing = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.0f), missionList.Content.RectTransform) { MaxSize = new Point(int.MaxValue, GUI.IntScale(15)) }, style: null); + new GUIFrame(new RectTransform(new Vector2(0.8f, 1.0f), spacing.RectTransform, Anchor.Center) { RelativeOffset = new Vector2(0.1f, 0.0f) }, "HorizontalLine"); } } - ButtonArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), missionContent.RectTransform, Anchor.BottomCenter), isHorizontal: true, childAnchor: Anchor.BottomRight) + if (!missionsToDisplay.Any()) { - IgnoreLayoutGroups = true, - RelativeSpacing = 0.025f - }; + var missionContentHorizontal = new GUILayoutGroup(new RectTransform(Vector2.One, missionList.Content.RectTransform), childAnchor: Anchor.TopLeft, isHorizontal: true) + { + RelativeSpacing = 0.025f, + Stretch = true + }; + GUIImage missionIcon = new GUIImage(new RectTransform(new Point((int)(missionContentHorizontal.Rect.Height * 0.7f)), missionContentHorizontal.RectTransform), style: "NoMissionIcon", scaleToFit: true); + missionIcon.RectTransform.MinSize = new Point((int)(missionContentHorizontal.Rect.Height * 0.7f)); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionContentHorizontal.RectTransform), + TextManager.Get("nomission"), font: GUI.LargeFont); + } + + /*missionContentHorizontal.Recalculate(); + missionContent.Recalculate(); + missionIcon.RectTransform.MinSize = new Point(0, missionContentHorizontal.Rect.Height); + missionTextContent.RectTransform.MaxSize = new Point(int.MaxValue, missionIcon.Rect.Width);*/ ContinueButton = new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), ButtonArea.RectTransform), TextManager.Get("Close")); ButtonArea.RectTransform.NonScaledSize = new Point(ButtonArea.Rect.Width, ContinueButton.Rect.Height); ButtonArea.RectTransform.IsFixedSize = true; - missionContent.Recalculate(); - //description overlapping with the buttons -> switch to small font - if (missionDescription != null && missionDescription.Rect.Y + missionDescription.TextSize.Y > ButtonArea.Rect.Y) - { - missionDescription.Font = GUI.Style.SmallFont; - //still overlapping -> shorten the text - if (missionDescription.Rect.Y + missionDescription.TextSize.Y > ButtonArea.Rect.Y && missionDescription.WrappedText.Contains('\n')) - { - missionDescription.ToolTip = missionDescription.Text; - missionDescription.Text = missionDescription.WrappedText.Split('\n').First() + "..."; - } - } + missionFrameContent.Recalculate(); // set layout ------------------------------------------------------------------- @@ -378,9 +375,114 @@ namespace Barotrauma return background; } + public void CreateReputationInfoPanel(GUIComponent parent, CampaignMode campaignMode) + { + GUIListBox reputationList = new GUIListBox(new RectTransform(Vector2.One, parent.RectTransform)) + { + Padding = new Vector4(4, 10, 0, 0) * GUI.Scale + }; + reputationList.ContentBackground.Color = Color.Transparent; + + if (startLocation.Type.HasOutpost && startLocation.Reputation != null) + { + var iconStyle = GUI.Style.GetComponentStyle("LocationReputationIcon"); + var locationFrame = CreateReputationElement( + reputationList.Content, + startLocation.Name, + startLocation.Reputation.Value, startLocation.Reputation.NormalizedValue, initialLocationReputation, + startLocation.Type.Name, "", + iconStyle?.GetDefaultSprite(), startLocation.Type.GetPortrait(0), iconStyle?.Color ?? Color.White); + CreatePathUnlockElement(locationFrame, null, startLocation); + } + + foreach (Faction faction in campaignMode.Factions) + { + float initialReputation = faction.Reputation.Value; + if (initialFactionReputations.ContainsKey(faction)) + { + initialReputation = initialFactionReputations[faction]; + } + else + { + DebugConsole.AddWarning($"Could not determine reputation change for faction \"{faction.Prefab.Name}\" (faction was not present at the start of the round)."); + } + var factionFrame = CreateReputationElement( + reputationList.Content, + faction.Prefab.Name, + faction.Reputation.Value, faction.Reputation.NormalizedValue, initialReputation, + faction.Prefab.ShortDescription, faction.Prefab.Description, + faction.Prefab.Icon, faction.Prefab.BackgroundPortrait, faction.Prefab.IconColor); + CreatePathUnlockElement(factionFrame, faction, null); + } + + float maxDescriptionHeight = 0.0f; + foreach (GUIComponent child in reputationList.Content.Children) + { + var descriptionElement = child.FindChild("description", recursive: true) as GUITextBlock; + maxDescriptionHeight = Math.Max(maxDescriptionHeight, descriptionElement.TextSize.Y * 1.1f); + } + foreach (GUIComponent child in reputationList.Content.Children) + { + var headerElement = child.FindChild("header", recursive: true) as GUITextBlock; + var descriptionElement = child.FindChild("description", recursive: true) as GUITextBlock; + descriptionElement.RectTransform.NonScaledSize = new Point(descriptionElement.Rect.Width, (int)maxDescriptionHeight); + descriptionElement.RectTransform.IsFixedSize = true; + child.RectTransform.NonScaledSize = new Point(child.Rect.Width, headerElement.Rect.Height + descriptionElement.RectTransform.Parent.Children.Sum(c => c.Rect.Height + ((GUILayoutGroup)descriptionElement.Parent).AbsoluteSpacing)); + } + + void CreatePathUnlockElement(GUIComponent reputationFrame, Faction faction, Location location) + { + if (GameMain.GameSession?.Campaign?.Map != null) + { + foreach (LocationConnection connection in GameMain.GameSession.Campaign.Map.Connections) + { + if (!connection.Locked || (!connection.Locations[0].Discovered && !connection.Locations[1].Discovered)) { continue; } + + var gateLocation = connection.Locations[0].IsGateBetweenBiomes ? connection.Locations[0] : connection.Locations[1]; + var unlockEvent = + EventSet.PrefabList.Find(ep => ep.UnlockPathEvent && ep.BiomeIdentifier == gateLocation.LevelData.Biome.Identifier) ?? + EventSet.PrefabList.Find(ep => ep.UnlockPathEvent && string.IsNullOrEmpty(ep.BiomeIdentifier)); + + if (unlockEvent == null) { continue; } + if (string.IsNullOrEmpty(unlockEvent.UnlockPathFaction) || unlockEvent.UnlockPathFaction.Equals("location", StringComparison.OrdinalIgnoreCase)) + { + if (location == null || gateLocation != location) { continue; } + } + else + { + if (faction == null || !faction.Prefab.Identifier.Equals(unlockEvent.UnlockPathFaction, StringComparison.OrdinalIgnoreCase)) { continue; } + } + + if (unlockEvent != null) + { + Reputation unlockReputation = gateLocation.Reputation; + Faction unlockFaction = null; + if (!string.IsNullOrEmpty(unlockEvent.UnlockPathFaction)) + { + unlockFaction = GameMain.GameSession.Campaign.Factions.Find(f => f.Prefab.Identifier.Equals(unlockEvent.UnlockPathFaction, StringComparison.OrdinalIgnoreCase)); + unlockReputation = unlockFaction?.Reputation; + } + float normalizedUnlockReputation = MathUtils.InverseLerp(unlockReputation.MinReputation, unlockReputation.MaxReputation, unlockEvent.UnlockPathReputation); + string unlockText = TextManager.GetWithVariables( + "lockedpathreputationrequirement", + new string[] { "[reputation]", "[biomename]" }, + new string[] { Reputation.GetFormattedReputationText(normalizedUnlockReputation, unlockEvent.UnlockPathReputation, addColorTags: true), $"‖color:gui.orange‖{connection.LevelData.Biome.DisplayName}‖end‖" }); + var unlockInfoPanel = new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.0f), reputationFrame.RectTransform, Anchor.BottomCenter) { MinSize = new Point(0, GUI.IntScale(30)), AbsoluteOffset = new Point(0, GUI.IntScale(3)) }, + unlockText, style: "GUIButtonRound", textAlignment: Alignment.Center, textColor: GUI.Style.TextColor, parseRichText: true); + unlockInfoPanel.Color = Color.Lerp(unlockInfoPanel.Color, Color.Black, 0.8f); + if (unlockInfoPanel.TextSize.X > unlockInfoPanel.Rect.Width * 0.7f) + { + unlockInfoPanel.Font = GUI.SmallFont; + } + } + } + } + } + } + private string GetHeaderText(bool gameOver, CampaignMode.TransitionType transitionType) { - string locationName = Submarine.MainSub.AtEndPosition ? endLocation?.Name : startLocation?.Name; + string locationName = Submarine.MainSub.AtEndExit ? endLocation?.Name : startLocation?.Name; string textTag; if (gameOver) @@ -396,17 +498,23 @@ namespace Barotrauma textTag = "RoundSummaryLeaving"; break; case CampaignMode.TransitionType.ProgressToNextLocation: - case CampaignMode.TransitionType.ProgressToNextEmptyLocation: locationName = endLocation?.Name; textTag = "RoundSummaryProgress"; break; + case CampaignMode.TransitionType.ProgressToNextEmptyLocation: + locationName = endLocation?.Name; + textTag = "RoundSummaryProgressToEmptyLocation"; + break; case CampaignMode.TransitionType.ReturnToPreviousLocation: - case CampaignMode.TransitionType.ReturnToPreviousEmptyLocation: locationName = startLocation?.Name; textTag = "RoundSummaryReturn"; break; + case CampaignMode.TransitionType.ReturnToPreviousEmptyLocation: + locationName = startLocation?.Name; + textTag = "RoundSummaryReturnToEmptyLocation"; + break; default: - textTag = Submarine.MainSub.AtEndPosition ? "RoundSummaryProgress" : "RoundSummaryReturn"; + textTag = Submarine.MainSub.AtEndExit ? "RoundSummaryProgress" : "RoundSummaryReturn"; break; } } @@ -456,7 +564,7 @@ namespace Barotrauma GUIListBox crewList = new GUIListBox(new RectTransform(Vector2.One, parent.RectTransform)) { - Padding = new Vector4(2, 5, 0, 0), + Padding = new Vector4(4, 10, 0, 0) * GUI.Scale, AutoHideScrollBar = false }; crewList.ContentBackground.Color = Color.Transparent; @@ -547,11 +655,11 @@ namespace Barotrauma ToolBox.LimitString(statusText, GUI.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: statusColor); } - private void CreateReputationElement(GUIComponent parent, + private GUIFrame CreateReputationElement(GUIComponent parent, string name, float reputation, float normalizedReputation, float initialReputation, string shortDescription, string fullDescription, Sprite icon, Sprite backgroundPortrait, Color iconColor) { - var factionFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.3f), parent.RectTransform), style: null); + var factionFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), parent.RectTransform), style: null); if (backgroundPortrait != null) { @@ -568,13 +676,13 @@ namespace Barotrauma var factionInfoHorizontal = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), factionFrame.RectTransform, Anchor.Center), childAnchor: Anchor.CenterLeft, isHorizontal: true) { - RelativeSpacing = 0.02f, + AbsoluteSpacing = GUI.IntScale(5), Stretch = true }; var factionTextContent = new GUILayoutGroup(new RectTransform(Vector2.One, factionInfoHorizontal.RectTransform)) { - RelativeSpacing = 0.05f, + AbsoluteSpacing = GUI.IntScale(10), Stretch = true }; var factionIcon = new GUIImage(new RectTransform(new Point((int)(factionInfoHorizontal.Rect.Height * 0.7f)), factionInfoHorizontal.RectTransform, scaleBasis: ScaleBasis.Smallest), icon, scaleToFit: true) @@ -583,12 +691,48 @@ namespace Barotrauma }; factionInfoHorizontal.Recalculate(); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), factionTextContent.RectTransform), + var header = new GUITextBlock(new RectTransform(new Point(factionTextContent.Rect.Width, GUI.IntScale(40)), factionTextContent.RectTransform), name, font: GUI.SubHeadingFont) { - Padding = Vector4.Zero + Padding = Vector4.Zero, + UserData = "header" }; - var factionDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.6f), factionTextContent.RectTransform), + header.RectTransform.IsFixedSize = true; + + var sliderHolder = new GUILayoutGroup(new RectTransform(new Point((int)(factionTextContent.Rect.Width * 0.8f), GUI.IntScale(20.0f)), factionTextContent.RectTransform), + childAnchor: Anchor.CenterLeft, isHorizontal: true) + { + RelativeSpacing = 0.05f, + Stretch = true + }; + sliderHolder.RectTransform.IsFixedSize = true; + factionTextContent.Recalculate(); + + new GUICustomComponent(new RectTransform(new Vector2(0.8f, 1.0f), sliderHolder.RectTransform), + onDraw: (sb, customComponent) => DrawReputationBar(sb, customComponent.Rect, normalizedReputation)); + + string reputationText = Reputation.GetFormattedReputationText(normalizedReputation, reputation, addColorTags: true); + int reputationChange = (int)Math.Round(reputation - initialReputation); + if (Math.Abs(reputationChange) > 0) + { + string changeText = $"{(reputationChange > 0 ? "+" : "") + reputationChange}"; + string colorStr = XMLExtensions.ColorToString(reputationChange > 0 ? GUI.Style.Green : GUI.Style.Red); + var rtData = RichTextData.GetRichTextData($"{reputationText} (‖color:{colorStr}‖{changeText}‖color:end‖)", out string sanitizedText); + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), sliderHolder.RectTransform), + rtData, sanitizedText, + textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont); + } + else + { + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), sliderHolder.RectTransform), + reputationText, + textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont, parseRichText: true); + } + + //spacing + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), factionTextContent.RectTransform) { MinSize = new Point(0, GUI.IntScale(5)) }, style: null); + + var factionDescription = new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.6f), factionTextContent.RectTransform), shortDescription, font: GUI.SmallFont, wrap: true) { UserData = "description", @@ -599,51 +743,32 @@ namespace Barotrauma factionDescription.ToolTip = fullDescription; } - var sliderHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), factionTextContent.RectTransform), - childAnchor: Anchor.CenterLeft, isHorizontal: true) - { - RelativeSpacing = 0.05f, - Stretch = true - }; - sliderHolder.RectTransform.MaxSize = new Point(int.MaxValue, GUI.IntScale(25.0f)); + //spacing + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), factionTextContent.RectTransform) { MinSize = new Point(0, GUI.IntScale(5)) }, style: null); + + factionInfoHorizontal.Recalculate(); factionTextContent.Recalculate(); - new GUICustomComponent(new RectTransform(new Vector2(0.8f, 1.0f), sliderHolder.RectTransform), - onDraw: (sb, customComponent) => DrawReputationBar(sb, customComponent.Rect, normalizedReputation)); - - string reputationText = ((int)Math.Round(reputation)).ToString(); - int reputationChange = (int)Math.Round( reputation - initialReputation); - if (Math.Abs(reputationChange) > 0) - { - string changeText = $"{(reputationChange > 0 ? "+" : "") + reputationChange}"; - string colorStr = XMLExtensions.ColorToString(reputationChange > 0 ? GUI.Style.Green : GUI.Style.Red); - var rtData = RichTextData.GetRichTextData($"{reputationText} (‖color:{colorStr}‖{changeText}‖color:end‖)", out string sanitizedText); - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), sliderHolder.RectTransform), - rtData, sanitizedText, - textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont); - } - else - { - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), sliderHolder.RectTransform), - reputationText, - textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont); - } + return factionFrame; } public static void DrawReputationBar(SpriteBatch sb, Rectangle rect, float normalizedReputation) { - GUI.DrawRectangle(sb, rect, GUI.Style.ColorInventoryBackground, isFilled: true); - if (normalizedReputation < 0.5f) + int segmentWidth = rect.Width / 5; + rect.Width = segmentWidth * 5; + for (int i = 0; i < 5; i++) { - int barWidth = (int)((0.5f - normalizedReputation) * rect.Width); - GUI.DrawRectangle(sb, new Rectangle(rect.Center.X - barWidth, rect.Y, barWidth, rect.Height), GUI.Style.Red, isFilled: true); + GUI.DrawRectangle(sb, new Rectangle(rect.X + (segmentWidth * i), rect.Y, segmentWidth, rect.Height), Reputation.GetReputationColor(i / 5.0f), isFilled: true); + GUI.DrawRectangle(sb, new Rectangle(rect.X + (segmentWidth * i), rect.Y, segmentWidth, rect.Height), GUI.Style.ColorInventoryBackground, isFilled: false); } - else if (normalizedReputation > 0.5f) - { - int barWidth = (int)((normalizedReputation - 0.5f) * rect.Width); - GUI.DrawRectangle(sb, new Rectangle(rect.Center.X, rect.Y, barWidth, rect.Height), GUI.Style.Green, isFilled: true); - } - GUI.DrawLine(sb, new Vector2(rect.Center.X, rect.Y - 2), new Vector2(rect.Center.X, rect.Bottom + 2), GUI.Style.TextColor); + GUI.DrawRectangle(sb, rect, GUI.Style.ColorInventoryBackground, isFilled: false); + + GUI.Arrow.Draw(sb, new Vector2(rect.X + rect.Width * normalizedReputation, rect.Y), GUI.Style.ColorInventoryBackground, scale: GUI.Scale, spriteEffect: SpriteEffects.FlipVertically); + GUI.Arrow.Draw(sb, new Vector2(rect.X + rect.Width * normalizedReputation, rect.Y), GUI.Style.TextColor, scale: GUI.Scale * 0.8f, spriteEffect: SpriteEffects.FlipVertically); + + GUI.DrawString(sb, new Vector2(rect.X, rect.Bottom), "-100", GUI.Style.TextColor, font: GUI.SmallFont); + Vector2 textSize = GUI.SmallFont.MeasureString("100"); + GUI.DrawString(sb, new Vector2(rect.Right - textSize.X, rect.Bottom), "100", GUI.Style.TextColor, font: GUI.SmallFont); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs index 6e9c0b744..fbeca0143 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs @@ -20,6 +20,7 @@ namespace Barotrauma Audio, VoiceChat, Controls, + Gameplay, #if DEBUG Debug #endif @@ -532,29 +533,19 @@ namespace Barotrauma UserData = tab }; - float tabWidth = 0.25f; + float tabWidth = 1.0f / tabs.Length; #if DEBUG - tabWidth = 0.2f; - if (tab != Tab.Debug) - { + string buttonText = tab != Tab.Debug ? TextManager.Get("SettingsTab." + tab.ToString()) : "Debug"; +#else + string buttonText = TextManager.Get("SettingsTab." + tab.ToString()); #endif - tabButtons[(int)tab] = new GUIButton(new RectTransform(new Vector2(tabWidth, 1.0f), tabButtonHolder.RectTransform), - TextManager.Get("SettingsTab." + tab.ToString()), style: "GUITabButton") - { - UserData = tab, - OnClicked = (bt, userdata) => { SelectTab((Tab)userdata); return true; } - }; -#if DEBUG - } - else + + tabButtons[(int)tab] = new GUIButton(new RectTransform(new Vector2(tabWidth, 1.0f), tabButtonHolder.RectTransform), style: "GUITabButton") { - tabButtons[(int)tab] = new GUIButton(new RectTransform(new Vector2(tabWidth, 1.0f), tabButtonHolder.RectTransform), "Debug", style: "GUITabButton") - { - UserData = tab, - OnClicked = (bt, userdata) => { SelectTab((Tab)userdata); return true; } - }; - } -#endif + UserData = tab, + OnClicked = (bt, userdata) => { SelectTab((Tab)userdata); return true; } + }; + tabButtons[(int)tab].Text = ToolBox.LimitString(buttonText, tabButtons[(int)tab].Font, (int)(0.75f * tabWidth * tabButtonHolder.Rect.Width)); } new GUIButton(new RectTransform(new Vector2(0.05f, 0.75f), tabButtonHolder.RectTransform, Anchor.BottomRight) { RelativeOffset = new Vector2(0.0f, 0.2f) }, style: "GUIBugButton") @@ -669,19 +660,6 @@ namespace Barotrauma Selected = TextureCompressionEnabled }; - GUITickBox pauseOnFocusLostBox = new GUITickBox(new RectTransform(tickBoxScale, leftColumn.RectTransform), - TextManager.Get("PauseOnFocusLost")) - { - Selected = PauseOnFocusLost, - ToolTip = TextManager.Get("PauseOnFocusLostToolTip"), - OnSelected = (tickBox) => - { - PauseOnFocusLost = tickBox.Selected; - UnsavedSettings = true; - return true; - } - }; - GUITextBlock particleLimitText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), rightColumn.RectTransform), TextManager.Get("ParticleLimit"), font: GUI.SubHeadingFont, wrap: true); GUIScrollBar particleScrollBar = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.05f), rightColumn.RectTransform), style: "GUISlider", barSize: 0.1f) @@ -773,56 +751,6 @@ namespace Barotrauma } }; - GUITextBlock HUDScaleText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), rightColumn.RectTransform), TextManager.Get("HUDScale"), font: GUI.SubHeadingFont, wrap: true); - GUIScrollBar HUDScaleScrollBar = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.05f), rightColumn.RectTransform), - style: "GUISlider", barSize: 0.1f) - { - UserData = HUDScaleText, - BarScroll = (HUDScale - MinHUDScale) / (MaxHUDScale - MinHUDScale), - OnMoved = (scrollBar, scroll) => - { - HUDScale = MathHelper.Lerp(MinHUDScale, MaxHUDScale, scroll); - ChangeSliderText(scrollBar, HUDScale); - OnHUDScaleChanged?.Invoke(); - return true; - }, - Step = 0.02f - }; - HUDScaleScrollBar.OnMoved(HUDScaleScrollBar, HUDScaleScrollBar.BarScroll); - - GUITextBlock inventoryScaleText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), rightColumn.RectTransform), TextManager.Get("InventoryScale"), font: GUI.SubHeadingFont); - GUIScrollBar inventoryScaleScrollBar = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.05f), rightColumn.RectTransform), - style: "GUISlider", barSize: 0.1f) - { - UserData = inventoryScaleText, - BarScroll = (InventoryScale - MinInventoryScale) / (MaxInventoryScale - MinInventoryScale), - OnMoved = (scrollBar, scroll) => - { - InventoryScale = MathHelper.Lerp(MinInventoryScale, MaxInventoryScale, scroll); - ChangeSliderText(scrollBar, InventoryScale); - return true; - }, - Step = 0.02f - }; - inventoryScaleScrollBar.OnMoved(inventoryScaleScrollBar, inventoryScaleScrollBar.BarScroll); - - GUITextBlock textScaleText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), rightColumn.RectTransform), TextManager.Get("TextScale"), font: GUI.SubHeadingFont); - GUIScrollBar textScaleScrollBar = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.05f), rightColumn.RectTransform), - style: "GUISlider", barSize: 0.1f) - { - UserData = textScaleText, - BarScroll = (TextScale - MinTextScale) / (MaxTextScale - MinTextScale), - OnMoved = (scrollBar, scroll) => - { - TextScale = MathHelper.Lerp(MinTextScale, MaxTextScale, scroll); - textScaleDirty = true; - ChangeSliderText(scrollBar, TextScale); - return true; - }, - Step = 0.01f - }; - textScaleScrollBar.OnMoved(textScaleScrollBar, textScaleScrollBar.BarScroll); - /// Audio tab ---------------------------------------------------------------- var audioContent = new GUILayoutGroup(new RectTransform(new Vector2(0.97f, 0.97f), tabs[(int)Tab.Audio].RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) @@ -1177,13 +1105,14 @@ namespace Barotrauma style: "GUISlider", barSize: 0.05f) { UserData = micVolumeText, - Range = new Vector2(0,540), - Step = 1.0f / 9.0f + Range = new Vector2(0, ((float)VoipConfig.BUFFER_SIZE / (float)VoipConfig.FREQUENCY) * 1000.0f * 25.0f), + Step = 1.0f / 25.0f }; cutoffPreventionSlider.BarScrollValue = VoiceChatCutoffPrevention; cutoffPreventionSlider.OnMoved = (scrollBar, scroll) => { - VoiceChatCutoffPrevention = (int)scrollBar.BarScrollValue; + int bufferMsLength = (int)(((float)VoipConfig.BUFFER_SIZE / (float)VoipConfig.FREQUENCY) * 1000.0f); + VoiceChatCutoffPrevention = (int)Math.Round(scrollBar.BarScrollValue / bufferMsLength) * bufferMsLength; cutoffPreventionText.Text = TextManager.Get("CutoffPrevention") + " " + TextManager.GetWithVariable("timeformatmilliseconds", "[milliseconds]", VoiceChatCutoffPrevention.ToString()); return true; @@ -1379,6 +1308,93 @@ namespace Barotrauma GUITextBlock.AutoScaleAndNormalize(defaultBindingsButton.TextBlock, legacyBindingsButton.TextBlock); }; + /// Gameplay tab ------------------------------------------------------------- + var gameplaySettingsGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.46f, 0.95f), tabs[(int)Tab.Gameplay].RectTransform, Anchor.TopLeft) + { RelativeOffset = new Vector2(0.025f, 0.02f) }) + { RelativeSpacing = 0.01f }; + + GUITickBox pauseOnFocusLostBox = new GUITickBox(new RectTransform(tickBoxScale, gameplaySettingsGroup.RectTransform), + TextManager.Get("PauseOnFocusLost")) + { + Selected = PauseOnFocusLost, + ToolTip = TextManager.Get("PauseOnFocusLostToolTip"), + OnSelected = (tickBox) => + { + PauseOnFocusLost = tickBox.Selected; + UnsavedSettings = true; + return true; + } + }; + + GUITickBox disableInGameHintsBox = new GUITickBox(new RectTransform(tickBoxScale, gameplaySettingsGroup.RectTransform), + TextManager.Get("DisableInGameHints")) + { + Selected = DisableInGameHints, + ToolTip = TextManager.Get("DisableInGameHintsToolTip"), + OnSelected = (tickBox) => + { + DisableInGameHints = tickBox.Selected; + if (!DisableInGameHints && GameMain.Config?.IgnoredHints != null) + { + // Reset the ignored hints when the hints are re-enabled (to-be-replaced by a separate button) + GameMain.Config.IgnoredHints.Clear(); + } + UnsavedSettings = true; + return true; + } + }; + + GUITextBlock HUDScaleText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), gameplaySettingsGroup.RectTransform), TextManager.Get("HUDScale"), font: GUI.SubHeadingFont, wrap: true); + GUIScrollBar HUDScaleScrollBar = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.05f), gameplaySettingsGroup.RectTransform), + style: "GUISlider", barSize: 0.1f) + { + UserData = HUDScaleText, + BarScroll = (HUDScale - MinHUDScale) / (MaxHUDScale - MinHUDScale), + OnMoved = (scrollBar, scroll) => + { + HUDScale = MathHelper.Lerp(MinHUDScale, MaxHUDScale, scroll); + ChangeSliderText(scrollBar, HUDScale); + OnHUDScaleChanged?.Invoke(); + return true; + }, + Step = 0.02f + }; + HUDScaleScrollBar.OnMoved(HUDScaleScrollBar, HUDScaleScrollBar.BarScroll); + + GUITextBlock inventoryScaleText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), gameplaySettingsGroup.RectTransform), TextManager.Get("InventoryScale"), font: GUI.SubHeadingFont); + GUIScrollBar inventoryScaleScrollBar = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.05f), gameplaySettingsGroup.RectTransform), + style: "GUISlider", barSize: 0.1f) + { + UserData = inventoryScaleText, + BarScroll = (InventoryScale - MinInventoryScale) / (MaxInventoryScale - MinInventoryScale), + OnMoved = (scrollBar, scroll) => + { + InventoryScale = MathHelper.Lerp(MinInventoryScale, MaxInventoryScale, scroll); + ChangeSliderText(scrollBar, InventoryScale); + return true; + }, + Step = 0.02f + }; + inventoryScaleScrollBar.OnMoved(inventoryScaleScrollBar, inventoryScaleScrollBar.BarScroll); + + GUITextBlock textScaleText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), gameplaySettingsGroup.RectTransform), TextManager.Get("TextScale"), font: GUI.SubHeadingFont); + GUIScrollBar textScaleScrollBar = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.05f), gameplaySettingsGroup.RectTransform), + style: "GUISlider", barSize: 0.1f) + { + UserData = textScaleText, + BarScroll = (TextScale - MinTextScale) / (MaxTextScale - MinTextScale), + OnMoved = (scrollBar, scroll) => + { + TextScale = MathHelper.Lerp(MinTextScale, MaxTextScale, scroll); + textScaleDirty = true; + ChangeSliderText(scrollBar, TextScale); + return true; + }, + Step = 0.01f + }; + textScaleScrollBar.OnMoved(textScaleScrollBar, textScaleScrollBar.BarScroll); + + /// Bottom buttons ------------------------------------------------------------- new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonArea.RectTransform, Anchor.BottomLeft), TextManager.Get("Cancel")) { @@ -1464,55 +1480,54 @@ namespace Barotrauma { RelativeOffset = new Vector2(0.02f, 0.02f) }) { RelativeSpacing = 0.01f }; - var automaticQuickStartTickBox = new GUITickBox(new RectTransform(tickBoxScale / 0.18f, debugTickBoxes.RectTransform, scaleBasis: ScaleBasis.BothHeight), "Automatic quickstart enabled", style: "GUITickBox"); - automaticQuickStartTickBox.Selected = AutomaticQuickStartEnabled; - automaticQuickStartTickBox.ToolTip = "Will the game automatically move on to Quickstart when the game is launched"; - automaticQuickStartTickBox.OnSelected = (tickBox) => + void addDebugTickBox(bool initialValue, Action set, string label, string tooltip) { - AutomaticQuickStartEnabled = tickBox.Selected; - UnsavedSettings = true; - return true; - }; + var tickBox = new GUITickBox(new RectTransform(tickBoxScale / 0.18f, debugTickBoxes.RectTransform, scaleBasis: ScaleBasis.BothHeight), label, style: "GUITickBox"); + tickBox.Selected = initialValue; + tickBox.ToolTip = tooltip; + tickBox.OnSelected = (tickBox) => + { + set(tickBox.Selected); + UnsavedSettings = true; + return true; + }; + } - var automaticCampaignLoadTickBox = new GUITickBox(new RectTransform(tickBoxScale / 0.18f, debugTickBoxes.RectTransform, scaleBasis: ScaleBasis.BothHeight), "Automatic campaign load enabled", style: "GUITickBox"); - automaticCampaignLoadTickBox.Selected = AutomaticCampaignLoadEnabled; - automaticCampaignLoadTickBox.ToolTip = "Will the game automatically load the latest campaign save when the game is launched"; - automaticCampaignLoadTickBox.OnSelected = (tickBox) => - { - AutomaticCampaignLoadEnabled = tickBox.Selected; - UnsavedSettings = true; - return true; - }; + addDebugTickBox( + AutomaticQuickStartEnabled, + (b) => AutomaticQuickStartEnabled = b, + "Automatic quickstart enabled", + "Will the game automatically move on to Quickstart when the game is launched"); - var showSplashScreenTickBox = new GUITickBox(new RectTransform(tickBoxScale / 0.18f, debugTickBoxes.RectTransform, scaleBasis: ScaleBasis.BothHeight), "Splash screen enabled", style: "GUITickBox"); - showSplashScreenTickBox.Selected = EnableSplashScreen; - showSplashScreenTickBox.ToolTip = "Are the splash screens shown when the game is launched"; - showSplashScreenTickBox.OnSelected = (tickBox) => - { - EnableSplashScreen = tickBox.Selected; - UnsavedSettings = true; - return true; - }; + addDebugTickBox( + AutomaticCampaignLoadEnabled, + (b) => AutomaticCampaignLoadEnabled = b, + "Automatic campaign load enabled", + "Will the game automatically load the latest campaign save when the game is launched"); - var verboseLoggingTickBox = new GUITickBox(new RectTransform(tickBoxScale / 0.18f, debugTickBoxes.RectTransform, scaleBasis: ScaleBasis.BothHeight), "Verbose logging enabled", style: "GUITickBox"); - verboseLoggingTickBox.Selected = VerboseLogging; - verboseLoggingTickBox.ToolTip = "Should verbose logging be used"; - verboseLoggingTickBox.OnSelected = (tickBox) => - { - VerboseLogging = tickBox.Selected; - UnsavedSettings = true; - return true; - }; + addDebugTickBox( + EnableSplashScreen, + (b) => EnableSplashScreen = b, + "Splash screen enabled", + "Are the splash screens shown when the game is launched"); - var textManagerDebugModeTickBox = new GUITickBox(new RectTransform(tickBoxScale / 0.18f, debugTickBoxes.RectTransform, scaleBasis: ScaleBasis.BothHeight), "TextManager debug mode enabled", style: "GUITickBox"); - textManagerDebugModeTickBox.Selected = TextManagerDebugModeEnabled; - textManagerDebugModeTickBox.ToolTip = "Does the TextManager return the text tags for debug purposes?"; - textManagerDebugModeTickBox.OnSelected = (tickBox) => - { - TextManagerDebugModeEnabled = tickBox.Selected; - UnsavedSettings = true; - return true; - }; + addDebugTickBox( + VerboseLogging, + (b) => VerboseLogging = b, + "Verbose logging enabled", + "Should verbose logging be used"); + + addDebugTickBox( + TextManagerDebugModeEnabled, + (b) => TextManagerDebugModeEnabled = b, + "TextManager debug mode enabled", + "Does the TextManager return the text tags for debug purposes?"); + + addDebugTickBox( + ModBreakerMode, + (b) => ModBreakerMode = b, + "Mod breaker mode enabled", + "Do horrible things when loading mods to see if it breaks?"); #endif UnsavedSettings = false; // Reset unsaved settings to false once the UI has been created diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index bc26ebca2..73cdda02b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -160,13 +160,6 @@ namespace Barotrauma CreateSlots(); } - public override void RemoveItem(Item item) - { - if (!Contains(item)) { return; } - base.RemoveItem(item); - CreateSlots(); - } - public override void CreateSlots() { if (visualSlots == null) { visualSlots = new VisualSlot[capacity]; } @@ -639,10 +632,16 @@ namespace Barotrauma foreach (Item doubleClickedItem in doubleClickedItems) { QuickUseItem(doubleClickedItem, true, true, true, quickUseAction, playSound: doubleClickedItem == doubleClickedItems.First()); + //only use one item if we're equipping or using it as a treatment if (quickUseAction == QuickUseAction.Equip || quickUseAction == QuickUseAction.UseTreatment) { break; } + //if the item was put in a limb slot, only put one item from the stack + if (doubleClickedItem.ParentInventory == this && !IsInLimbSlot(doubleClickedItem, InvSlotType.Any)) + { + break; + } } } @@ -696,6 +695,7 @@ namespace Barotrauma if (firstItem != null && !DraggingItems.Contains(firstItem) && Character.Controlled?.Inventory == this && GUI.KeyboardDispatcher.Subscriber == null && !CrewManager.IsCommandInterfaceOpen && PlayerInput.InventoryKeyHit(visualSlots[i].InventoryKeyIndex)) { + if (SubEditorScreen.IsSubEditor() && SubEditorScreen.SkipInventorySlotUpdate) { continue; } #if LINUX // some window managers on Linux use windows key + number to change workspaces or perform other actions if (PlayerInput.KeyDown(Keys.RightWindows) || PlayerInput.KeyDown(Keys.LeftWindows)) { continue; } @@ -810,6 +810,8 @@ namespace Barotrauma highlightedSubInventorySlot.Inventory.HideTimer = 0.0f; } } + + HintManager.OnShowSubInventory(slotRef?.Item); } public void AssignQuickUseNumKeys() @@ -835,6 +837,13 @@ namespace Barotrauma if (item.ParentInventory != this) { + if (Screen.Selected == GameMain.GameScreen) + { + if (item.NonInteractable || item.NonPlayerTeamInteractable) + { + return QuickUseAction.None; + } + } if (item.ParentInventory == null || item.ParentInventory.Locked) { return QuickUseAction.None; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs index a223189a9..4ee0d2b1f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs @@ -70,17 +70,15 @@ namespace Barotrauma.Items.Components public override void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { base.ClientRead(type, msg, sendingTime); + + bool readAttachData = msg.ReadBoolean(); + if (!readAttachData) { return; } + bool shouldBeAttached = msg.ReadBoolean(); Vector2 simPosition = new Vector2(msg.ReadSingle(), msg.ReadSingle()); UInt16 submarineID = msg.ReadUInt16(); Submarine sub = Entity.FindEntityByID(submarineID) as Submarine; - if (!attachable) - { - DebugConsole.ThrowError("Received an attachment event for an item that's not attachable."); - return; - } - if (shouldBeAttached) { if (!attached) @@ -96,7 +94,6 @@ namespace Barotrauma.Items.Components if (attached) { DropConnectedWires(null); - if (body != null) { item.body = body; @@ -106,6 +103,11 @@ namespace Barotrauma.Items.Components DeattachFromWall(); } + else + { + item.SetTransform(simPosition, 0.0f); + item.Submarine = sub; + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index 32abbb2a1..ee39accbf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -179,6 +179,23 @@ namespace Barotrauma.Items.Components { CanBeFocused = false }; + + // Expand the frame vertically if it's too small to fit the text + if (label != null && label.RectTransform.RelativeSize.Y > 0.5f) + { + int newHeight = (int)(GuiFrame.Rect.Height + (2 * (label.RectTransform.RelativeSize.Y - 0.5f) * content.Rect.Height)); + if (newHeight > GuiFrame.RectTransform.MaxSize.Y) + { + Point newMaxSize = GuiFrame.RectTransform.MaxSize; + newMaxSize.Y = newHeight; + GuiFrame.RectTransform.MaxSize = newMaxSize; + } + GuiFrame.RectTransform.Resize(new Point(GuiFrame.Rect.Width, newHeight)); + content.RectTransform.Resize(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin); + label.CalculateHeightFromText(); + guiCustomComponent.RectTransform.Resize(new Vector2(1.0f, Math.Max(1.0f - label.RectTransform.RelativeSize.Y, minInventoryAreaSize))); + } + Inventory.RectTransform = guiCustomComponent.RectTransform; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index 0e1db1b6d..ecc4b8b83 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -39,6 +39,8 @@ namespace Barotrauma.Items.Components private Pair tooltip; + private GUITextBlock requiredTimeBlock; + partial void InitProjSpecific() { CreateGUI(); @@ -384,8 +386,6 @@ namespace Barotrauma.Items.Components FabricationRecipe targetItem = fabricatedItem ?? selectedItem; if (targetItem != null) { - var itemIcon = targetItem.TargetItem.InventoryIcon ?? targetItem.TargetItem.sprite; - Rectangle slotRect = outputContainer.Inventory.visualSlots[0].Rect; if (fabricatedItem != null) @@ -398,11 +398,15 @@ namespace Barotrauma.Items.Components GUI.Style.Green * 0.5f, isFilled: true); } - itemIcon.Draw( - spriteBatch, - slotRect.Center.ToVector2(), - color: targetItem.TargetItem.InventoryIconColor * 0.4f, - scale: Math.Min(slotRect.Width / itemIcon.size.X, slotRect.Height / itemIcon.size.Y) * 0.9f); + if (outputContainer.Inventory.IsEmpty()) + { + var itemIcon = targetItem.TargetItem.InventoryIcon ?? targetItem.TargetItem.sprite; + itemIcon.Draw( + spriteBatch, + slotRect.Center.ToVector2(), + color: targetItem.TargetItem.InventoryIconColor * 0.4f, + scale: Math.Min(slotRect.Width / itemIcon.size.X, slotRect.Height / itemIcon.size.Y) * 0.9f); + } } if (tooltip != null) @@ -522,7 +526,7 @@ namespace Barotrauma.Items.Components AutoScaleHorizontal = true, }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform), ToolBox.SecondsToReadableTime(requiredTime), + requiredTimeBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform), ToolBox.SecondsToReadableTime(requiredTime), font: GUI.SmallFont); return true; } @@ -606,6 +610,12 @@ namespace Barotrauma.Items.Components } } + partial void UpdateRequiredTimeProjSpecific() + { + if (requiredTimeBlock == null) { return; } + requiredTimeBlock.Text = ToolBox.SecondsToReadableTime(timeUntilReady > 0.0f ? timeUntilReady : requiredTime); + } + public void ClientWrite(IWriteMessage msg, object[] extraData = null) { int itemIndex = pendingFabricatedItem == null ? -1 : fabricationRecipes.IndexOf(pendingFabricatedItem); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index 047b5375a..d98beb236 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -21,18 +21,14 @@ namespace Barotrauma.Items.Components private readonly List displayedSubs = new List(); + private Point prevResolution; + partial void InitProjSpecific(XElement element) { noPowerTip = TextManager.Get("SteeringNoPowerTip"); CreateGUI(); } - protected override void OnResolutionChanged() - { - base.OnResolutionChanged(); - CreateHUD(); - } - protected override void CreateGUI() { GuiFrame.RectTransform.RelativeOffset = new Vector2(0.05f, 0.0f); @@ -76,15 +72,10 @@ namespace Barotrauma.Items.Components hullInfoFrame.AddToGUIUpdateList(order: 1); } - public override void OnMapLoaded() - { - base.OnMapLoaded(); - CreateHUD(); - } - private void CreateHUD() { - submarineContainer.ClearChildren(); + prevResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); + submarineContainer?.ClearChildren(); if (item.Submarine == null) { return; } @@ -94,19 +85,15 @@ namespace Barotrauma.Items.Components displayedSubs.AddRange(item.Submarine.DockedTo); } - public override void FlipX(bool relativeToSub) - { - CreateHUD(); - } - public override void UpdateHUD(Character character, float deltaTime, Camera cam) { //recreate HUD if the subs we should display have changed - if ((item.Submarine == null && displayedSubs.Count > 0) || //item not inside a sub anymore, but display is still showing subs - !displayedSubs.Contains(item.Submarine) || //current sub not displayer - item.Submarine.DockedTo.Any(s => !displayedSubs.Contains(s)) || //some of the docked subs not diplayed - !submarineContainer.Children.Any() || // We lack a GUI - displayedSubs.Any(s => s != item.Submarine && !item.Submarine.DockedTo.Contains(s))) //displaying a sub that shouldn't be displayed + if ((item.Submarine == null && displayedSubs.Count > 0) || //item not inside a sub anymore, but display is still showing subs + !displayedSubs.Contains(item.Submarine) || //current sub not displayer + prevResolution.X != GameMain.GraphicsWidth || prevResolution.Y != GameMain.GraphicsHeight || //resolution changed + item.Submarine.DockedTo.Any(s => !displayedSubs.Contains(s)) || //some of the docked subs not diplayed + !submarineContainer.Children.Any() || // We lack a GUI + displayedSubs.Any(s => s != item.Submarine && !item.Submarine.DockedTo.Contains(s))) //displaying a sub that shouldn't be displayed { CreateHUD(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index 19056309a..7986a538b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -16,7 +16,8 @@ namespace Barotrauma.Items.Components { Default, Disruption, - Destructible + Destructible, + LongRange } private PathFinder pathFinder; @@ -69,6 +70,9 @@ namespace Barotrauma.Items.Components private const float DisruptionUpdateInterval = 0.2f; private float disruptionUpdateTimer; + private const float LongRangeUpdateInterval = 10.0f; + private float longRangeUpdateTimer; + private float showDirectionalIndicatorTimer; private readonly List nearbyObjects = new List(); @@ -122,6 +126,10 @@ namespace Barotrauma.Items.Components { BlipType.Destructible, new Color[] { Color.TransparentBlack, new Color(74, 113, 75) * 0.8f, new Color(151, 236, 172) * 0.8f, new Color(153, 217, 234) * 0.8f } + }, + { + BlipType.LongRange, + new Color[] { Color.TransparentBlack, Color.TransparentBlack, new Color(254, 68, 19) * 0.8f, Color.TransparentBlack } } }; @@ -133,7 +141,7 @@ namespace Barotrauma.Items.Components public static Vector2 GUISizeCalculation => Vector2.One * Math.Min(GUI.RelativeHorizontalAspectRatio, 1f) * sonarAreaSize; - private List>> MineralClusters { get; set; } + private List<(Vector2 center, List resources)> MineralClusters { get; set; } private readonly List textBlocksToScaleAndNormalize = new List(); @@ -471,25 +479,26 @@ namespace Barotrauma.Items.Components { if (MineralClusters == null) { - MineralClusters = new List>>(); - foreach (var p in Level.Loaded.PathPoints) + MineralClusters = new List<(Vector2, List)>(); + Level.Loaded.PathPoints.ForEach(p => p.ClusterLocations.ForEach(c => AddIfValid(c))); + Level.Loaded.AbyssResources.ForEach(c => AddIfValid(c)); + + void AddIfValid(Level.ClusterLocation c) { - foreach (var c in p.ClusterLocations) + if (c.Resources == null) { return; } + if (c.Resources.None(i => i != null && !i.Removed && i.Tags.Contains("ore"))) { return; } + var pos = Vector2.Zero; + foreach (var r in c.Resources) { - if (c.Resources.None(i => i != null && !i.Removed && i.Tags.Contains("ore"))) { continue; } - var pos = Vector2.Zero; - foreach (var r in c.Resources) - { - pos += r.WorldPosition; - } - pos /= c.Resources.Count; - MineralClusters.Add(new Tuple>(pos, c.Resources)); + pos += r.WorldPosition; } + pos /= c.Resources.Count; + MineralClusters.Add((center: pos, resources: c.Resources)); } } else { - MineralClusters.RemoveAll(t => t.Item2 == null || t.Item2.None() || t.Item2.All(i => i == null || i.Removed)); + MineralClusters.RemoveAll(c => c.resources == null || c.resources.None() || c.resources.All(i => i == null || i.Removed)); } } @@ -673,7 +682,6 @@ namespace Barotrauma.Items.Components } disruptionUpdateTimer -= deltaTime; - for (var pingIndex = 0; pingIndex < activePingsCount; ++pingIndex) { var activePing = activePings[pingIndex]; @@ -683,12 +691,46 @@ namespace Barotrauma.Items.Components pingRadius, activePing.PrevPingRadius, displayScale, range / zoom, passive: false, pingStrength: 2.0f); activePing.PrevPingRadius = pingRadius; } - if (disruptionUpdateTimer <= 0.0f) { disruptionUpdateTimer = DisruptionUpdateInterval; } + longRangeUpdateTimer -= deltaTime; + if (longRangeUpdateTimer <= 0.0f) + { + foreach (Character c in Character.CharacterList) + { + if (c.AnimController.CurrentHull != null || !c.Enabled) { continue; } + if (c.Params.HideInSonar) { continue; } + + if (!c.IsUnconscious && c.Params.DistantSonarRange > 0.0f && + ((c.WorldPosition - transducerCenter) * displayScale).LengthSquared() > DisplayRadius * DisplayRadius) + { + Vector2 targetVector = c.WorldPosition - transducerCenter; + if (targetVector.LengthSquared() > MathUtils.Pow2(c.Params.DistantSonarRange)) { continue; } + float dist = targetVector.Length(); + Vector2 targetDir = targetVector / dist; + int blipCount = (int)MathHelper.Clamp(c.Mass, 50, 200); + for (int i = 0; i < blipCount; i++) + { + float angle = Rand.Range(-0.5f, 0.5f); + Vector2 blipDir = MathUtils.RotatePoint(targetDir, angle); + Vector2 invBlipDir = MathUtils.RotatePoint(targetDir, -angle); + var longRangeBlip = new SonarBlip(transducerCenter + blipDir * Range * 0.9f, Rand.Range(1.9f, 2.1f), Rand.Range(1.0f, 1.5f), BlipType.LongRange) + { + Velocity = -invBlipDir * (MathUtils.Round(Rand.Range(8000.0f, 15000.0f), 2000.0f) - Math.Abs(angle * angle * 10000.0f)), + Rotation = (float)Math.Atan2(-invBlipDir.Y, invBlipDir.X), + Alpha = MathUtils.Pow2((c.Params.DistantSonarRange - dist) / c.Params.DistantSonarRange) + }; + longRangeBlip.Size.Y *= 5.0f; + sonarBlips.Add(longRangeBlip); + } + } + } + longRangeUpdateTimer = LongRangeUpdateInterval; + } + if (currentMode == Mode.Active && currentPingIndex != -1) { return; @@ -828,8 +870,8 @@ namespace Barotrauma.Items.Components float directionalPingVisibility = useDirectionalPing && currentMode == Mode.Active ? 1.0f : showDirectionalIndicatorTimer; if (directionalPingVisibility > 0.0f) { - Vector2 sector1 = MathUtils.RotatePointAroundTarget(pingDirection * DisplayRadius, Vector2.Zero, DirectionalPingSector * 0.5f); - Vector2 sector2 = MathUtils.RotatePointAroundTarget(pingDirection * DisplayRadius, Vector2.Zero, -DirectionalPingSector * 0.5f); + Vector2 sector1 = MathUtils.RotatePointAroundTarget(pingDirection * DisplayRadius, Vector2.Zero, MathHelper.ToRadians(DirectionalPingSector * 0.5f)); + Vector2 sector2 = MathUtils.RotatePointAroundTarget(pingDirection * DisplayRadius, Vector2.Zero, MathHelper.ToRadians(-DirectionalPingSector * 0.5f)); DrawLine(spriteBatch, Vector2.Zero, sector1, Color.LightCyan * 0.2f * directionalPingVisibility, width: 3); DrawLine(spriteBatch, Vector2.Zero, sector2, Color.LightCyan * 0.2f * directionalPingVisibility, width: 3); } @@ -862,9 +904,9 @@ namespace Barotrauma.Items.Components { DrawMarker(spriteBatch, Level.Loaded.StartLocation.Name, - "outpost", + Level.Loaded.StartOutpost != null ? "outpost" : "location", Level.Loaded.StartLocation.Name, - Level.Loaded.StartPosition, transducerCenter, + Level.Loaded.StartExitPosition, transducerCenter, displayScale, center, DisplayRadius); } @@ -872,9 +914,9 @@ namespace Barotrauma.Items.Components { DrawMarker(spriteBatch, Level.Loaded.EndLocation.Name, - "outpost", + Level.Loaded.EndOutpost != null ? "outpost" : "location", Level.Loaded.EndLocation.Name, - Level.Loaded.EndPosition, transducerCenter, + Level.Loaded.EndExitPosition, transducerCenter, displayScale, center, DisplayRadius); } @@ -906,10 +948,8 @@ namespace Barotrauma.Items.Components } } - if (GameMain.GameSession.Mission != null) + foreach (Mission mission in GameMain.GameSession.Missions) { - var mission = GameMain.GameSession.Mission; - if (!string.IsNullOrWhiteSpace(mission.SonarLabel)) { foreach (Vector2 sonarPosition in mission.SonarPositions) @@ -926,16 +966,16 @@ namespace Barotrauma.Items.Components if (AllowUsingMineralScanner && useMineralScanner && CurrentMode == Mode.Active && MineralClusters != null) { - foreach (var t in MineralClusters) + foreach (var c in MineralClusters) { - var unobtainedMinerals = t.Item2.Where(i => i != null && i.GetRootInventoryOwner() == i); + var unobtainedMinerals = c.resources.Where(i => i != null && i.GetRootInventoryOwner() == i); if (unobtainedMinerals.None()) { continue; } - if (!CheckResourceMarkerVisibility(t.Item1, transducerCenter)) { continue; } + if (!CheckResourceMarkerVisibility(c.center, transducerCenter)) { continue; } var i = unobtainedMinerals.FirstOrDefault(); if (i == null) { continue; } DrawMarker(spriteBatch, i.Name, "mineral", i, - t.Item1, transducerCenter, + c.center, transducerCenter, displayScale, center, DisplayRadius * 0.95f, onlyShowTextOnMouseOver: true); } @@ -947,6 +987,19 @@ namespace Barotrauma.Items.Components if (connectedSubs.Contains(sub)) { continue; } if (sub.WorldPosition.Y > Level.Loaded.Size.Y) { continue; } + if (item.Submarine != null) + { + //hide enemy team + if (sub.TeamID == CharacterTeamType.Team1 && (item.Submarine.TeamID == CharacterTeamType.Team2 || Character.Controlled?.TeamID == CharacterTeamType.Team2)) + { + continue; + } + else if (sub.TeamID == CharacterTeamType.Team2 && (item.Submarine.TeamID == CharacterTeamType.Team1 || Character.Controlled?.TeamID == CharacterTeamType.Team1)) + { + continue; + } + } + DrawMarker(spriteBatch, sub.Info.DisplayName, sub.Info.HasTag(SubmarineTag.Shuttle) ? "shuttle" : "submarine", @@ -1046,15 +1099,18 @@ namespace Barotrauma.Items.Components foreach (DockingPort dockingPort in DockingPort.List) { if (Level.Loaded != null && dockingPort.Item.Submarine.WorldPosition.Y > Level.Loaded.Size.Y) { continue; } - if (dockingPort.Item.Submarine == null) { continue; } if (dockingPort.Item.Submarine.Info.IsWreck) { continue; } + if (!dockingPort.Item.Submarine.ShowSonarMarker && !dockingPort.Item.Submarine.Info.IsOutpost) { continue; } //don't show the docking ports of the opposing team on the sonar if (item.Submarine != null) { - if ((dockingPort.Item.Submarine.TeamID == CharacterTeamType.Team1 && item.Submarine.TeamID == CharacterTeamType.Team2) || - (dockingPort.Item.Submarine.TeamID == CharacterTeamType.Team2 && item.Submarine.TeamID == CharacterTeamType.Team1)) + if (dockingPort.Item.Submarine.TeamID == CharacterTeamType.Team1 && (item.Submarine.TeamID == CharacterTeamType.Team2 || Character.Controlled?.TeamID == CharacterTeamType.Team2)) + { + continue; + } + else if (dockingPort.Item.Submarine.TeamID == CharacterTeamType.Team2 && (item.Submarine.TeamID == CharacterTeamType.Team1 || Character.Controlled?.TeamID == CharacterTeamType.Team1)) { continue; } @@ -1382,6 +1438,7 @@ namespace Barotrauma.Items.Components MathHelper.Clamp(c.Mass * 0.03f, 0.1f, 2.0f)); if (!passive && !CheckBlipVisibility(blip, transducerPos)) { continue; } sonarBlips.Add(blip); + HintManager.OnSonarSpottedCharacter(Item, c); } continue; } @@ -1401,6 +1458,7 @@ namespace Barotrauma.Items.Components MathHelper.Clamp(limb.Mass * 0.1f, 0.1f, 2.0f)); if (!passive && !CheckBlipVisibility(blip, transducerPos)) { continue; } sonarBlips.Add(blip); + HintManager.OnSonarSpottedCharacter(Item, c); } } } @@ -1554,12 +1612,12 @@ namespace Barotrauma.Items.Components float scale = (strength + 3.0f) * blip.Scale * blipScale; Color color = ToolBox.GradientLerp(strength, blipColorGradient[blip.BlipType]); - sonarBlip.Draw(spriteBatch, center + pos, color, sonarBlip.Origin, blip.Rotation ?? MathUtils.VectorToAngle(pos), + sonarBlip.Draw(spriteBatch, center + pos, color * blip.Alpha, sonarBlip.Origin, blip.Rotation ?? MathUtils.VectorToAngle(pos), blip.Size * scale * 0.5f, SpriteEffects.None, 0); pos += Rand.Range(0.0f, 1.0f) * dir + Rand.Range(-scale, scale) * normal; - sonarBlip.Draw(spriteBatch, center + pos, color * 0.5f, sonarBlip.Origin, 0, scale, SpriteEffects.None, 0); + sonarBlip.Draw(spriteBatch, center + pos, color * 0.5f * blip.Alpha, sonarBlip.Origin, 0, scale, SpriteEffects.None, 0); } private void DrawMarker(SpriteBatch spriteBatch, string label, string iconIdentifier, object targetIdentifier, Vector2 worldPosition, Vector2 transducerPosition, float scale, Vector2 center, float radius, @@ -1644,7 +1702,7 @@ namespace Barotrauma.Items.Components } } - if (string.IsNullOrEmpty(iconIdentifier) || !targetIcons.ContainsKey(iconIdentifier)) + if (iconIdentifier == null || !targetIcons.ContainsKey(iconIdentifier)) { GUI.DrawRectangle(spriteBatch, new Rectangle((int)markerPos.X - 3, (int)markerPos.Y - 3, 6, 6), markerColor, thickness: 2); } @@ -1777,6 +1835,7 @@ namespace Barotrauma.Items.Components public float? Rotation; public Vector2 Size; public Sonar.BlipType BlipType; + public float Alpha = 1.0f; public SonarBlip(Vector2 pos, float fadeTimer, float scale, Sonar.BlipType blipType = Sonar.BlipType.Default) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index 622ea1cf9..1f32930b3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -351,14 +351,19 @@ namespace Barotrauma.Items.Components { OnClicked = (btn, userdata) => { - if (GameMain.GameSession?.Campaign is CampaignMode campaign) + if (GameMain.GameSession?.Missions.Any(m => !m.AllowUndocking) ?? false) + { + new GUIMessageBox("", TextManager.Get("undockingdisabledbymission")); + return false; + } + else if (GameMain.GameSession?.Campaign is CampaignMode campaign) { if (Level.IsLoadedOutpost && DockingSources.Any(d => d.Docked && (d.DockingTarget?.Item.Submarine?.Info?.IsOutpost ?? false))) { // Undocking from an outpost - campaign.CampaignUI.SelectTab(CampaignMode.InteractionType.Map); campaign.ShowCampaignUI = true; + campaign.CampaignUI.SelectTab(CampaignMode.InteractionType.Map); return false; } else if (!Level.IsLoadedOutpost && DockingModeEnabled && ActiveDockingSource != null && @@ -398,7 +403,7 @@ namespace Barotrauma.Items.Components { if (GameMain.Client == null) { - item.SendSignal(0, "1", "toggle_docking", sender: null); + item.SendSignal("1", "toggle_docking"); } else { @@ -722,7 +727,19 @@ namespace Barotrauma.Items.Components } } - pressureWarningText.Visible = item.Submarine != null && item.Submarine.AtDamageDepth && Timing.TotalTime % 1.0f < 0.8f; + pressureWarningText.Visible = item.Submarine != null && Timing.TotalTime % 1.0f < 0.8f; + float depthEffectThreshold = 500.0f; + if (Level.Loaded != null && pressureWarningText.Visible && + item.Submarine.RealWorldDepth > Level.Loaded.RealWorldCrushDepth - depthEffectThreshold && item.Submarine.RealWorldDepth > item.Submarine.RealWorldCrushDepth - depthEffectThreshold) + { + pressureWarningText.Visible = true; + pressureWarningText.Text = item.Submarine.AtDamageDepth ? TextManager.Get("SteeringDepthWarning") : TextManager.Get("SteeringDepthWarningLow").Replace("[crushdepth]", ((int)item.Submarine.RealWorldCrushDepth).ToString()); + } + else + { + pressureWarningText.Visible = false; + } + iceSpireWarningText.Visible = item.Submarine != null && !pressureWarningText.Visible && showIceSpireWarning && Timing.TotalTime % 1.0f < 0.8f; if (Vector2.DistanceSquared(PlayerInput.MousePosition, steerArea.Rect.Center.ToVector2()) < steerRadius * steerRadius) @@ -748,7 +765,7 @@ namespace Barotrauma.Items.Components } if (!AutoPilot && Character.DisableControls && GUI.KeyboardDispatcher.Subscriber == null) { - steeringAdjustSpeed = character == null ? 0.2f : MathHelper.Lerp(0.2f, 1.0f, character.GetSkillLevel("helm") / 100.0f); + steeringAdjustSpeed = character == null ? DefaultSteeringAdjustSpeed : MathHelper.Lerp(0.2f, 1.0f, character.GetSkillLevel("helm") / 100.0f); Vector2 input = Vector2.Zero; if (PlayerInput.KeyDown(InputType.Left)) { input -= Vector2.UnitX; } if (PlayerInput.KeyDown(InputType.Right)) { input += Vector2.UnitX; } @@ -914,7 +931,7 @@ namespace Barotrauma.Items.Components if (dockingButtonClicked) { - item.SendSignal(0, "1", "toggle_docking", sender: null); + item.SendSignal("1", "toggle_docking"); } if (autoPilot) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs index 2fd5955c2..4b6c91176 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs @@ -98,8 +98,11 @@ namespace Barotrauma.Items.Components public override void UpdateHUD(Character character, float deltaTime, Camera cam) { - float chargeRatio = charge / capacity; - chargeIndicator.Color = ToolBox.GradientLerp(chargeRatio, Color.Red, Color.Orange, Color.Green); + if (chargeIndicator != null) + { + float chargeRatio = charge / capacity; + chargeIndicator.Color = ToolBox.GradientLerp(chargeRatio, Color.Red, Color.Orange, Color.Green); + } } public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs index 18d7f5650..963b5b9c7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs @@ -1,5 +1,4 @@ -using Barotrauma.Networking; -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; @@ -28,15 +27,7 @@ namespace Barotrauma.Items.Components int x = panelRect.X, y = panelRect.Y; int width = panelRect.Width, height = panelRect.Height; - Vector2 scale = new Vector2(GUI.Scale); - if (panel.GuiFrame.RectTransform.MaxSize.X < int.MaxValue) - { - scale.X = panel.GuiFrame.RectTransform.MaxSize.X / panel.GuiFrame.Rect.Width; - } - if (panel.GuiFrame.RectTransform.MaxSize.Y < int.MaxValue) - { - scale.Y = panel.GuiFrame.RectTransform.MaxSize.Y / panel.GuiFrame.Rect.Height; - } + Vector2 scale = GetScale(panel.GuiFrame.RectTransform.MaxSize, panel.GuiFrame.Rect.Size); bool mouseInRect = panelRect.Contains(PlayerInput.MousePosition); @@ -66,15 +57,15 @@ namespace Barotrauma.Items.Components //two passes: first the connector, then the wires to get the wires to render in front for (int i = 0; i < 2; i++) { - Vector2 rightPos = new Vector2(x + width - 80 * scale.X, y + 60 * scale.Y); - Vector2 leftPos = new Vector2(x + 80 * scale.X, y + 60 * scale.Y); + Vector2 rightPos = GetRightPos(x, y, width, scale); + Vector2 leftPos = GetLeftPos(x, y, scale); Vector2 rightWirePos = new Vector2(x + width - 5 * scale.X, y + 30 * scale.Y); Vector2 leftWirePos = new Vector2(x + 5 * scale.X, y + 30 * scale.Y); int wireInterval = (height - (int)(20 * scale.Y)) / Math.Max(totalWireCount, 1); - int connectorIntervalLeft = (height - (int)(100 * scale.Y)) / Math.Max(panel.Connections.Count(c => c.IsOutput), 1); - int connectorIntervalRight = (height - (int)(100 * scale.Y)) / Math.Max(panel.Connections.Count(c => !c.IsOutput), 1); + int connectorIntervalLeft = GetConnectorIntervalLeft(height, scale, panel); + int connectorIntervalRight = GetConnectorIntervalRight(height, scale, panel); foreach (Connection c in panel.Connections) { @@ -101,15 +92,12 @@ namespace Barotrauma.Items.Components { if (i == 0) { - c.DrawConnection(spriteBatch, panel, rightPos, - new Vector2(rightPos.X - GUI.SmallFont.MeasureString(c.DisplayName.ToUpper()).X - 25 * panel.Scale, rightPos.Y + 5 * panel.Scale), - scale); + c.DrawConnection(spriteBatch, panel, rightPos, GetOutputLabelPosition(rightPos, panel, c), scale); } else { c.DrawWires(spriteBatch, panel, rightPos, rightWirePos, mouseInRect, equippedWire, wireInterval); } - rightPos.Y += connectorIntervalLeft; rightWirePos.Y += c.Wires.Count(w => w != null) * wireInterval; } @@ -117,15 +105,12 @@ namespace Barotrauma.Items.Components { if (i == 0) { - c.DrawConnection(spriteBatch, panel, leftPos, - new Vector2(leftPos.X + 25 * panel.Scale, leftPos.Y - 5 * panel.Scale - GUI.SmallFont.MeasureString(c.DisplayName.ToUpper()).Y), - scale); + c.DrawConnection(spriteBatch, panel, leftPos, GetInputLabelPosition(leftPos, panel, c), scale); } else { c.DrawWires(spriteBatch, panel, leftPos, leftWirePos, mouseInRect, equippedWire, wireInterval); } - leftPos.Y += connectorIntervalRight; leftWirePos.Y += c.Wires.Count(w => w != null) * wireInterval; } @@ -215,14 +200,11 @@ namespace Barotrauma.Items.Components private void DrawConnection(SpriteBatch spriteBatch, ConnectionPanel panel, Vector2 position, Vector2 labelPos, Vector2 scale) { string text = DisplayName.ToUpper(); - Vector2 textSize = GUI.SmallFont.MeasureString(text); //nasty - var labelSprite = GUI.Style.GetComponentStyle("ConnectionPanelLabel")?.Sprites.Values.First().First(); - if (labelSprite != null) + if (GUI.Style.GetComponentStyle("ConnectionPanelLabel")?.Sprites.Values.First().First() is UISprite labelSprite) { - Rectangle labelArea = new Rectangle(labelPos.ToPoint(), textSize.ToPoint()); - labelArea.Inflate(10 * scale.X, 3 * scale.Y); + Rectangle labelArea = GetLabelArea(labelPos, text, scale); labelSprite.Draw(spriteBatch, labelArea, IsPower ? GUI.Style.Red : Color.SteelBlue); } @@ -256,7 +238,8 @@ namespace Barotrauma.Items.Components if (!PlayerInput.PrimaryMouseButtonHeld()) { - if (GameMain.NetworkMember != null || panel.CheckCharacterSuccess(Character.Controlled)) + if ((GameMain.NetworkMember != null || panel.CheckCharacterSuccess(Character.Controlled)) && + Wires.Count(w => w != null) < MaxPlayerConnectableWires) { //find an empty cell for the new connection int index = FindEmptyIndex(); @@ -390,5 +373,115 @@ namespace Barotrauma.Items.Components } } } + + public static bool CheckConnectionLabelOverlap(ConnectionPanel panel, out Point newRectSize) + { + Rectangle panelRect = panel.GuiFrame.Rect; + int x = panelRect.X, y = panelRect.Y; + Vector2 scale = GetScale(panel.GuiFrame.RectTransform.MaxSize, panel.GuiFrame.Rect.Size); + Vector2 rightPos = GetRightPos(x, y, panelRect.Width, scale); + Vector2 leftPos = GetLeftPos(x, y, scale); + int connectorIntervalLeft = GetConnectorIntervalLeft(panelRect.Height, scale, panel); + int connectorIntervalRight = GetConnectorIntervalRight(panelRect.Height, scale, panel); + newRectSize = panelRect.Size; + var labelAreas = new List(); + for (int i = 0; i < 100; i++) + { + labelAreas.Clear(); + foreach (var c in panel.Connections) + { + if (c.IsOutput) + { + var labelArea = GetLabelArea(GetOutputLabelPosition(rightPos, panel, c), c.DisplayName.ToUpper(), scale); + labelAreas.Add(labelArea); + rightPos.Y += connectorIntervalLeft; + } + else + { + var labelArea = GetLabelArea(GetInputLabelPosition(leftPos, panel, c), c.DisplayName.ToUpper(), scale); + labelAreas.Add(labelArea); + leftPos.Y += connectorIntervalRight; + } + } + bool foundOverlap = false; + for (int j = 0; j < labelAreas.Count; j++) + { + for (int k = 0; k < labelAreas.Count; k++) + { + if (k == j) { continue; } + if (!labelAreas[j].Intersects(labelAreas[k])) { continue; } + newRectSize += new Point(10); + Point maxSize = new Point( + Math.Max(panel.GuiFrame.RectTransform.MaxSize.X, newRectSize.X), + Math.Max(panel.GuiFrame.RectTransform.MaxSize.Y, newRectSize.Y)); + scale = GetScale(maxSize, newRectSize); + rightPos = GetRightPos(x, y, newRectSize.X, scale); + leftPos = GetLeftPos(x, y, scale); + connectorIntervalLeft = GetConnectorIntervalLeft(newRectSize.Y, scale, panel); + connectorIntervalRight = GetConnectorIntervalRight(newRectSize.Y, scale, panel); + foundOverlap = true; + break; + } + } + if (!foundOverlap) { break; } + } + return newRectSize.X != panel.GuiFrame.Rect.Width || newRectSize.Y > panel.GuiFrame.Rect.Height; + } + + private static Vector2 GetScale(Point maxSize, Point size) + { + Vector2 scale = new Vector2(GUI.Scale); + if (maxSize.X < int.MaxValue) + { + scale.X = maxSize.X / size.X; + } + if (maxSize.Y < int.MaxValue) + { + scale.Y = maxSize.Y / size.Y; + } + return scale; + } + + private static Vector2 GetInputLabelPosition(Vector2 connectorPosition, ConnectionPanel panel, Connection connection) + { + return new Vector2( + connectorPosition.X + 25 * panel.Scale, + connectorPosition.Y - 5 * panel.Scale - GUI.SmallFont.MeasureString(connection.DisplayName.ToUpper()).Y); + } + + private static Vector2 GetOutputLabelPosition(Vector2 connectorPosition, ConnectionPanel panel, Connection connection) + { + return new Vector2( + connectorPosition.X - 25 * panel.Scale - GUI.SmallFont.MeasureString(connection.DisplayName.ToUpper()).X, + connectorPosition.Y + 5 * panel.Scale); + } + + private static Rectangle GetLabelArea(Vector2 labelPos, string text, Vector2 scale) + { + Vector2 textSize = GUI.SmallFont.MeasureString(text); + Rectangle labelArea = new Rectangle(labelPos.ToPoint(), textSize.ToPoint()); + labelArea.Inflate(10 * scale.X, 3 * scale.Y); + return labelArea; + } + + private static Vector2 GetLeftPos(int x, int y, Vector2 scale) + { + return new Vector2(x + 80 * scale.X, y + 60 * scale.Y); + } + + private static Vector2 GetRightPos(int x, int y, int width, Vector2 scale) + { + return new Vector2(x + width - 80 * scale.X, y + 60 * scale.Y); + } + + private static int GetConnectorIntervalLeft(int height, Vector2 scale, ConnectionPanel panel) + { + return (height - (int)(100 * scale.Y)) / Math.Max(panel.Connections.Count(c => c.IsOutput), 1); + } + + private static int GetConnectorIntervalRight(int height, Vector2 scale, ConnectionPanel panel) + { + return (height - (int)(100 * scale.Y)) / Math.Max(panel.Connections.Count(c => !c.IsOutput), 1); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs index 2886e0e74..1162e4bff 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs @@ -24,9 +24,15 @@ namespace Barotrauma.Items.Components get { return GuiFrame.Rect.Width / 400.0f; } } - partial void InitProjSpecific(XElement element) + private Point originalMaxSize; + private Vector2 originalRelativeSize; + + partial void InitProjSpecific() { if (GuiFrame == null) { return; } + originalMaxSize = GuiFrame.RectTransform.MaxSize; + originalRelativeSize = GuiFrame.RectTransform.RelativeSize; + CheckForLabelOverlap(); new GUICustomComponent(new RectTransform(Vector2.One, GuiFrame.RectTransform), DrawConnections, null) { UserData = this @@ -40,7 +46,7 @@ namespace Barotrauma.Items.Components partial void UpdateProjSpecific(float deltaTime) { - foreach (Wire wire in DisconnectedWires) + foreach (var _ in DisconnectedWires) { if (Rand.Range(0.0f, 500.0f) < 1.0f) { @@ -112,6 +118,32 @@ namespace Barotrauma.Items.Components } } + protected override void OnResolutionChanged() + { + base.OnResolutionChanged(); + if (GuiFrame == null) { return; } + CheckForLabelOverlap(); + } + + private void CheckForLabelOverlap() + { + GuiFrame.RectTransform.MaxSize = originalMaxSize; + GuiFrame.RectTransform.Resize(originalRelativeSize); + if (Connection.CheckConnectionLabelOverlap(this, out Point newRectSize)) + { + int xCenter = (int)(GameMain.GraphicsWidth / 2.0f); + int maxNewWidth = 2 * Math.Min(xCenter - HUDLayoutSettings.CrewArea.Right, xCenter - HUDLayoutSettings.ChatBoxArea.Right); + int yCenter = (int)(GameMain.GraphicsHeight / 2.0f); + int maxNewHeight = 2 * Math.Min(yCenter - HUDLayoutSettings.MessageAreaTop.Bottom, HUDLayoutSettings.InventoryTopY - yCenter); + // Make sure we don't expand the panel interface too much + newRectSize = new Point(Math.Min(newRectSize.X, maxNewWidth), Math.Min(newRectSize.Y, maxNewHeight)); + GuiFrame.RectTransform.MaxSize = new Point( + Math.Max(GuiFrame.RectTransform.MaxSize.X, newRectSize.X), + Math.Max(GuiFrame.RectTransform.MaxSize.Y, newRectSize.Y)); + GuiFrame.RectTransform.Resize(newRectSize); + } + } + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { if (GameMain.Client.MidRoundSyncing) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs index da095f186..365680a44 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs @@ -50,7 +50,8 @@ namespace Barotrauma.Items.Components var textBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), layoutGroup.RectTransform), ciElement.Signal, style: "GUITextBoxNoIcon") { OverflowClip = true, - UserData = ciElement + UserData = ciElement, + MaxTextLength = ciElement.MaxTextLength }; //reset size restrictions set by the Style to make sure the elements can fit the interface textBox.RectTransform.MinSize = textBox.Frame.RectTransform.MinSize = new Point(0, 0); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs index d16f75c7f..353e4550b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs @@ -63,21 +63,23 @@ namespace Barotrauma.Items.Components } OutputValue = input; - item.SendSignal(0, input, "signal_out", null); ShowOnDisplay(input); + item.SendSignal(input, "signal_out"); } - partial void ShowOnDisplay(string input) + partial void ShowOnDisplay(string input, bool addToHistory = true) { - messageHistory.Add(input); - while (messageHistory.Count > MaxMessages) + if (addToHistory) { - messageHistory.RemoveAt(0); - } - - while (historyBox.Content.CountChildren > MaxMessages) - { - historyBox.RemoveChild(historyBox.Content.Children.First()); + messageHistory.Add(input); + while (messageHistory.Count > MaxMessages) + { + messageHistory.RemoveAt(0); + } + while (historyBox.Content.CountChildren > MaxMessages) + { + historyBox.RemoveChild(historyBox.Content.Children.First()); + } } GUITextBlock newBlock = new GUITextBlock( diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs index 353e689fe..ce86894a0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs @@ -87,7 +87,7 @@ namespace Barotrauma.Items.Components SpriteEffects.None, depth); } - } + } private static Sprite defaultWireSprite; private Sprite overrideSprite; private Sprite wireSprite; @@ -156,7 +156,8 @@ namespace Barotrauma.Items.Components drawOffset = sub.DrawPosition + sub.HiddenSubPosition; } - float depth = item.IsSelected ? 0.0f : SubEditorScreen.IsWiringMode() ? 0.02f : wireSprite.Depth + (item.ID % 100) * 0.000001f;// item.GetDrawDepth(wireSprite.Depth, wireSprite); + float baseDepth = UseSpriteDepth ? item.SpriteDepth : wireSprite.Depth; + float depth = item.IsSelected ? 0.0f : SubEditorScreen.IsWiringMode() ? 0.02f : baseDepth + (item.ID % 100) * 0.000001f;// item.GetDrawDepth(wireSprite.Depth, wireSprite); if (item.IsHighlighted) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs index 0c09668cf..67772a0a2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs @@ -225,7 +225,7 @@ namespace Barotrauma.Items.Components foreach (AfflictionPrefab affliction in combinedAfflictionStrengths.Keys) { - texts.Add(TextManager.AddPunctuation(':', affliction.Name, ((int)combinedAfflictionStrengths[affliction]).ToString() + " %")); + texts.Add(TextManager.AddPunctuation(':', affliction.Name, Math.Max(((int)combinedAfflictionStrengths[affliction]), 1).ToString() + " %")); textColors.Add(Color.Lerp(GUI.Style.Orange, GUI.Style.Red, combinedAfflictionStrengths[affliction] / affliction.MaxStrength)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index 991bb560a..8991ef88a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -130,12 +130,15 @@ namespace Barotrauma.Items.Components } } - powerIndicator = new GUIProgressBar(new RectTransform(new Vector2(0.18f, 0.03f), GUI.Canvas, Anchor.TopCenter) + powerIndicator = new GUIProgressBar(new RectTransform(new Vector2(0.18f, 0.03f), GUI.Canvas, Anchor.BottomCenter) { - MinSize = new Point(100,20), + MinSize = new Point(100, 20), RelativeOffset = new Vector2(0.0f, 0.01f) - }, - barSize: 0.0f, style: "DeviceProgressBar"); + }, + barSize: 0.0f, style: "DeviceProgressBar") + { + CanBeFocused = false + }; } public override void Move(Vector2 amount) @@ -497,9 +500,15 @@ namespace Barotrauma.Items.Components foreach (MapEntity e in item.linkedTo) { if (!(e is Item linkedItem)) { continue; } - availableAmmo.AddRange(linkedItem.ContainedItems); - } - + var itemContainer = linkedItem.GetComponent(); + if (itemContainer == null) { continue; } + availableAmmo.AddRange(itemContainer.Inventory.AllItems); + for (int i = 0; i < itemContainer.Inventory.Capacity - itemContainer.Inventory.AllItems.Count(); i++) + { + availableAmmo.Add(null); + } + } + float chargeRate = powerConsumption <= 0.0f ? 1.0f : @@ -531,15 +540,16 @@ namespace Barotrauma.Items.Components if (ShowProjectileIndicator) { Point slotSize = (Inventory.SlotSpriteSmall.size * Inventory.UIScale).ToPoint(); - int spacing = 5; + Point spacing = new Point(GUI.IntScale(5), GUI.IntScale(20)); int slotsPerRow = Math.Min(availableAmmo.Count, 6); - int totalWidth = slotSize.X * slotsPerRow + spacing * (slotsPerRow - 1); - Point invSlotPos = new Point(GameMain.GraphicsWidth / 2 - totalWidth / 2, (int)(60 * GUI.Scale)); + int totalWidth = slotSize.X * slotsPerRow + spacing.X * (slotsPerRow - 1); + int rows = (int)Math.Ceiling(availableAmmo.Count / (float)slotsPerRow); + Point invSlotPos = new Point(GameMain.GraphicsWidth / 2 - totalWidth / 2, powerIndicator.Rect.Y - (slotSize.Y + spacing.Y) * rows); for (int i = 0; i < availableAmmo.Count; i++) { // TODO: Optimize? Creates multiple new objects per frame? Inventory.DrawSlot(spriteBatch, null, - new VisualSlot(new Rectangle(invSlotPos + new Point((i % slotsPerRow) * (slotSize.X + spacing), (int)Math.Floor(i / (float)slotsPerRow) * (slotSize.Y + spacing)), slotSize)), + new VisualSlot(new Rectangle(invSlotPos + new Point((i % slotsPerRow) * (slotSize.X + spacing.X), (int)Math.Floor(i / (float)slotsPerRow) * (slotSize.Y + spacing.Y)), slotSize)), availableAmmo[i], -1, true); } if (flashNoAmmo) @@ -578,7 +588,7 @@ namespace Barotrauma.Items.Components //ID ushort.MaxValue = launched without a projectile if (projectileID == ushort.MaxValue) { - Launch(null); + Launch(null, user); } else { @@ -587,7 +597,7 @@ namespace Barotrauma.Items.Components DebugConsole.ThrowError("Failed to launch a projectile - item with the ID \"" + projectileID + " not found"); return; } - Launch(projectile, launchRotation: newTargetRotation); + Launch(projectile, user, launchRotation: newTargetRotation); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs index 02b174c32..acec84bc8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs @@ -165,7 +165,14 @@ namespace Barotrauma.Items.Components if (isLocked) { - Lock(isNetworkMessage: true, forcePosition: true); + if (DockingTarget.joint != null) + { + DockingTarget.Lock(isNetworkMessage: true); + } + else + { + Lock(isNetworkMessage: true); + } } } else diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 883248dc0..e370c2eee 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -215,8 +215,10 @@ namespace Barotrauma public Inventory Inventory; public readonly Item Item; public readonly bool IsSubSlot; - public string Tooltip; - public List TooltipRichTextData; + public string Tooltip { get; private set; } + public List TooltipRichTextData { get; private set;} + + public int tooltipDisplayedCondition; public SlotReference(Inventory parentInventory, VisualSlot slot, int slotIndex, bool isSubSlot, Inventory subInventory = null) { @@ -227,16 +229,29 @@ namespace Barotrauma IsSubSlot = isSubSlot; Item = ParentInventory.GetItemAt(slotIndex); - int stackCount = 1; - if (parentInventory != null && Item != null) - { - stackCount = parentInventory.GetItemsAt(slotIndex).Count(); - } - - TooltipRichTextData = RichTextData.GetRichTextData(GetTooltip(Item, stackCount), out Tooltip); + RefreshTooltip(); } - private string GetTooltip(Item item, int stackCount) + public bool TooltipNeedsRefresh() + { + if (Item == null) { return false; } + return (int)Item.ConditionPercentage != tooltipDisplayedCondition; + } + + public void RefreshTooltip() + { + if (Item == null) { return; } + IEnumerable itemsInSlot = null; + if (ParentInventory != null && Item != null) + { + itemsInSlot = ParentInventory.GetItemsAt(SlotIndex); + } + TooltipRichTextData = RichTextData.GetRichTextData(GetTooltip(Item, itemsInSlot), out string newTooltip); + Tooltip = newTooltip; + tooltipDisplayedCondition = (int)Item.ConditionPercentage; + } + + private string GetTooltip(Item item, IEnumerable itemsInSlot) { if (item == null) { return null; } @@ -288,9 +303,13 @@ namespace Barotrauma } } - string colorStr = XMLExtensions.ColorToString(item.SpawnedInOutpost ? GUI.Style.Red : Color.White); + string colorStr = XMLExtensions.ColorToString(!item.AllowStealing ? GUI.Style.Red : Color.White); toolTip = $"‖color:{colorStr}‖{item.Name}‖color:end‖"; + if (itemsInSlot.All(it => it.NonInteractable || it.NonPlayerTeamInteractable)) + { + toolTip += " " + TextManager.Get("connectionlocked"); + } if (!item.IsFullCondition && !item.Prefab.HideConditionBar) { string conditionColorStr = XMLExtensions.ColorToString(ToolBox.GradientLerp(item.Condition / item.MaxCondition, GUI.Style.ColorInventoryEmpty, GUI.Style.ColorInventoryHalf, GUI.Style.ColorInventoryFull)); @@ -298,7 +317,7 @@ namespace Barotrauma } if (!string.IsNullOrEmpty(description)) { toolTip += '\n' + description; } } - if (stackCount > 2) + if (itemsInSlot.Count() > 1) { string colorStr = XMLExtensions.ColorToString(GUI.Style.Blue); toolTip += $"\n‖color:{colorStr}‖[{GameMain.Config.KeyBindText(InputType.TakeOneFromInventorySlot)}] {TextManager.Get("inputtype.takeonefrominventoryslot")}‖color:end‖"; @@ -315,10 +334,10 @@ namespace Barotrauma { get { - return DraggingItems.Any() && - Character.Controlled != null && + return Character.Controlled != null && Character.Controlled.SelectedConstruction == null && - CharacterHealth.OpenHealthWindow == null; + CharacterHealth.OpenHealthWindow == null && + DraggingItems.Any(); } } @@ -565,39 +584,41 @@ namespace Barotrauma if (!DraggingItems.Any()) { - if (PlayerInput.PrimaryMouseButtonDown() && slots[slotIndex].Any()) + var interactableItems = Screen.Selected == GameMain.GameScreen ? slots[slotIndex].Items.Where(it => !it.NonInteractable && !it.NonPlayerTeamInteractable) : slots[slotIndex].Items; + if (PlayerInput.PrimaryMouseButtonDown() && interactableItems.Any()) { if (PlayerInput.KeyDown(InputType.TakeHalfFromInventorySlot)) { - DraggingItems.AddRange(slots[slotIndex].Items.Skip(slots[slotIndex].ItemCount / 2)); + DraggingItems.AddRange(interactableItems.Skip(interactableItems.Count() / 2)); } else if (PlayerInput.KeyDown(InputType.TakeOneFromInventorySlot)) { - DraggingItems.Add(slots[slotIndex].First()); + DraggingItems.Add(interactableItems.First()); } else { - DraggingItems.AddRange(slots[slotIndex].Items); + DraggingItems.AddRange(interactableItems); } DraggingSlot = slot; } } else if (PlayerInput.PrimaryMouseButtonReleased()) { - if (PlayerInput.DoubleClicked() && slots[slotIndex].Any()) + var interactableItems = Screen.Selected == GameMain.GameScreen ? slots[slotIndex].Items.Where(it => !it.NonInteractable && !it.NonPlayerTeamInteractable) : slots[slotIndex].Items; + if (PlayerInput.DoubleClicked() && interactableItems.Any()) { doubleClickedItems.Clear(); if (PlayerInput.KeyDown(InputType.TakeHalfFromInventorySlot)) { - doubleClickedItems.AddRange(slots[slotIndex].Items.Skip(slots[slotIndex].ItemCount / 2)); + doubleClickedItems.AddRange(interactableItems.Skip(interactableItems.Count() / 2)); } else if (PlayerInput.KeyDown(InputType.TakeOneFromInventorySlot)) { - doubleClickedItems.Add(slots[slotIndex].First()); + doubleClickedItems.Add(interactableItems.First()); } else { - doubleClickedItems.AddRange(slots[slotIndex].Items); + doubleClickedItems.AddRange(interactableItems); } } } @@ -1241,9 +1262,11 @@ namespace Barotrauma if (selectedSlot.ParentInventory?.Owner != Character.Controlled && selectedSlot.ParentInventory?.Owner != Character.Controlled.SelectedCharacter && selectedSlot.ParentInventory?.Owner != Character.Controlled.SelectedConstruction && + !(Character.Controlled.SelectedConstruction?.linkedTo.Contains(selectedSlot.ParentInventory?.Owner) ?? false) && rootOwner != Character.Controlled && rootOwner != Character.Controlled.SelectedCharacter && - rootOwner != Character.Controlled.SelectedConstruction) + rootOwner != Character.Controlled.SelectedConstruction && + !(Character.Controlled.SelectedConstruction?.linkedTo.Contains(rootOwner) ?? false)) { selectedSlot = null; } @@ -1362,6 +1385,10 @@ namespace Barotrauma { Rectangle slotRect = selectedSlot.Slot.Rect; slotRect.Location += selectedSlot.Slot.DrawOffset.ToPoint(); + if (selectedSlot.TooltipNeedsRefresh()) + { + selectedSlot.RefreshTooltip(); + } DrawToolTip(spriteBatch, selectedSlot.Tooltip, slotRect, selectedSlot.TooltipRichTextData); } } @@ -1533,7 +1560,7 @@ namespace Barotrauma } Color spriteColor = sprite == item.Sprite ? item.GetSpriteColor() : item.GetInventoryIconColor(); - if (inventory != null && inventory.Locked) { spriteColor *= 0.5f; } + if (inventory != null && (inventory.Locked || inventory.slots[slotIndex].Items.All(it => it.NonInteractable || it.NonPlayerTeamInteractable))) { spriteColor *= 0.5f; } if (CharacterHealth.OpenHealthWindow != null && !item.UseInHealthInterface) { spriteColor = Color.Lerp(spriteColor, Color.TransparentBlack, 0.5f); @@ -1544,7 +1571,7 @@ namespace Barotrauma } sprite.Draw(spriteBatch, itemPos, spriteColor, rotation, scale); - if (item.SpawnedInOutpost && CharacterInventory.LimbSlotIcons.ContainsKey(InvSlotType.LeftHand)) + if (!item.AllowStealing && CharacterInventory.LimbSlotIcons.ContainsKey(InvSlotType.LeftHand)) { var stealIcon = CharacterInventory.LimbSlotIcons[InvSlotType.LeftHand]; Vector2 iconSize = new Vector2(25 * GUI.Scale); @@ -1559,7 +1586,7 @@ namespace Barotrauma { maxStackSize = Math.Min(maxStackSize, item.Container.GetComponent()?.MaxStackSize ?? maxStackSize); } - if (maxStackSize > 1) + if (maxStackSize > 1 && inventory != null) { int itemCount = slot.MouseOn() ? inventory.slots[slotIndex].ItemCount : inventory.slots[slotIndex].Items.Where(it => !DraggingItems.Contains(it)).Count(); if (item.IsFullCondition || MathUtils.NearlyEqual(item.Condition, 0.0f) || itemCount > 1) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index ac63d80d2..9735fabb0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -87,7 +87,7 @@ namespace Barotrauma { get { - if (!GameMain.SubEditorScreen.ShowThalamus && prefab.Category.HasFlag(MapEntityCategory.Thalamus)) + if (GameMain.SubEditorScreen.IsSubcategoryHidden(prefab.Subcategory)) { return false; } @@ -618,6 +618,7 @@ namespace Barotrauma editingHUD = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.25f), GUI.Canvas, Anchor.CenterRight) { MinSize = new Point(400, 0) }) { UserData = this }; GUIListBox listBox = new GUIListBox(new RectTransform(new Vector2(0.95f, 0.8f), editingHUD.RectTransform, Anchor.Center), style: null) { + CanTakeKeyBoardFocus = false, Spacing = (int)(25 * GUI.Scale) }; @@ -1463,6 +1464,7 @@ namespace Barotrauma byte bodyType = msg.ReadByte(); bool spawnedInOutpost = msg.ReadBoolean(); + bool allowStealing = msg.ReadBoolean(); byte teamID = msg.ReadByte(); bool tagsChanged = msg.ReadBoolean(); string tags = ""; @@ -1534,7 +1536,8 @@ namespace Barotrauma var item = new Item(itemPrefab, pos, sub, id: itemId) { - SpawnedInOutpost = spawnedInOutpost + SpawnedInOutpost = spawnedInOutpost, + AllowStealing = allowStealing }; if (item.body != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs index 16da4be6a..e899eb482 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs @@ -22,47 +22,47 @@ namespace Barotrauma var underwaterExplosion = GameMain.ParticleManager.CreateParticle("underwaterexplosion", worldPosition, Vector2.Zero, 0.0f, hull); if (underwaterExplosion != null) { - underwaterExplosion.Size *= MathHelper.Clamp(attack.Range / 150.0f, 0.5f, 10.0f); + underwaterExplosion.Size *= MathHelper.Clamp(Attack.Range / 150.0f, 0.5f, 10.0f); underwaterExplosion.StartDelay = 0.0f; } } - for (int i = 0; i < attack.Range * 0.1f; i++) + for (int i = 0; i < Attack.Range * 0.1f; i++) { if (!underwater) { float particleSpeed = Rand.Range(0.0f, 1.0f); - particleSpeed = particleSpeed * particleSpeed * attack.Range; + particleSpeed = particleSpeed * particleSpeed * Attack.Range; if (flames) { - float particleScale = MathHelper.Clamp(attack.Range * 0.0025f, 0.5f, 2.0f); + float particleScale = MathHelper.Clamp(Attack.Range * 0.0025f, 0.5f, 2.0f); var flameParticle = GameMain.ParticleManager.CreateParticle("explosionfire", - ClampParticlePos(worldPosition + Rand.Vector((float)System.Math.Sqrt(Rand.Range(0.0f, attack.Range))), hull), + ClampParticlePos(worldPosition + Rand.Vector((float)System.Math.Sqrt(Rand.Range(0.0f, Attack.Range))), hull), Rand.Vector(Rand.Range(0.0f, particleSpeed)), 0.0f, hull); if (flameParticle != null) flameParticle.Size *= particleScale; } if (smoke) { GameMain.ParticleManager.CreateParticle(Rand.Range(0.0f, 1.0f) < 0.5f ? "explosionsmoke" : "smoke", - ClampParticlePos(worldPosition + Rand.Vector((float)System.Math.Sqrt(Rand.Range(0.0f, attack.Range))), hull), + ClampParticlePos(worldPosition + Rand.Vector((float)System.Math.Sqrt(Rand.Range(0.0f, Attack.Range))), hull), Rand.Vector(Rand.Range(0.0f, particleSpeed)), 0.0f, hull); } } else if (underwaterBubble) { - Vector2 bubblePos = Rand.Vector(Rand.Range(0.0f, attack.Range * 0.5f)); + Vector2 bubblePos = Rand.Vector(Rand.Range(0.0f, Attack.Range * 0.5f)); GameMain.ParticleManager.CreateParticle("risingbubbles", worldPosition + bubblePos, Vector2.Zero, 0.0f, hull); - if (i < attack.Range * 0.02f) + if (i < Attack.Range * 0.02f) { var underwaterExplosion = GameMain.ParticleManager.CreateParticle("underwaterexplosion", worldPosition + bubblePos, Vector2.Zero, 0.0f, hull); if (underwaterExplosion != null) { - underwaterExplosion.Size *= MathHelper.Clamp(attack.Range / 300.0f, 0.5f, 2.0f) * Rand.Range(0.8f, 1.2f); + underwaterExplosion.Size *= MathHelper.Clamp(Attack.Range / 300.0f, 0.5f, 2.0f) * Rand.Range(0.8f, 1.2f); } } @@ -77,7 +77,7 @@ namespace Barotrauma if (flash) { - float displayRange = flashRange.HasValue ? flashRange.Value : attack.Range; + float displayRange = flashRange.HasValue ? flashRange.Value : Attack.Range; if (displayRange < 0.1f) { return; } var light = new LightSource(worldPosition, displayRange, Color.LightYellow, null); CoroutineManager.StartCoroutine(DimLight(light)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index 82627664c..2d8875d30 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -90,7 +90,10 @@ namespace Barotrauma private GUIComponent CreateEditingHUD(bool inGame = false) { editingHUD = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.25f), GUI.Canvas, Anchor.CenterRight) { MinSize = new Point(400, 0) }) { UserData = this }; - GUIListBox listBox = new GUIListBox(new RectTransform(new Vector2(0.95f, 0.8f), editingHUD.RectTransform, Anchor.Center), style: null); + GUIListBox listBox = new GUIListBox(new RectTransform(new Vector2(0.95f, 0.8f), editingHUD.RectTransform, Anchor.Center), style: null) + { + CanTakeKeyBoardFocus = false + }; new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true, titleFont: GUI.LargeFont); PositionEditingHUD(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs index 8537fd04d..ebf26774d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs @@ -302,7 +302,7 @@ namespace Barotrauma { GUI.DrawLine(spriteBatch, new Vector2(edge.Point1.X + cell.Translation.X, -(edge.Point1.Y + cell.Translation.Y)), new Vector2(edge.Point2.X + cell.Translation.X, -(edge.Point2.Y + cell.Translation.Y)), edge.NextToCave ? Color.Red : (cell.Body == null ? Color.Cyan * 0.5f : (edge.IsSolid ? Color.White : Color.Gray)), - width: edge.NextToCave ? 8 :1); + width: edge.NextToCave ? 8 : 1); } foreach (Vector2 point in cell.BodyVertices) @@ -322,6 +322,11 @@ namespace Barotrauma } }*/ + foreach (var abyssIsland in level.AbyssIslands) + { + GUI.DrawRectangle(spriteBatch, new Vector2(abyssIsland.Area.X, -abyssIsland.Area.Y - abyssIsland.Area.Height), abyssIsland.Area.Size.ToVector2(), Color.Cyan, thickness: 5); + } + foreach (var ruin in level.Ruins) { ruin.DebugDraw(spriteBatch); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index 6565af460..150a4bfc4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -48,6 +48,7 @@ namespace Barotrauma.Lights private readonly List lights; public bool LosEnabled = true; + public float LosAlpha = 1f; public LosMode LosMode = LosMode.Transparent; public bool LightingEnabled = true; @@ -497,7 +498,6 @@ namespace Barotrauma.Lights graphics.SetRenderTarget(LosTexture); - spriteBatch.Begin(SpriteSortMode.Deferred, transformMatrix: cam.Transform * Matrix.CreateScale(new Vector3(GameMain.Config.LightMapScale, GameMain.Config.LightMapScale, 1.0f))); if (ObstructVision) { graphics.Clear(Color.Black); @@ -507,16 +507,17 @@ namespace Barotrauma.Lights float rotation = MathUtils.VectorToAngle(losOffset); Vector2 scale = new Vector2( - MathHelper.Clamp(losOffset.Length() / 256.0f, 2.0f, 5.0f), 2.0f); + MathHelper.Clamp(losOffset.Length() / 256.0f, 4.0f, 5.0f), 3.0f); + spriteBatch.Begin(SpriteSortMode.Deferred, transformMatrix: cam.Transform * Matrix.CreateScale(new Vector3(GameMain.Config.LightMapScale, GameMain.Config.LightMapScale, 1.0f))); spriteBatch.Draw(visionCircle, new Vector2(ViewTarget.WorldPosition.X, -ViewTarget.WorldPosition.Y), null, Color.White, rotation, new Vector2(visionCircle.Width * 0.2f, visionCircle.Height / 2), scale, SpriteEffects.None, 0.0f); + spriteBatch.End(); } else { graphics.Clear(Color.White); } - spriteBatch.End(); //-------------------------------------- diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index b7d9dfe51..28a0f056c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -2,8 +2,8 @@ using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; +using Microsoft.Xna.Framework.Input; namespace Barotrauma { @@ -63,7 +63,19 @@ namespace Barotrauma private Sprite[,] mapTiles; private bool[,] tileDiscovered; - private Pair connectionTooltip; + private float connectionHighlightState; + + private (Rectangle targetArea, string tip)? tooltip; + private string sanitizedTooltip; + private List tooltipRichTextData; + private string prevTooltip; + + private (SubmarineInfo pendingSub, float realWorldCrushDepth) pendingSubInfo; + + /*private (Rectangle targetArea, string tip)? connectionTooltip; + private string sanitizedConnectionTooltip; + private List connectionTooltipRichTextData; + private string prevConnectionTooltip;*/ #if DEBUG private GUIComponent editor; @@ -93,13 +105,6 @@ namespace Barotrauma }; } #endif - public Location CurrentDisplayLocation - { - get - { - return GameMain.GameSession.Campaign.CurrentDisplayLocation; - } - } partial void InitProjectSpecific() { @@ -120,7 +125,6 @@ namespace Barotrauma DrawOffset = -CurrentLocation.MapPosition; } - Vector2 tileSize = generationParams.MapTiles.Values.First().First().size * generationParams.MapTileScale; int tilesX = (int)Math.Ceiling(Width / tileSize.X); int tilesY = (int)Math.Ceiling(Height / tileSize.Y); @@ -227,8 +231,8 @@ namespace Barotrauma if (change.Messages.Any()) { string msg = change.Messages[Rand.Range(0, change.Messages.Count)] - .Replace("[previousname]", prevName) - .Replace("[name]", location.Name); + .Replace("[previousname]", $"‖color:gui.orange‖{prevName}‖end‖") + .Replace("[name]", $"‖color:gui.orange‖{location.Name}‖end‖"); location.LastTypeChangeMessage = msg; if (GameMain.Client != null) { @@ -254,28 +258,52 @@ namespace Barotrauma { Rectangle rect = mapContainer.Rect; - if (CurrentDisplayLocation != null) + var currentDisplayLocation = GameMain.GameSession?.Campaign?.GetCurrentDisplayLocation(); + + if (currentDisplayLocation != null) { - if (!CurrentDisplayLocation.Discovered) + if (!currentDisplayLocation.Discovered) { - RemoveFogOfWar(CurrentDisplayLocation); - CurrentDisplayLocation.Discovered = true; - if (CurrentDisplayLocation.MapPosition.X > furthestDiscoveredLocation.MapPosition.X) + RemoveFogOfWar(currentDisplayLocation); + currentDisplayLocation.Discovered = true; + if (currentDisplayLocation.MapPosition.X > furthestDiscoveredLocation.MapPosition.X) { - furthestDiscoveredLocation = CurrentDisplayLocation; + furthestDiscoveredLocation = currentDisplayLocation; } } } - currLocationIndicatorPos = Vector2.Lerp(currLocationIndicatorPos, CurrentDisplayLocation.MapPosition, deltaTime); + Vector2 currentPosition = currentDisplayLocation.MapPosition; + if (Level.Loaded?.Type == LevelData.LevelType.LocationConnection && Level.Loaded.StartLocation != null && Level.Loaded.EndLocation != null) + { + Vector2 startPos = currentDisplayLocation == Level.Loaded.StartLocation ? Level.Loaded.StartLocation.MapPosition : Level.Loaded.EndLocation.MapPosition; + int moveDir = currentDisplayLocation == Level.Loaded.StartLocation ? 1 : -1; + + Vector2 diff = Level.Loaded.EndLocation.MapPosition - Level.Loaded.StartLocation.MapPosition; + currentPosition = startPos + + Vector2.Normalize(diff) * Math.Min(100, diff.Length() * 0.2f) * moveDir; + } + else + { + currentPosition += Vector2.UnitY * 35; + } + + currLocationIndicatorPos = Vector2.Lerp(currLocationIndicatorPos, currentPosition, deltaTime); #if DEBUG if (GameMain.DebugDraw) { if (editor == null) CreateEditor(); editor.AddToGUIUpdateList(order: 1); } + + if (PlayerInput.KeyHit(Keys.Space)) + { + Radiation?.OnStep(); + } #endif + Radiation?.MapUpdate(deltaTime); + if (mapAnimQueue.Count > 0) { hudVisibility = Math.Max(hudVisibility - deltaTime, 0.0f); @@ -299,15 +327,16 @@ namespace Barotrauma for (int i = 0; i < Locations.Count; i++) { Location location = Locations[i]; - if (IsInFogOfWar(location) && !(CurrentDisplayLocation?.Connections.Any(c => c.Locations.Contains(location)) ?? false) && !GameMain.DebugDraw) { continue; } + if (IsInFogOfWar(location) && !(currentDisplayLocation?.Connections.Any(c => c.Locations.Contains(location)) ?? false) && !GameMain.DebugDraw) { continue; } Vector2 pos = rectCenter + (location.MapPosition + viewOffset) * zoom; if (!rect.Contains(pos)) { continue; } - float iconScale = generationParams.LocationIconSize / location.Type.Sprite.size.X; - if (location == CurrentDisplayLocation) { iconScale *= 1.2f; } + Sprite locationSprite = location.IsCriticallyRadiated() ? location.Type.RadiationSprite ?? location.Type.Sprite : location.Type.Sprite; + float iconScale = generationParams.LocationIconSize / locationSprite.size.X; + if (location == currentDisplayLocation) { iconScale *= 1.2f; } - Rectangle drawRect = location.Type.Sprite.SourceRect; + Rectangle drawRect = locationSprite.SourceRect; drawRect.Width = (int)(drawRect.Width * iconScale * zoom * 1.4f); drawRect.Height = (int)(drawRect.Height * iconScale * zoom * 1.4f); drawRect.X = (int)pos.X - drawRect.Width / 2; @@ -324,6 +353,15 @@ namespace Barotrauma } } + if (SelectedConnection != null) + { + connectionHighlightState = Math.Min(connectionHighlightState + deltaTime, 1.0f); + } + else + { + connectionHighlightState = 0.0f; + } + if (GUI.KeyboardDispatcher.Subscriber == null) { float moveSpeed = 1000.0f; @@ -336,21 +374,27 @@ namespace Barotrauma } targetZoom = MathHelper.Clamp(targetZoom, generationParams.MinZoom, generationParams.MaxZoom); - zoom = MathHelper.Lerp(zoom, targetZoom, 0.1f); + zoom = MathHelper.Lerp(zoom, targetZoom * GUI.Scale, 0.1f); if (GUI.MouseOn == mapContainer) { foreach (LocationConnection connection in Connections) { - if (HighlightedLocation != CurrentDisplayLocation && - connection.Locations.Contains(HighlightedLocation) && connection.Locations.Contains(CurrentDisplayLocation)) + if (HighlightedLocation != currentDisplayLocation && + connection.Locations.Contains(HighlightedLocation) && + connection.Locations.Contains(currentDisplayLocation)) { if (PlayerInput.PrimaryMouseButtonClicked() && SelectedLocation != HighlightedLocation && HighlightedLocation != null) { - //clients aren't allowed to select the location without a permission - if ((GameMain.GameSession?.GameMode as CampaignMode)?.AllowedToManageCampaign() ?? false) + if (connection.Locked) { + new GUIMessageBox(string.Empty, TextManager.Get("LockedPathTooltip")); + } + //clients aren't allowed to select the location without a permission + else if ((GameMain.GameSession?.GameMode as CampaignMode)?.AllowedToManageCampaign() ?? false) + { + connectionHighlightState = 0.0f; SelectedConnection = connection; SelectedLocation = HighlightedLocation; @@ -371,13 +415,13 @@ namespace Barotrauma { if (PlayerInput.DoubleClicked() && HighlightedLocation != null) { - var passedConnection = CurrentDisplayLocation.Connections.Find(c => c.OtherLocation(CurrentDisplayLocation) == HighlightedLocation); + var passedConnection = currentDisplayLocation.Connections.Find(c => c.OtherLocation(currentDisplayLocation) == HighlightedLocation); if (passedConnection != null) { passedConnection.Passed = true; } - Location prevLocation = CurrentDisplayLocation; + Location prevLocation = currentDisplayLocation; CurrentLocation = HighlightedLocation; Level.Loaded.DebugSetStartLocation(CurrentLocation); Level.Loaded.DebugSetEndLocation(null); @@ -389,6 +433,7 @@ namespace Barotrauma { CurrentLocation.CreateStore(); ProgressWorld(); + Radiation?.OnStep(1); } else { @@ -412,12 +457,13 @@ namespace Barotrauma public void Draw(SpriteBatch spriteBatch, GUICustomComponent mapContainer) { - connectionTooltip = null; + tooltip = null; + var currentDisplayLocation = GameMain.GameSession?.Campaign?.GetCurrentDisplayLocation(); Rectangle rect = mapContainer.Rect; Vector2 viewSize = new Vector2(rect.Width / zoom, rect.Height / zoom); - Vector2 edgeBuffer = rect.Size.ToVector2() / 2; + Vector2 edgeBuffer = new Vector2(rect.Width * 0.05f); DrawOffset.X = MathHelper.Clamp(DrawOffset.X, -Width - edgeBuffer.X + viewSize.X / 2.0f, edgeBuffer.X - viewSize.X / 2.0f); DrawOffset.Y = MathHelper.Clamp(DrawOffset.Y, -Height - edgeBuffer.Y + viewSize.Y / 2.0f, edgeBuffer.Y - viewSize.Y / 2.0f); @@ -481,30 +527,16 @@ namespace Barotrauma } float rawNoiseScale = 1.0f + PerlinNoise.GetPerlin((int)(Timing.TotalTime * 1 - 1), (int)(Timing.TotalTime * 1 - 1)); - cameraNoiseStrength = PerlinNoise.GetPerlin((int)(Timing.TotalTime * 1 - 1), (int)(Timing.TotalTime * 1 - 1)); + DrawNoise(spriteBatch, rect, rawNoiseScale); - noiseOverlay.DrawTiled(spriteBatch, rect.Location.ToVector2(), rect.Size.ToVector2(), - startOffset: new Vector2(Rand.Range(0.0f, noiseOverlay.SourceRect.Width), Rand.Range(0.0f, noiseOverlay.SourceRect.Height)), - color : Color.White * cameraNoiseStrength * 0.1f, - textureScale: Vector2.One * rawNoiseScale); + Radiation?.Draw(spriteBatch, rect, zoom); - noiseOverlay.DrawTiled(spriteBatch, rect.Location.ToVector2(), rect.Size.ToVector2(), - startOffset: new Vector2(Rand.Range(0.0f, noiseOverlay.SourceRect.Width), Rand.Range(0.0f, noiseOverlay.SourceRect.Height)), - color: new Color(20,20,20,50), - textureScale: Vector2.One * rawNoiseScale * 2); - - noiseOverlay.DrawTiled(spriteBatch, Vector2.Zero, new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight), - startOffset: new Vector2(Rand.Range(0.0f, noiseOverlay.SourceRect.Width), Rand.Range(0.0f, noiseOverlay.SourceRect.Height)), - color: Color.White * cameraNoiseStrength * 0.1f, - textureScale: Vector2.One * noiseScale); - - Pair tooltip = null; if (generationParams.ShowLocations) { foreach (LocationConnection connection in Connections) { if (IsInFogOfWar(connection.Locations[0]) && IsInFogOfWar(connection.Locations[1])) { continue; } - DrawConnection(spriteBatch, connection, rect, viewOffset); + DrawConnection(spriteBatch, connection, rect, viewOffset, currentDisplayLocation); } for (int i = 0; i < Locations.Count; i++) @@ -512,19 +544,48 @@ namespace Barotrauma Location location = Locations[i]; if (IsInFogOfWar(location)) { continue; } Vector2 pos = rectCenter + (location.MapPosition + viewOffset) * zoom; - - Rectangle drawRect = location.Type.Sprite.SourceRect; + + Sprite locationSprite = location.IsCriticallyRadiated() ? location.Type.RadiationSprite ?? location.Type.Sprite : location.Type.Sprite; + + Rectangle drawRect = locationSprite.SourceRect; drawRect.X = (int)pos.X - drawRect.Width / 2; drawRect.Y = (int)pos.Y - drawRect.Width / 2; if (!rect.Intersects(drawRect)) { continue; } - if (location == CurrentDisplayLocation ) + Color color = location.Type.SpriteColor; + if (!location.Discovered) { color = Color.White; } + if (location.Connections.Find(c => c.Locations.Contains(currentDisplayLocation)) == null) { + color *= 0.5f; + } + + float iconScale = location == currentDisplayLocation ? 1.2f : 1.0f; + if (location == HighlightedLocation) + { + iconScale *= 1.2f; + } + + locationSprite.Draw(spriteBatch, pos, color, + scale: generationParams.LocationIconSize / locationSprite.size.X * iconScale * zoom); + + if (location == currentDisplayLocation) + { + if (SelectedLocation != null) + { + Vector2 dir = Vector2.Normalize(SelectedLocation.MapPosition - currLocationIndicatorPos); + GUI.Arrow.Draw(spriteBatch, + rectCenter + (currLocationIndicatorPos + viewOffset) * zoom + dir * generationParams.LocationIconSize * 0.6f * zoom, + generationParams.IndicatorColor, + GUI.Arrow.Origin, + rotate: MathUtils.VectorToAngle(dir) + MathHelper.PiOver2, + new Vector2(0.5f, 1.0f) * zoom); + } generationParams.CurrentLocationIndicator.Draw(spriteBatch, rectCenter + (currLocationIndicatorPos + viewOffset) * zoom, generationParams.IndicatorColor, - generationParams.CurrentLocationIndicator.Origin, 0, Vector2.One * (generationParams.LocationIconSize / generationParams.CurrentLocationIndicator.size.X) * 1.7f * zoom); + generationParams.CurrentLocationIndicator.Origin, 0, Vector2.One * (generationParams.LocationIconSize / generationParams.CurrentLocationIndicator.size.X) * 0.8f * zoom); + } if (location == SelectedLocation) @@ -535,31 +596,15 @@ namespace Barotrauma generationParams.SelectedLocationIndicator.Origin, 0, Vector2.One * (generationParams.LocationIconSize / generationParams.SelectedLocationIndicator.size.X) * 1.7f * zoom); } - Color color = location.Type.SpriteColor; - if (!location.Discovered) { color = Color.White; } - if (location.Connections.Find(c => c.Locations.Contains(CurrentDisplayLocation)) == null) - { - color *= 0.5f; - } - - float iconScale = location == CurrentDisplayLocation ? 1.2f : 1.0f; - if (location == HighlightedLocation) - { - iconScale *= 1.2f; - } - - location.Type.Sprite.Draw(spriteBatch, pos, color, - scale: generationParams.LocationIconSize / location.Type.Sprite.size.X * iconScale * zoom); if (location.TimeSinceLastTypeChange < 1 && !string.IsNullOrEmpty(location.LastTypeChangeMessage) && generationParams.TypeChangeIcon != null) { Vector2 typeChangeIconPos = pos + new Vector2(1.35f, -0.35f) * generationParams.LocationIconSize * 0.5f * zoom; float typeChangeIconScale = 18.0f / generationParams.TypeChangeIcon.SourceRect.Width; generationParams.TypeChangeIcon.Draw(spriteBatch, typeChangeIconPos, GUI.Style.Red, scale: typeChangeIconScale * zoom); - if (Vector2.Distance(PlayerInput.MousePosition, typeChangeIconPos) < generationParams.TypeChangeIcon.SourceRect.Width * zoom) + if (Vector2.Distance(PlayerInput.MousePosition, typeChangeIconPos) < generationParams.TypeChangeIcon.SourceRect.Width * zoom && + (tooltip == null || IsPreferredTooltip(typeChangeIconPos))) { - tooltip = new Pair( - new Rectangle(typeChangeIconPos.ToPoint(), new Point(30)), - location.LastTypeChangeMessage); + tooltip = (new Rectangle(typeChangeIconPos.ToPoint(), new Point(30)), location.LastTypeChangeMessage); } } if (location != CurrentLocation && CurrentLocation.AvailableMissions.Any(m => m.Locations.Contains(location)) && generationParams.MissionIcon != null) @@ -567,16 +612,14 @@ namespace Barotrauma Vector2 missionIconPos = pos + new Vector2(1.35f, 0.35f) * generationParams.LocationIconSize * 0.5f * zoom; float missionIconScale = 18.0f / generationParams.MissionIcon.SourceRect.Width; generationParams.MissionIcon.Draw(spriteBatch, missionIconPos, generationParams.IndicatorColor, scale: missionIconScale * zoom); - if (Vector2.Distance(PlayerInput.MousePosition, missionIconPos) < generationParams.MissionIcon.SourceRect.Width * zoom) + if (Vector2.Distance(PlayerInput.MousePosition, missionIconPos) < generationParams.MissionIcon.SourceRect.Width * zoom && IsPreferredTooltip(missionIconPos)) { var availableMissions = CurrentLocation.AvailableMissions.Where(m => m.Locations.Contains(location)); - tooltip = new Pair( - new Rectangle(missionIconPos.ToPoint(), new Point(30)), - TextManager.Get("mission") + '\n'+ string.Join('\n', availableMissions.Select(m => "- " + m.Name))); + tooltip = (new Rectangle(missionIconPos.ToPoint(), new Point(30)), TextManager.Get("mission") + '\n'+ string.Join('\n', availableMissions.Select(m => "- " + m.Name))); } } - if (GameMain.DebugDraw && location == HighlightedLocation && (!location.Discovered || !location.Type.HasOutpost)) + if (GameMain.DebugDraw && location == HighlightedLocation && (!location.Discovered || !location.HasOutpost())) { if (location.Reputation != null) { @@ -603,12 +646,27 @@ namespace Barotrauma DrawDecorativeHUD(spriteBatch, rect); - if (HighlightedLocation != null) + bool drawRadiationTooltip = true; + + if (tooltip != null) { + if (tooltip.Value.tip != prevTooltip) + { + prevTooltip = tooltip.Value.tip; + tooltipRichTextData = RichTextData.GetRichTextData(tooltip.Value.tip, out sanitizedTooltip); + } + GUIComponent.DrawToolTip(spriteBatch, sanitizedTooltip, tooltip.Value.targetArea, tooltipRichTextData); + drawRadiationTooltip = false; + } + else if (HighlightedLocation != null) + { + drawRadiationTooltip = false; Vector2 pos = rectCenter + (HighlightedLocation.MapPosition + viewOffset) * zoom; pos.X += 50 * zoom; + pos.X = (int)pos.X; + pos.Y = (int)pos.Y; Vector2 nameSize = GUI.LargeFont.MeasureString(HighlightedLocation.Name); - Vector2 typeSize = GUI.Font.MeasureString(HighlightedLocation.Type.Name); + Vector2 typeSize = string.IsNullOrEmpty(HighlightedLocation.Type.Name) ? Vector2.Zero : GUI.Font.MeasureString(HighlightedLocation.Type.Name); Vector2 size = new Vector2(Math.Max(nameSize.X, typeSize.X), nameSize.Y + typeSize.Y); bool showReputation = hudVisibility > 0.0f && HighlightedLocation.Discovered && HighlightedLocation.Type.HasOutpost && HighlightedLocation.Reputation != null; string repLabelText = null, repValueText = null; @@ -616,15 +674,14 @@ namespace Barotrauma if (showReputation) { repLabelText = TextManager.Get("reputation"); - repLabelSize = GUI.Font.MeasureString(repLabelText); - size.X = Math.Max(size.X, repLabelSize.X); - repBarSize = new Vector2(Math.Max(0.75f * size.X, 100), repLabelSize.Y); - size.X = Math.Max(size.X, (4.0f / 3.0f) * repBarSize.X); - size.Y += 2 * repLabelSize.Y + 4 + repBarSize.Y; - repValueText = ((int)HighlightedLocation.Reputation.Value).ToString(); + repLabelSize = GUI.Font.MeasureString(repLabelText); + repBarSize = new Vector2(GUI.IntScale(200), repLabelSize.Y); + size.Y += 2 * repLabelSize.Y + GUI.IntScale(5) + repBarSize.Y; + repValueText = HighlightedLocation.Reputation.GetFormattedReputationText(addColorTags: false); + size.X = Math.Max(size.X, repBarSize.X + GUI.Font.MeasureString(repValueText).X + GUI.IntScale(10)); } GUI.Style.GetComponentStyle("OuterGlow").Sprites[GUIComponent.ComponentState.None][0].Draw( - spriteBatch, new Rectangle((int)(pos.X - 60 * GUI.Scale), (int)(pos.Y - size.Y), (int)(size.X + 120 * GUI.Scale), (int)(size.Y * 2.2f)), Color.Black * hudVisibility); + spriteBatch, new Rectangle((int)(pos.X - 60 * GUI.Scale), (int)(pos.Y - size.Y), (int)(size.X + 120 * GUI.Scale), (int)(size.Y * 2.2f)), Color.Black * hudVisibility); var topLeftPos = pos - new Vector2(0.0f, size.Y / 2); GUI.DrawString(spriteBatch, topLeftPos, HighlightedLocation.Name, GUI.Style.TextColor * hudVisibility * 1.5f, font: GUI.LargeFont); topLeftPos += new Vector2(0.0f, nameSize.Y); @@ -633,26 +690,51 @@ namespace Barotrauma { topLeftPos += new Vector2(0.0f, typeSize.Y + repLabelSize.Y); GUI.DrawString(spriteBatch, topLeftPos, repLabelText, GUI.Style.TextColor * hudVisibility * 1.5f); - topLeftPos += new Vector2(0.0f, repLabelSize.Y + 4); + topLeftPos += new Vector2(0.0f, repLabelSize.Y + GUI.IntScale(10)); Rectangle repBarRect = new Rectangle(new Point((int)topLeftPos.X, (int)topLeftPos.Y), new Point((int)repBarSize.X, (int)repBarSize.Y)); RoundSummary.DrawReputationBar(spriteBatch, repBarRect, HighlightedLocation.Reputation.NormalizedValue); - GUI.DrawString(spriteBatch, new Vector2(repBarRect.Right + 4, repBarRect.Top), repValueText, GUI.Style.TextColor); + GUI.DrawString(spriteBatch, new Vector2(repBarRect.Right + GUI.IntScale(5), repBarRect.Top), repValueText, Reputation.GetReputationColor(HighlightedLocation.Reputation.NormalizedValue)); } } - if (tooltip != null) + + if (drawRadiationTooltip) { - GUIComponent.DrawToolTip(spriteBatch, tooltip.Second, tooltip.First); - } - if (connectionTooltip != null) - { - GUIComponent.DrawToolTip(spriteBatch, connectionTooltip.Second, connectionTooltip.First); + Radiation?.DrawFront(spriteBatch); } + spriteBatch.End(); GameMain.Instance.GraphicsDevice.ScissorRectangle = prevScissorRect; spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); } - private void DrawConnection(SpriteBatch spriteBatch, LocationConnection connection, Rectangle viewArea, Vector2 viewOffset, Color? overrideColor = null) + public static void DrawNoise(SpriteBatch spriteBatch, Rectangle rect, float strength) + { + noiseOverlay ??= new Sprite("Content/UI/noise.png", Vector2.Zero); + + float noiseT = (float)(Timing.TotalTime * 0.01f); + float noiseScale = (float)PerlinNoise.CalculatePerlin(noiseT * 5.0f, noiseT * 2.0f, 0) * 5.0f; + + float rawNoiseScale = 1.0f + GetPerlinNoise(); + + noiseOverlay.DrawTiled(spriteBatch, rect.Location.ToVector2(), rect.Size.ToVector2(), + startOffset: new Vector2(Rand.Range(0.0f, noiseOverlay.SourceRect.Width), Rand.Range(0.0f, noiseOverlay.SourceRect.Height)), + color : Color.White * strength * 0.1f, + textureScale: Vector2.One * rawNoiseScale); + + noiseOverlay.DrawTiled(spriteBatch, rect.Location.ToVector2(), rect.Size.ToVector2(), + startOffset: new Vector2(Rand.Range(0.0f, noiseOverlay.SourceRect.Width), Rand.Range(0.0f, noiseOverlay.SourceRect.Height)), + color: new Color(20,20,20,50), + textureScale: Vector2.One * rawNoiseScale * 2); + + noiseOverlay.DrawTiled(spriteBatch, Vector2.Zero, new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight), + startOffset: new Vector2(Rand.Range(0.0f, noiseOverlay.SourceRect.Width), Rand.Range(0.0f, noiseOverlay.SourceRect.Height)), + color: Color.White * strength * 0.1f, + textureScale: Vector2.One * noiseScale); + } + + private static float GetPerlinNoise() => PerlinNoise.GetPerlin((int)(Timing.TotalTime * 1 - 1), (int)(Timing.TotalTime * 1 - 1)); + + private void DrawConnection(SpriteBatch spriteBatch, LocationConnection connection, Rectangle viewArea, Vector2 viewOffset, Location currentDisplayLocation, Color? overrideColor = null) { Color connectionColor; if (GameMain.DebugDraw) @@ -674,19 +756,22 @@ namespace Barotrauma int width = (int)(generationParams.LocationConnectionWidth * zoom); + //current level if (Level.Loaded?.LevelData == connection.LevelData) { connectionColor = generationParams.HighlightedConnectionColor; width = (int)(width * 1.5f); } - if (SelectedLocation != CurrentDisplayLocation && - (connection.Locations.Contains(SelectedLocation) && connection.Locations.Contains(CurrentDisplayLocation))) + //selected connection + if (SelectedLocation != currentDisplayLocation && + connection.Locations.Contains(SelectedLocation) && connection.Locations.Contains(currentDisplayLocation)) { connectionColor = generationParams.HighlightedConnectionColor; width *= 2; } - else if (HighlightedLocation != CurrentDisplayLocation && - (connection.Locations.Contains(HighlightedLocation) && connection.Locations.Contains(CurrentDisplayLocation))) + //highlighted connection + else if (HighlightedLocation != currentDisplayLocation && + connection.Locations.Contains(HighlightedLocation) && connection.Locations.Contains(currentDisplayLocation)) { connectionColor = generationParams.HighlightedConnectionColor; width *= 2; @@ -741,16 +826,52 @@ namespace Barotrauma } float dist = Vector2.Distance(start, end); var connectionSprite = connection.Passed ? generationParams.PassedConnectionSprite : generationParams.ConnectionSprite; + + Color segmentColor = connectionColor; + int segmentWidth = width; + if (connection == SelectedConnection) + { + float t = (i - startIndex) / (float)(endIndex - startIndex - 1); + if (currentDisplayLocation == connection.Locations[1]) { t = 1.0f - t; } + if (t > connectionHighlightState) + { + segmentWidth /= 2; + segmentColor = connection.Passed ? generationParams.ConnectionColor : generationParams.UnvisitedConnectionColor; + } + else + { + } + } + spriteBatch.Draw(connectionSprite.Texture, - new Rectangle((int)start.X, (int)start.Y, (int)(dist - 1 * zoom), width), - connectionSprite.SourceRect, connectionColor * a, MathUtils.VectorToAngle(end - start), + new Rectangle((int)start.X, (int)start.Y, (int)(dist - 1 * zoom), segmentWidth), + connectionSprite.SourceRect, segmentColor * a, + MathUtils.VectorToAngle(end - start), new Vector2(0, connectionSprite.size.Y / 2), SpriteEffects.None, 0.01f); } + + int iconCount = 0, iconIndex = 0; if (connectionStart.HasValue && connectionEnd.HasValue) - { - GUIComponentStyle crushDepthWarningIconStyle = null; + { + if (connection.LevelData.HasBeaconStation) { iconCount++; } + if (connection.LevelData.HasHuntingGrounds) { iconCount++; } + if (connection.Locked) { iconCount++; } string tooltip = null; - var subCrushDepth = Submarine.MainSub?.RealWorldCrushDepth ?? Level.DefaultRealWorldCrushDepth; + float subCrushDepth = Level.DefaultRealWorldCrushDepth; + var currentOrPendingSub = SubmarineSelection.CurrentOrPendingSubmarine(); + if (Submarine.MainSub != null && Submarine.MainSub.Info == currentOrPendingSub) + { + subCrushDepth = Submarine.MainSub.RealWorldCrushDepth; + } + else if (currentOrPendingSub != null) + { + if (pendingSubInfo.pendingSub != currentOrPendingSub) + { + // Store the real world crush depth for the pending sub so that we don't have to calculate it again every time + pendingSubInfo = (currentOrPendingSub, currentOrPendingSub.GetRealWorldCrushDepth()); + } + subCrushDepth = pendingSubInfo.realWorldCrushDepth; + } if (GameMain.GameSession?.Campaign?.UpgradeManager != null) { var hullUpgradePrefab = UpgradePrefab.Find("increasewallhealth"); @@ -769,38 +890,71 @@ namespace Barotrauma } } + string crushDepthWarningIconStyle = null; if (connection.LevelData.InitialDepth * Physics.DisplayToRealWorldRatio > subCrushDepth) { - crushDepthWarningIconStyle = GUI.Style.GetComponentStyle("CrushDepthWarningHighIcon"); + iconCount++; + crushDepthWarningIconStyle = "CrushDepthWarningHighIcon"; tooltip = "crushdepthwarninghigh"; } else if ((connection.LevelData.InitialDepth + connection.LevelData.Size.Y) * Physics.DisplayToRealWorldRatio > subCrushDepth) { - crushDepthWarningIconStyle = GUI.Style.GetComponentStyle("CrushDepthWarningLowIcon"); + iconCount++; + crushDepthWarningIconStyle = "CrushDepthWarningLowIcon"; tooltip = "crushdepthwarninglow"; } + if (connection.LevelData.HasBeaconStation) + { + var beaconStationIconStyle = connection.LevelData.IsBeaconActive ? "BeaconStationActive" : "BeaconStationInactive"; + DrawIcon(beaconStationIconStyle, (int)(28 * zoom), TextManager.Get(connection.LevelData.IsBeaconActive ? "BeaconStationActiveTooltip" : "BeaconStationInactiveTooltip")); + } + + if (connection.Locked) + { + var gateLocation = connection.Locations[0].IsGateBetweenBiomes ? connection.Locations[0] : connection.Locations[1]; + var unlockEvent = + EventSet.PrefabList.Find(ep => ep.UnlockPathEvent && ep.BiomeIdentifier == gateLocation.LevelData.Biome.Identifier) ?? + EventSet.PrefabList.Find(ep => ep.UnlockPathEvent && string.IsNullOrEmpty(ep.BiomeIdentifier)); + + if (unlockEvent != null) + { + Reputation unlockReputation = CurrentLocation.Reputation; + Faction unlockFaction = null; + if (!string.IsNullOrEmpty(unlockEvent.UnlockPathFaction)) + { + unlockFaction = GameMain.GameSession.Campaign.Factions.Find(f => f.Prefab.Identifier.Equals(unlockEvent.UnlockPathFaction, StringComparison.OrdinalIgnoreCase)); + unlockReputation = unlockFaction?.Reputation; + } + + DrawIcon( + "LockedLocationConnection", (int)(28 * zoom), + TextManager.GetWithVariables(unlockEvent.UnlockPathTooltip ?? "LockedPathTooltip", + new string[] { "[requiredreputation]", "[currentreputation]" }, + new string[] { Reputation.GetFormattedReputationText(MathUtils.InverseLerp(unlockReputation.MinReputation, unlockReputation.MaxReputation, unlockEvent.UnlockPathReputation), unlockEvent.UnlockPathReputation, addColorTags: true), unlockReputation.GetFormattedReputationText(addColorTags: true) })); + } + else + { + DrawIcon("LockedLocationConnection", (int)(28 * zoom), TextManager.Get("LockedPathTooltip")); + } + + } + + if (connection.LevelData.HasHuntingGrounds) + { + DrawIcon("HuntingGrounds", (int)(28 * zoom), TextManager.Get("HuntingGroundsTooltip")); + } + if (crushDepthWarningIconStyle != null) { - Vector2 iconPos = (connectionStart.Value + connectionEnd.Value) / 2; - float iconSize = 32.0f * GUI.Scale; - bool mouseOn = HighlightedLocation == null && Vector2.DistanceSquared(iconPos, PlayerInput.MousePosition) < iconSize * iconSize; - Sprite crushDepthWarningIcon = crushDepthWarningIconStyle.GetDefaultSprite(); - crushDepthWarningIcon.Draw(spriteBatch, iconPos, - mouseOn ? crushDepthWarningIconStyle.HoverColor : crushDepthWarningIconStyle.Color, - scale: iconSize / crushDepthWarningIcon.size.X); - if (mouseOn) - { - connectionTooltip = new Pair( - new Rectangle(iconPos.ToPoint(), new Point((int)iconSize)), - TextManager.Get(tooltip) - .Replace("[initialdepth]", ((int)(connection.LevelData.InitialDepth * Physics.DisplayToRealWorldRatio)).ToString()) - .Replace("[submarinecrushdepth]", ((int)subCrushDepth).ToString())); - } + DrawIcon(crushDepthWarningIconStyle, (int)(32 * zoom), + TextManager.Get(tooltip) + .Replace("[initialdepth]", $"‖color:gui.orange‖{(int)(connection.LevelData.InitialDepth * Physics.DisplayToRealWorldRatio)}‖end‖") + .Replace("[submarinecrushdepth]", $"‖color:gui.orange‖{(int)subCrushDepth}‖end‖")); } } - if (GameMain.DebugDraw && zoom > 1.0f && generationParams.ShowLevelTypeNames) + if (GameMain.DebugDraw && zoom > (1.0f * GUI.Scale) && generationParams.ShowLevelTypeNames) { Vector2 center = rectCenter + (connection.CenterPos + viewOffset) * zoom; if (viewArea.Contains(center) && connection.Biome != null) @@ -808,6 +962,30 @@ namespace Barotrauma GUI.DrawString(spriteBatch, center, connection.Biome.Identifier + " (" + connection.Difficulty + ")", Color.White); } } + + void DrawIcon(string iconStyle, int iconSize, string tooltipText) + { + Vector2 iconPos = (connectionStart.Value + connectionEnd.Value) / 2; + Vector2 iconDiff = Vector2.Normalize(connectionEnd.Value - connectionStart.Value) * iconSize; + + iconPos += (iconDiff * -(iconCount - 1) / 2.0f) + iconDiff * iconIndex; + + var style = GUI.Style.GetComponentStyle(iconStyle); + bool mouseOn = Vector2.DistanceSquared(iconPos, PlayerInput.MousePosition) < iconSize * iconSize && IsPreferredTooltip(iconPos); + Sprite iconSprite = style.GetDefaultSprite(); + iconSprite.Draw(spriteBatch, iconPos, (mouseOn ? style.HoverColor : style.Color) * 0.7f, + scale: iconSize / iconSprite.size.X); + if (mouseOn) + { + tooltip = (new Rectangle((iconPos - Vector2.One * iconSize / 2).ToPoint(), new Point(iconSize)), tooltipText); + } + iconIndex++; + } + } + + private bool IsPreferredTooltip(Vector2 tooltipPos) + { + return tooltip == null || Vector2.DistanceSquared(tooltipPos, PlayerInput.MousePosition) < Vector2.DistanceSquared(tooltip.Value.targetArea.Center.ToVector2(), PlayerInput.MousePosition); } private float hudVisibility; @@ -828,8 +1006,8 @@ namespace Barotrauma private void UpdateMapAnim(MapAnim anim, float deltaTime) { - //pause animation while there are messageboxes on screen - if (GUIMessageBox.MessageBoxes.Count > 0) return; + //pause animation while there are messageboxes (other than hints) on screen + if (GUIMessageBox.MessageBoxes.Count(c => !(c is GUIMessageBox mb) || mb.MessageBoxType != GUIMessageBox.Type.Hint) > 0) { return; } if (!string.IsNullOrEmpty(anim.StartMessage)) { @@ -838,8 +1016,9 @@ namespace Barotrauma return; } - if (anim.StartZoom == null) { anim.StartZoom = MathUtils.InverseLerp(generationParams.MinZoom, generationParams.MaxZoom, zoom); } - if (anim.EndZoom == null) { anim.EndZoom = MathUtils.InverseLerp(generationParams.MinZoom, generationParams.MaxZoom, zoom); } + float unscaledZoom = zoom / GUI.Scale; + if (anim.StartZoom == null) { anim.StartZoom = MathUtils.InverseLerp(generationParams.MinZoom, generationParams.MaxZoom, unscaledZoom); } + if (anim.EndZoom == null) { anim.EndZoom = MathUtils.InverseLerp(generationParams.MinZoom, generationParams.MaxZoom, unscaledZoom); } anim.StartPos = (anim.StartLocation == null) ? -DrawOffset : anim.StartLocation.MapPosition; @@ -852,7 +1031,8 @@ namespace Barotrauma zoom = MathHelper.Lerp(generationParams.MinZoom, generationParams.MaxZoom, - MathHelper.SmoothStep(anim.StartZoom.Value, anim.EndZoom.Value, t)); + MathHelper.SmoothStep(anim.StartZoom.Value, anim.EndZoom.Value, t)) + * GUI.Scale; if (anim.Timer >= anim.Duration) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Radiation.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Radiation.cs new file mode 100644 index 000000000..990d91775 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Radiation.cs @@ -0,0 +1,68 @@ +#nullable enable +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace Barotrauma +{ + internal partial class Radiation + { + private static readonly string radiationTooltip = TextManager.Get("RadiationTooltip"); + private static float spriteIndex; + private readonly SpriteSheet sheet = GUI.Style.RadiationAnimSpriteSheet; + private int maxFrames => sheet.FrameCount + 1; + + private bool isHovingOver; + + public void Draw(SpriteBatch spriteBatch, Rectangle container, float zoom) + { + if (!Enabled) { return; } + + UISprite uiSprite = GUI.Style.RadiationSprite; + var (offsetX, offsetY) = Map.DrawOffset * zoom; + var (centerX, centerY) = container.Center.ToVector2(); + var (halfSizeX, halfSizeY) = new Vector2(container.Width / 2f, container.Height / 2f) * zoom; + float viewBottom = centerY + Map.Height * zoom; + Vector2 topLeft = new Vector2(centerX + offsetX - halfSizeX, centerY + offsetY - halfSizeY); + Vector2 size = new Vector2((Amount - increasedAmount) * zoom + halfSizeX, viewBottom - topLeft.Y); + if (size.X < 0) { return; } + + Vector2 spriteScale = new Vector2(zoom); + + uiSprite.Sprite.DrawTiled(spriteBatch, topLeft, size, Params.RadiationAreaColor, Vector2.Zero, textureScale: spriteScale); + + Vector2 topRight = topLeft + Vector2.UnitX * size.X; + + int index = 0; + for (float i = 0; i <= size.Y; i += sheet.FrameSize.Y / 2f * zoom) + { + bool isEven = ++index % 2 == 0; + Vector2 origin = new Vector2(0.5f, 0) * sheet.FrameSize.X; + // every other sprite's animation is reversed to make it seem more chaotic + int sprite = (int) MathF.Floor(isEven ? spriteIndex : maxFrames - spriteIndex); + sheet.Draw(spriteBatch, sprite, topRight + new Vector2(0, i), Params.RadiationBorderTint, origin, 0f, spriteScale); + } + + isHovingOver = container.Contains(PlayerInput.MousePosition) && PlayerInput.MousePosition.X < topLeft.X + size.X; + } + + public void DrawFront(SpriteBatch spriteBatch) + { + if (isHovingOver) + { + GUIComponent.DrawToolTip(spriteBatch, radiationTooltip, PlayerInput.MousePosition + new Vector2(18 * GUI.Scale)); + } + } + + public void MapUpdate(float deltaTime) + { + float spriteStep = Params.BorderAnimationSpeed * deltaTime; + spriteIndex = (spriteIndex + spriteStep) % maxFrames; + + if (increasedAmount > 0) + { + increasedAmount -= (lastIncrease / Params.AnimationSpeed) * deltaTime; + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index b9cbf543f..03649abd1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -20,9 +20,6 @@ namespace Barotrauma public static Vector2 StartMovingPos => startMovingPos; - // Quick undo/redo for size and movement only. TODO: Remove if we do a more general implementation. - private Memento rectMemento; - public event Action Resized; private static bool resizing; @@ -65,8 +62,6 @@ namespace Barotrauma } } - //protected bool isSelected; - private static bool disableSelect; public static bool DisableSelect { @@ -805,7 +800,7 @@ namespace Barotrauma } if (selectionPos != null && selectionPos != Vector2.Zero) { - GUI.DrawRectangle(spriteBatch, new Vector2(selectionPos.X, -selectionPos.Y), selectionSize, Color.DarkRed, false, 0, (int)Math.Max(1.5f / GameScreen.Selected.Cam.Zoom, 1.0f)); + GUI.DrawRectangle(spriteBatch, new Vector2(selectionPos.X, -selectionPos.Y), selectionSize, Color.DarkRed, false, 0, 2f / GameScreen.Selected.Cam.Zoom); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index d1effedfe..9d4200691 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -24,20 +24,23 @@ namespace Barotrauma { get { - if (!GameMain.SubEditorScreen.ShowThalamus && prefab.Category.HasFlag(MapEntityCategory.Thalamus)) + if (GameMain.SubEditorScreen.IsSubcategoryHidden(prefab.Subcategory)) { return false; } return HasBody ? ShowWalls : ShowStructures; } } - - private string specialTag; + +#if DEBUG [Editable, Serialize("", true)] +#else + [Serialize("", true)] +#endif public string SpecialTag { - get { return specialTag; } - set { specialTag = value; } + get; + set; } partial void InitProjSpecific() @@ -92,7 +95,10 @@ namespace Barotrauma { int heightScaled = (int)(20 * GUI.Scale); editingHUD = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.25f), GUI.Canvas, Anchor.CenterRight) { MinSize = new Point(400, 0) }) { UserData = this }; - GUIListBox listBox = new GUIListBox(new RectTransform(new Vector2(0.95f, 0.8f), editingHUD.RectTransform, Anchor.Center), style: null); + GUIListBox listBox = new GUIListBox(new RectTransform(new Vector2(0.95f, 0.8f), editingHUD.RectTransform, Anchor.Center), style: null) + { + CanTakeKeyBoardFocus = false + }; var editor = new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true, titleFont: GUI.LargeFont) { UserData = this }; if (Submarine.MainSub?.Info?.Type == SubmarineType.OutpostModule) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineBody.cs index e271e9138..2a63d6690 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineBody.cs @@ -79,14 +79,22 @@ namespace Barotrauma if (maxDamageStructure != null) { - SoundPlayer.PlayDamageSound( - soundTag, - impact * 10.0f, - ConvertUnits.ToDisplayUnits(impactSimPos), - MathHelper.Lerp(2000.0f, 10000.0f, (impact - MinCollisionImpact) / 2.0f), - maxDamageStructure.Tags); + PlayDamageSound(impactSimPos, impact, soundTag, maxDamageStructure); } } + private void PlayDamageSound(Vector2 impactSimPos, float impact, string soundTag, Structure hitStructure = null) + { + if (impact < MinCollisionImpact) { return; } + + SoundPlayer.PlayDamageSound( + soundTag, + impact * 10.0f, + ConvertUnits.ToDisplayUnits(impactSimPos), + MathHelper.Lerp(2000.0f, 10000.0f, (impact - MinCollisionImpact) / 2.0f), + hitStructure?.Tags); + } + + } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs index 6ef20f966..412dcad18 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs @@ -1,5 +1,6 @@ using Microsoft.Xna.Framework; using System; +using System.Collections.Generic; using System.Linq; namespace Barotrauma @@ -19,7 +20,7 @@ namespace Barotrauma { var texture = TextureLoader.FromStream(mem, path: FilePath, compress: false); if (texture == null) { throw new Exception("PreviewImage texture returned null"); } - PreviewImage = new Sprite(texture, null, null); + PreviewImage = new Sprite(texture, sourceRectangle: null, newOffset: null, path: FilePath); } } catch (Exception e) @@ -32,21 +33,49 @@ namespace Barotrauma } } - public void CreatePreviewWindow(GUIComponent parent) { var content = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform), style: null); - if (PreviewImage == null) + var previewButton = new GUIButton(new RectTransform(new Vector2(1f, 0.5f), content.RectTransform), style: null) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform), TextManager.Get(SavedSubmarines.Contains(this) ? "SubPreviewImageNotFound" : "SubNotDownloaded")); + CanBeFocused = SubmarineElement != null, + OnClicked = (btn, obj) => { SubmarinePreview.Create(this); return false; }, + }; + + var previewImage = PreviewImage ?? savedSubmarines.Find(s => s.Name.Equals(Name, StringComparison.OrdinalIgnoreCase))?.PreviewImage; + if (previewImage == null) + { + new GUITextBlock(new RectTransform(Vector2.One, previewButton.RectTransform), TextManager.Get(SavedSubmarines.Contains(this) ? "SubPreviewImageNotFound" : "SubNotDownloaded")); } else { - var submarinePreviewBackground = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform), style: null) { Color = Color.Black }; - new GUIImage(new RectTransform(new Vector2(0.98f), submarinePreviewBackground.RectTransform, Anchor.Center), PreviewImage, scaleToFit: true); - new GUIFrame(new RectTransform(Vector2.One, submarinePreviewBackground.RectTransform), "InnerGlow", color: Color.Black); + var submarinePreviewBackground = new GUIFrame(new RectTransform(Vector2.One, previewButton.RectTransform), style: null) + { + Color = Color.Black, + HoverColor = Color.Black, + SelectedColor = Color.Black, + PressedColor = Color.Black, + CanBeFocused = false, + }; + new GUIImage(new RectTransform(new Vector2(0.98f), submarinePreviewBackground.RectTransform, Anchor.Center), previewImage, scaleToFit: true) { CanBeFocused = false }; + new GUIFrame(new RectTransform(Vector2.One, submarinePreviewBackground.RectTransform), "InnerGlow", color: Color.Black) { CanBeFocused = false }; } + + if (SubmarineElement != null) + { + new GUIFrame(new RectTransform(Vector2.One * 0.12f, previewButton.RectTransform, anchor: Anchor.BottomRight, pivot: Pivot.BottomRight, scaleBasis: ScaleBasis.BothHeight) + { + AbsoluteOffset = new Point((int)(0.03f * previewButton.Rect.Height)) + }, + "ExpandButton", Color.White) + { + Color = Color.White, + HoverColor = Color.White, + PressedColor = Color.White + }; + } + var descriptionBox = new GUIListBox(new RectTransform(new Vector2(1, 0.5f), content.RectTransform, Anchor.BottomCenter)) { UserData = "descriptionbox", @@ -56,39 +85,31 @@ namespace Barotrauma ScalableFont font = parent.Rect.Width < 350 ? GUI.SmallFont : GUI.Font; - CreateSpecsWindow(descriptionBox, font); - - //space - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), descriptionBox.Content.RectTransform), style: null); - - if (!string.IsNullOrEmpty(Description)) - { - new GUITextBlock(new RectTransform(new Vector2(1, 0), descriptionBox.Content.RectTransform), - TextManager.Get("SaveSubDialogDescription", fallBackTag: "WorkshopItemDescription"), font: GUI.Font, wrap: true) - { CanBeFocused = false, ForceUpperCase = true }; - } - - new GUITextBlock(new RectTransform(new Vector2(1, 0), descriptionBox.Content.RectTransform), Description, font: font, wrap: true) - { - CanBeFocused = false - }; + CreateSpecsWindow(descriptionBox, font, includeDescription: true); } - public void CreateSpecsWindow(GUIListBox parent, ScalableFont font) + public void CreateSpecsWindow(GUIListBox parent, ScalableFont font, bool includeTitle = true, bool includeClass = true, bool includeDescription = false) { float leftPanelWidth = 0.6f; - float rightPanelWidth = 0.4f / leftPanelWidth; + float rightPanelWidth = 0.4f; string className = !HasTag(SubmarineTag.Shuttle) ? TextManager.Get($"submarineclass.{SubmarineClass}") : TextManager.Get("shuttle"); - int nameHeight = (int)GUI.LargeFont.MeasureString(DisplayName, true).Y; int classHeight = (int)GUI.SubHeadingFont.MeasureString(className).Y; int leftPanelWidthInt = (int)(parent.Rect.Width * leftPanelWidth); - var submarineNameText = new GUITextBlock(new RectTransform(new Point(leftPanelWidthInt, nameHeight + HUDLayoutSettings.Padding / 2), parent.Content.RectTransform), DisplayName, textAlignment: Alignment.CenterLeft, font: GUI.LargeFont) { CanBeFocused = false }; - submarineNameText.RectTransform.MinSize = new Point(0, (int)submarineNameText.TextSize.Y); - var submarineClassText = new GUITextBlock(new RectTransform(new Point(leftPanelWidthInt, classHeight), parent.Content.RectTransform), className, textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont) { CanBeFocused = false }; - submarineClassText.RectTransform.MinSize = new Point(0, (int)submarineClassText.TextSize.Y); - + GUITextBlock submarineNameText = null; + GUITextBlock submarineClassText = null; + if (includeTitle) + { + int nameHeight = (int)GUI.LargeFont.MeasureString(DisplayName, true).Y; + submarineNameText = new GUITextBlock(new RectTransform(new Point(leftPanelWidthInt, nameHeight + HUDLayoutSettings.Padding / 2), parent.Content.RectTransform), DisplayName, textAlignment: Alignment.CenterLeft, font: GUI.LargeFont) { CanBeFocused = false }; + submarineNameText.RectTransform.MinSize = new Point(0, (int)submarineNameText.TextSize.Y); + } + if (includeClass) + { + submarineClassText = new GUITextBlock(new RectTransform(new Point(leftPanelWidthInt, classHeight), parent.Content.RectTransform), className, textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont) { CanBeFocused = false }; + submarineClassText.RectTransform.MinSize = new Point(0, (int)submarineClassText.TextSize.Y); + } Vector2 realWorldDimensions = Dimensions * Physics.DisplayToRealWorldRatio; if (realWorldDimensions != Vector2.Zero) { @@ -149,8 +170,30 @@ namespace Barotrauma versionText.RectTransform.MinSize = new Point(0, versionText.Children.First().Rect.Height); } - submarineNameText.AutoScaleHorizontal = true; - GUITextBlock.AutoScaleAndNormalize(parent.Content.Children.Where(c => c is GUITextBlock && c != submarineNameText).Cast()); + if (submarineNameText != null) + { + submarineNameText.AutoScaleHorizontal = true; + } + + GUITextBlock descBlock = null; + if (includeDescription) + { + //space + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), parent.Content.RectTransform), style: null); + + if (!string.IsNullOrEmpty(Description)) + { + var wsItemDesc = new GUITextBlock(new RectTransform(new Vector2(1, 0), parent.Content.RectTransform), + TextManager.Get("SaveSubDialogDescription", fallBackTag: "WorkshopItemDescription"), font: GUI.Font, wrap: true) + { CanBeFocused = false, ForceUpperCase = true }; + + descBlock = new GUITextBlock(new RectTransform(new Vector2(1, 0), parent.Content.RectTransform), Description, font: font, wrap: true) + { + CanBeFocused = false + }; + } + } + GUITextBlock.AutoScaleAndNormalize(parent.Content.GetAllChildren().Where(c => c != submarineNameText && c != descBlock)); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs new file mode 100644 index 000000000..6fb90c45e --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs @@ -0,0 +1,648 @@ +using Barotrauma.Extensions; +using Barotrauma.IO; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; + +namespace Barotrauma +{ + class SubmarinePreview : IDisposable + { + private SpriteRecorder spriteRecorder; + private SubmarineInfo submarineInfo; + private Camera camera; + private Task loadTask; + private volatile bool isDisposed; + + private GUIFrame previewFrame; + + private class HullCollection + { + public readonly List Rects; + public readonly string Name; + + public HullCollection(string identifier) + { + Rects = new List(); + Name = TextManager.Get(identifier, returnNull: true) ?? identifier; + } + + public void AddRect(XElement element) + { + Rectangle rect = element.GetAttributeRect("rect", Rectangle.Empty); + rect.Y = -rect.Y; + Rects.Add(rect); + } + } + + private struct Door + { + public readonly Rectangle Rect; + + public Door(Rectangle rect) + { + rect.Y = -rect.Y; + Rect = rect; + } + } + + private Dictionary hullCollections; + private List doors; + + + private static SubmarinePreview instance = null; + + public static void Create(SubmarineInfo submarineInfo) + { + Close(); + instance = new SubmarinePreview(submarineInfo); + } + + public static void Close() + { + instance?.Dispose(); + } + + private SubmarinePreview(SubmarineInfo subInfo) + { + camera = new Camera(); + submarineInfo = subInfo; + spriteRecorder = new SpriteRecorder(); + isDisposed = false; + loadTask = null; + + hullCollections = new Dictionary(); + doors = new List(); + + previewFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null); + new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, previewFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); + + new GUIButton(new RectTransform(Vector2.One, previewFrame.RectTransform), "", style: null) + { + OnClicked = (btn, obj) => { Dispose(); return false; } + }; + + var innerFrame = new GUIFrame(new RectTransform(Vector2.One * 0.9f, previewFrame.RectTransform, Anchor.Center)); + int innerPadding = GUI.IntScale(100f); + var innerPadded = new GUIFrame(new RectTransform(new Point(innerFrame.Rect.Width - innerPadding, innerFrame.Rect.Height - innerPadding), previewFrame.RectTransform, Anchor.Center), style: null) + { + OutlineColor = Color.Black, + OutlineThickness = 2 + }; + + GUITextBlock titleText = null; + GUIListBox specsContainer = null; + + new GUICustomComponent(new RectTransform(Vector2.One, innerPadded.RectTransform, Anchor.Center), + (spriteBatch, component) => { + camera.UpdateTransform(interpolate: true, updateListener: false); + Rectangle drawRect = new Rectangle(component.Rect.X + 1, component.Rect.Y + 1, component.Rect.Width - 2, component.Rect.Height - 2); + RenderSubmarine(spriteBatch, drawRect); + }, + (deltaTime, component) => { + bool isMouseOnComponent = GUI.MouseOn == component; + camera.MoveCamera(deltaTime, allowZoom: isMouseOnComponent, followSub: false); + if (isMouseOnComponent && + (PlayerInput.MidButtonHeld() || PlayerInput.LeftButtonHeld())) + { + Vector2 moveSpeed = PlayerInput.MouseSpeed * (float)deltaTime * 60.0f / camera.Zoom; + moveSpeed.X = -moveSpeed.X; + camera.Position += moveSpeed; + } + + if (titleText != null && specsContainer != null) + { + specsContainer.Visible = GUI.IsMouseOn(titleText); + } + }); + + var topContainer = new GUIFrame(new RectTransform(new Vector2(1f, 0.07f), innerPadded.RectTransform, Anchor.TopLeft), style: null) + { + Color = Color.Black * 0.65f + }; + var topLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.97f, 5f / 7f), topContainer.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft); + + titleText = new GUITextBlock(new RectTransform(new Vector2(0.95f, 1f), topLayout.RectTransform), subInfo.DisplayName, font: GUI.LargeFont); + new GUIButton(new RectTransform(new Vector2(0.05f, 1f), topLayout.RectTransform), TextManager.Get("Close")) + { + OnClicked = (btn, obj) => { Dispose(); return false; } + }; + + specsContainer = new GUIListBox(new RectTransform(new Vector2(0.4f, 1f), innerPadded.RectTransform, Anchor.TopLeft) { RelativeOffset = new Vector2(0.015f, 0.07f) }) + { + Color = Color.Black * 0.65f, + ScrollBarEnabled = false, + ScrollBarVisible = false, + Spacing = 5 + }; + subInfo.CreateSpecsWindow(specsContainer, GUI.Font, includeTitle: false, includeDescription: true); + int width = specsContainer.Rect.Width; + void recalculateSpecsContainerHeight() + { + int totalSize = 0; + var children = specsContainer.Content.Children.Where(c => c.Visible); + foreach (GUIComponent child in children) + { + totalSize += child.Rect.Height; + } + totalSize += specsContainer.Content.CountChildren * specsContainer.Spacing; + if (specsContainer.PadBottom) + { + GUIComponent last = specsContainer.Content.Children.LastOrDefault(); + if (last != null) + { + totalSize += specsContainer.Rect.Height - last.Rect.Height; + } + } + specsContainer.RectTransform.Resize(new Point(width, totalSize), true); + specsContainer.RecalculateChildren(); + } + //hell + recalculateSpecsContainerHeight(); + specsContainer.Content.GetAllChildren().ForEach(c => + { + var firstChild = c.Children.FirstOrDefault() as GUITextBlock; + if (firstChild != null) + { + firstChild.CalculateHeightFromText(); firstChild.SetTextPos(); + c.RectTransform.MinSize = new Point(0, firstChild.Rect.Height); + } + c.CalculateHeightFromText(); c.SetTextPos(); + }); + recalculateSpecsContainerHeight(); + + GeneratePreviewMeshes(); + } + + public static void AddToGUIUpdateList() + { + instance?.previewFrame?.AddToGUIUpdateList(); + } + + public Task GeneratePreviewMeshes() + { + if (loadTask != null) { throw new InvalidOperationException("Tried to start SubmarinePreview loadTask more than once!"); } + loadTask = Task.Run(GeneratePreviewMeshesInternal); + return loadTask; + } + + private async Task GeneratePreviewMeshesInternal() + { + await Task.Yield(); + spriteRecorder.Begin(SpriteSortMode.BackToFront); + + HashSet toIgnore = new HashSet(); + + foreach (var subElement in submarineInfo.SubmarineElement.Elements()) + { + switch (subElement.Name.LocalName.ToLowerInvariant()) + { + case "item": + foreach (var component in subElement.Elements()) + { + switch (component.Name.LocalName.ToLowerInvariant()) + { + case "itemcontainer": + ExtractItemContainerIds(component, toIgnore); + break; + case "connectionpanel": + ExtractConnectionPanelLinks(component, toIgnore); + break; + } + } + break; + } + if (isDisposed) { return; } + await Task.Yield(); + } + + foreach (var subElement in submarineInfo.SubmarineElement.Elements()) + { + switch (subElement.Name.LocalName.ToLowerInvariant()) + { + case "item": + if (!toIgnore.Contains(subElement.GetAttributeInt("ID", 0))) + { + BakeMapEntity(subElement); + } + break; + case "structure": + BakeMapEntity(subElement); + break; + case "hull": + string identifier = subElement.GetAttributeString("roomname", "").ToLowerInvariant(); + if (!string.IsNullOrEmpty(identifier)) + { + HullCollection hullCollection = null; + if (!hullCollections.TryGetValue(identifier, out hullCollection)) + { + hullCollection = new HullCollection(identifier); + hullCollections.Add(identifier, hullCollection); + } + hullCollection.AddRect(subElement); + } + break; + } + if (isDisposed) { return; } + await Task.Yield(); + } + spriteRecorder.End(); + + camera.Position = (spriteRecorder.Min + spriteRecorder.Max) * 0.5f; + float scaledSpan = (spriteRecorder.Max - spriteRecorder.Min).X / camera.Resolution.X; + camera.Zoom = 0.8f / scaledSpan; + camera.StopMovement(); + } + + private void ExtractItemContainerIds(XElement component, HashSet ids) + { + string containedString = component.GetAttributeString("contained", ""); + string[] itemIdStrings = containedString.Split(','); + for (int i = 0; i < itemIdStrings.Length; i++) + { + foreach (string idStr in itemIdStrings[i].Split(';')) + { + if (!int.TryParse(idStr, NumberStyles.Any, CultureInfo.InvariantCulture, out int id)) { continue; } + if (id != 0 && !ids.Contains(id)) { ids.Add(id); } + } + } + } + + private void ExtractConnectionPanelLinks(XElement component, HashSet ids) + { + var pins = component.Elements("input").Concat(component.Elements("output")); + foreach (var pin in pins) + { + var links = pin.Elements("link"); + foreach (var link in links) + { + int id = link.GetAttributeInt("w", 0); + if (id != 0 && !ids.Contains(id)) { ids.Add(id); } + } + } + } + + private void BakeMapEntity(XElement element) + { + string identifier = element.GetAttributeString("identifier", ""); + if (string.IsNullOrEmpty(identifier)) { return; } + Rectangle rect = element.GetAttributeRect("rect", Rectangle.Empty); + if (rect.Equals(Rectangle.Empty)) { return; } + + float depth = element.GetAttributeFloat("spritedepth", 1f); + bool flippedX = element.GetAttributeBool("flippedx", false); + bool flippedY = element.GetAttributeBool("flippedy", false); + + float scale = element.GetAttributeFloat("scale", 1f); + Color color = element.GetAttributeColor("spritecolor", Color.White); + + float rotation = element.GetAttributeFloat("rotation", 0f); + + MapEntityPrefab prefab = MapEntityPrefab.List.FirstOrDefault(p => p.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase)); + if (prefab == null) { return; } + + var texture = prefab.sprite.Texture; + var srcRect = prefab.sprite.SourceRect; + + SpriteEffects spriteEffects = SpriteEffects.None; + if (flippedX) + { + spriteEffects |= SpriteEffects.FlipHorizontally; + } + if (flippedY) + { + spriteEffects |= SpriteEffects.FlipVertically; + } + + var prevEffects = prefab.sprite.effects; + prefab.sprite.effects ^= spriteEffects; + + bool overrideSprite = false; + ItemPrefab itemPrefab = prefab as ItemPrefab; + StructurePrefab structurePrefab = prefab as StructurePrefab; + if (itemPrefab != null) + { + BakeItemComponents(itemPrefab, rect, color, scale, rotation, depth, out overrideSprite); + } + + if (!overrideSprite) + { + if (structurePrefab != null) + { + ParseUpgrades(structurePrefab.ConfigElement, ref scale); + + if (!prefab.ResizeVertical) + { + rect.Height = (int)(rect.Height * scale / prefab.Scale); + } + if (!prefab.ResizeHorizontal) + { + rect.Width = (int)(rect.Width * scale / prefab.Scale); + } + var textureScale = element.GetAttributeVector2("texturescale", Vector2.One); + + Vector2 backGroundOffset = Vector2.Zero; + + Vector2 textureOffset = element.GetAttributeVector2("textureoffset", Vector2.Zero); + if (flippedX) { textureOffset.X = -textureOffset.X; } + if (flippedY) { textureOffset.Y = -textureOffset.Y; } + + backGroundOffset = new Vector2( + MathUtils.PositiveModulo((int)-textureOffset.X, prefab.sprite.SourceRect.Width), + MathUtils.PositiveModulo((int)-textureOffset.Y, prefab.sprite.SourceRect.Height)); + + prefab.sprite.DrawTiled( + spriteRecorder, + rect.Location.ToVector2() * new Vector2(1f, -1f), + rect.Size.ToVector2(), + color: color, + startOffset: backGroundOffset, + textureScale: textureScale * scale, + depth: depth); + } + else if (itemPrefab != null) + { + bool usePrefabValues = element.GetAttributeBool("isoverride", false) != itemPrefab.IsOverride; + if (usePrefabValues) + { + scale = itemPrefab.ConfigElement.GetAttributeFloat(scale, "scale", "Scale"); + } + + ParseUpgrades(itemPrefab.ConfigElement, ref scale); + + if (prefab.ResizeVertical || prefab.ResizeHorizontal) + { + if (!prefab.ResizeHorizontal) + { + rect.Width = (int)(prefab.sprite.size.X * scale); + } + if (!prefab.ResizeVertical) + { + rect.Height = (int)(prefab.sprite.size.Y * scale); + } + + var spritePos = rect.Center.ToVector2(); + //spritePos.Y = rect.Height - spritePos.Y; + + prefab.sprite.DrawTiled( + spriteRecorder, + rect.Location.ToVector2() * new Vector2(1f, -1f), + rect.Size.ToVector2(), + color: color, + textureScale: Vector2.One * scale, + depth: depth); + + foreach (var decorativeSprite in itemPrefab.DecorativeSprites) + { + float offsetState = 0f; + Vector2 offset = decorativeSprite.GetOffset(ref offsetState, Vector2.Zero) * scale; + if (flippedX && itemPrefab.CanSpriteFlipX) { offset.X = -offset.X; } + if (flippedY && itemPrefab.CanSpriteFlipY) { offset.Y = -offset.Y; } + decorativeSprite.Sprite.DrawTiled(spriteRecorder, + new Vector2(spritePos.X + offset.X - rect.Width / 2, -(spritePos.Y + offset.Y + rect.Height / 2)), + rect.Size.ToVector2(), color: color, + textureScale: Vector2.One * scale, + depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - prefab.sprite.Depth), 0.999f)); + } + } + else + { + rect.Width = (int)(rect.Width * scale / prefab.Scale); + rect.Height = (int)(rect.Height * scale / prefab.Scale); + + var spritePos = rect.Center.ToVector2(); + spritePos.Y -= rect.Height; + //spritePos.Y = rect.Height - spritePos.Y; + + prefab.sprite.Draw( + spriteRecorder, + spritePos * new Vector2(1f, -1f), + color, + prefab.sprite.Origin, + rotation, + scale, + prefab.sprite.effects, depth); + + foreach (var decorativeSprite in itemPrefab.DecorativeSprites) + { + float rotationState = 0f; float offsetState = 0f; + float rot = decorativeSprite.GetRotation(ref rotationState, 0f); + Vector2 offset = decorativeSprite.GetOffset(ref offsetState, Vector2.Zero) * scale; + if (flippedX && itemPrefab.CanSpriteFlipX) { offset.X = -offset.X; } + if (flippedY && itemPrefab.CanSpriteFlipY) { offset.Y = -offset.Y; } + decorativeSprite.Sprite.Draw(spriteRecorder, new Vector2(spritePos.X + offset.X, -(spritePos.Y + offset.Y)), color, + MathHelper.ToRadians(rotation) + rot, decorativeSprite.GetScale(0f) * scale, prefab.sprite.effects, + depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - prefab.sprite.Depth), 0.999f)); + } + } + } + } + + prefab.sprite.effects = prevEffects; + } + + private void BakeItemComponents( + ItemPrefab prefab, + Rectangle rect, Color color, + float scale, float rotation, float depth, + out bool overrideSprite) + { + overrideSprite = false; + + foreach (var subElement in prefab.ConfigElement.Elements()) + { + switch (subElement.Name.LocalName.ToLowerInvariant()) + { + case "turret": + Sprite barrelSprite = null; + Sprite railSprite = null; + foreach (XElement turretSubElem in subElement.Elements()) + { + switch (turretSubElem.Name.ToString().ToLowerInvariant()) + { + case "barrelsprite": + barrelSprite = new Sprite(turretSubElem); + break; + case "railsprite": + railSprite = new Sprite(turretSubElem); + break; + } + } + + var transformedBarrelPos = MathUtils.RotatePointAroundTarget( + subElement.GetAttributeVector2("barrelpos", Vector2.Zero) * scale, + new Vector2(rect.Width / 2, rect.Height / 2), + MathHelper.ToRadians(rotation)); + + Vector2 drawPos = new Vector2(rect.X + transformedBarrelPos.X, rect.Y - transformedBarrelPos.Y); + drawPos.Y = -drawPos.Y; + + railSprite?.Draw(spriteRecorder, + drawPos, + color, + rotation + MathHelper.PiOver2, scale, + SpriteEffects.None, depth + (railSprite.Depth - prefab.sprite.Depth)); + + barrelSprite?.Draw(spriteRecorder, + drawPos - new Vector2((float)Math.Cos(MathHelper.ToRadians(rotation)), (float)Math.Sin(MathHelper.ToRadians(rotation))) * scale, + color, + rotation + MathHelper.PiOver2, scale, + SpriteEffects.None, depth + (barrelSprite.Depth - prefab.sprite.Depth)); + + break; + case "door": + doors.Add(new Door(rect)); + + var doorSpriteElem = subElement.Elements().FirstOrDefault(e => e.Name.LocalName.Equals("sprite", StringComparison.OrdinalIgnoreCase)); + if (doorSpriteElem != null) + { + string texturePath = doorSpriteElem.GetAttributeString("texture", ""); + Vector2 pos = rect.Location.ToVector2() * new Vector2(1f, -1f); + if (subElement.GetAttributeBool("horizontal", false)) + { + pos.Y += (float)rect.Height * 0.5f; + } + else + { + pos.X += (float)rect.Width * 0.5f; + } + Sprite doorSprite = new Sprite(doorSpriteElem, texturePath.Contains("/") ? "" : Path.GetDirectoryName(prefab.FilePath)); + spriteRecorder.Draw(doorSprite.Texture, pos, + new Rectangle((int)doorSprite.SourceRect.X, + (int)doorSprite.SourceRect.Y, + (int)doorSprite.size.X, (int)doorSprite.size.Y), + color, 0.0f, doorSprite.Origin, new Vector2(scale), SpriteEffects.None, doorSprite.Depth); + } + break; + case "ladder": + var backgroundSprElem = subElement.Elements().FirstOrDefault(e => e.Name.LocalName.Equals("backgroundsprite", StringComparison.OrdinalIgnoreCase)); + if (backgroundSprElem != null) + { + Sprite backgroundSprite = new Sprite(backgroundSprElem); + backgroundSprite.DrawTiled(spriteRecorder, + new Vector2(rect.Left, -rect.Top) - backgroundSprite.Origin * scale, + new Vector2(backgroundSprite.size.X * scale, rect.Height), color: color, + textureScale: Vector2.One * scale, + depth: depth + 0.1f); + } + break; + } + } + } + + public void ParseUpgrades(XElement prefabConfigElement, ref float scale) + { + foreach (var upgrade in prefabConfigElement.Elements("Upgrade")) + { + var upgradeVersion = new Version(upgrade.GetAttributeString("gameversion", "0.0.0.0")); + if (upgradeVersion >= submarineInfo.GameVersion) + { + string scaleModifier = upgrade.GetAttributeString("scale", "*1"); + + if (scaleModifier.StartsWith("*")) + { + if (float.TryParse(scaleModifier.Substring(1), NumberStyles.Any, CultureInfo.InvariantCulture, out float parsedScale)) + { + scale *= parsedScale; + } + } + else + { + if (float.TryParse(scaleModifier, NumberStyles.Any, CultureInfo.InvariantCulture, out float parsedScale)) + { + scale = parsedScale; + } + } + } + } + } + + private void RenderSubmarine(SpriteBatch spriteBatch, Rectangle scissorRectangle) + { + if (spriteRecorder == null) { return; } + + GUI.DrawRectangle(spriteBatch, scissorRectangle, new Color(0.051f, 0.149f, 0.271f, 1.0f), isFilled: true); + + if (!spriteRecorder.ReadyToRender) + { + string waitText = !loadTask.IsCompleted ? + "Generating preview..." : + (loadTask.Exception?.ToString() ?? "Task completed without marking as ready to render"); + Vector2 origin = (GUI.Font.MeasureString(waitText) * 0.5f); + origin.X = MathF.Round(origin.X); + origin.Y = MathF.Round(origin.Y); + GUI.Font.DrawString( + spriteBatch, + waitText, + scissorRectangle.Center.ToVector2(), + Color.White, + 0f, + origin, + 1f, + SpriteEffects.None, + 0f); + return; + } + spriteBatch.End(); + + var prevScissorRect = GameMain.Instance.GraphicsDevice.ScissorRectangle; + GameMain.Instance.GraphicsDevice.ScissorRectangle = scissorRectangle; + + spriteRecorder.Render(camera); + + var mousePos = camera.ScreenToWorld(PlayerInput.MousePosition); + mousePos.Y = -mousePos.Y; + + spriteBatch.Begin(SpriteSortMode.BackToFront, rasterizerState: GameMain.ScissorTestEnable, transformMatrix: camera.Transform); + GameMain.Instance.GraphicsDevice.ScissorRectangle = scissorRectangle; + foreach (var hullCollection in hullCollections.Values) + { + bool mouseOver = false; + + foreach (var rect in hullCollection.Rects) + { + mouseOver = rect.Contains(mousePos); + if (mouseOver) { break; } + } + + foreach (var rect in hullCollection.Rects) + { + GUI.DrawRectangle(spriteBatch, rect, mouseOver ? Color.Red : Color.Blue, depth: mouseOver ? 0.45f : 0.5f, thickness: (mouseOver ? 4f : 2f) / camera.Zoom); + } + + if (mouseOver) + { + string str = hullCollection.Name; + Vector2 strSize = GUI.Font.MeasureString(str) / camera.Zoom; + Vector2 padding = new Vector2(30, 30) / camera.Zoom; + Vector2 shift = new Vector2(10, 0) / camera.Zoom; + + GUI.DrawRectangle(spriteBatch, mousePos + shift, strSize + padding, Color.Black, isFilled: true, depth: 0.25f); + GUI.Font.DrawString(spriteBatch, str, mousePos + shift + (strSize + padding) * 0.5f, Color.White, 0f, strSize * camera.Zoom * 0.5f, 1f / camera.Zoom, SpriteEffects.None, 0f); + } + } + foreach (var door in doors) + { + GUI.DrawRectangle(spriteBatch, door.Rect, GUI.Style.Green * 0.5f, isFilled: true, depth: 0.4f); + } + spriteBatch.End(); + + GameMain.Instance.GraphicsDevice.ScissorRectangle = prevScissorRect; + spriteBatch.Begin(SpriteSortMode.Deferred); + } + + public void Dispose() + { + previewFrame = null; + spriteRecorder?.Dispose(); + isDisposed = true; + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs index 725889e03..a616356c8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs @@ -222,18 +222,20 @@ namespace Barotrauma private bool ChangeSpawnType(GUIButton button, object obj) { GUITextBlock spawnTypeText = button.Parent.GetChildByUserData("spawntypetext") as GUITextBlock; - spawnType += (int)button.UserData; - var values = Enum.GetValues(typeof(SpawnType)); + var values = (SpawnType[])Enum.GetValues(typeof(SpawnType)); + int currIndex = values.IndexOf(spawnType); + currIndex += (int)button.UserData; int firstIndex = 1; int lastIndex = values.Length - 1; - if ((int)spawnType > lastIndex) + if (currIndex > lastIndex) { - spawnType = (SpawnType)firstIndex; + currIndex = firstIndex; } - if ((int)spawnType < firstIndex) + if (currIndex < firstIndex) { - spawnType = (SpawnType)values.GetValue(lastIndex); + currIndex = lastIndex; } + spawnType = values[currIndex]; spawnTypeText.Text = spawnType.ToString(); return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs index c9444afcb..898d39d8c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs @@ -3,12 +3,13 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; +using System.Text; namespace Barotrauma.Networking { partial class BannedPlayer { - public BannedPlayer(string name, UInt16 uniqueIdentifier, bool isRangeBan, string endPoint, ulong steamID) + public BannedPlayer(string name, UInt16 uniqueIdentifier, bool isRangeBan, string endPoint, ulong steamID, string reason, DateTime? expiration) { this.Name = name; this.EndPoint = endPoint; @@ -16,6 +17,8 @@ namespace Barotrauma.Networking ParseEndPointAsSteamId(); this.IsRangeBan = isRangeBan; this.UniqueIdentifier = uniqueIdentifier; + this.Reason = reason; + this.ExpirationTime = expiration; } } @@ -152,12 +155,24 @@ namespace Barotrauma.Networking bannedPlayers.Clear(); UInt32 bannedPlayerCount = incMsg.ReadVariableUInt32(); + for (int i = 0; i < (int)bannedPlayerCount; i++) { string name = incMsg.ReadString(); UInt16 uniqueIdentifier = incMsg.ReadUInt16(); - bool isRangeBan = incMsg.ReadBoolean(); incMsg.ReadPadBits(); - + bool isRangeBan = incMsg.ReadBoolean(); + bool includesExpiration = incMsg.ReadBoolean(); + incMsg.ReadPadBits(); + + DateTime? expiration = null; + if (includesExpiration) + { + double hoursFromNow = incMsg.ReadDouble(); + expiration = DateTime.Now + TimeSpan.FromHours(hoursFromNow); + } + + string reason = incMsg.ReadString(); + string endPoint = ""; UInt64 steamID = 0; if (isOwner) @@ -170,7 +185,7 @@ namespace Barotrauma.Networking endPoint = "Endpoint concealed by host"; steamID = 0; } - bannedPlayers.Add(new BannedPlayer(name, uniqueIdentifier, isRangeBan, endPoint, steamID)); + bannedPlayers.Add(new BannedPlayer(name, uniqueIdentifier, isRangeBan, endPoint, steamID, reason, expiration)); } if (banFrame != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs index f0cf3371c..3a94e246e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs @@ -15,7 +15,7 @@ namespace Barotrauma.Networking public static void ClientRead(IReadMessage msg) { - UInt16 ID = msg.ReadUInt16(); + UInt16 id = msg.ReadUInt16(); ChatMessageType type = (ChatMessageType)msg.ReadByte(); PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None; string txt = ""; @@ -29,6 +29,14 @@ namespace Barotrauma.Networking string senderName = msg.ReadString(); Character senderCharacter = null; + Client senderClient = null; + bool hasSenderClient = msg.ReadBoolean(); + if (hasSenderClient) + { + UInt64 clientId = msg.ReadUInt64(); + senderClient = GameMain.Client.ConnectedClients.Find(c => c.SteamID == clientId || c.ID == clientId); + if (senderClient != null) { senderName = senderClient.Name; } + } bool hasSenderCharacter = msg.ReadBoolean(); if (hasSenderCharacter) { @@ -38,6 +46,7 @@ namespace Barotrauma.Networking senderName = senderCharacter.Name; } } + msg.ReadPadBits(); switch (type) { @@ -48,7 +57,54 @@ namespace Barotrauma.Networking UInt16 targetCharacterID = msg.ReadUInt16(); Character targetCharacter = Entity.FindEntityByID(targetCharacterID) as Character; Entity targetEntity = Entity.FindEntityByID(msg.ReadUInt16()); - int optionIndex = msg.ReadByte(); + + Order orderPrefab = null; + int? optionIndex = null; + string orderOption = null; + + // The option of a Dismiss order is written differently so we know what order we target + // now that the game supports multiple current orders simultaneously + if (orderIndex >= 0 && orderIndex < Order.PrefabList.Count) + { + orderPrefab = Order.PrefabList[orderIndex]; + if (orderPrefab.Identifier != "dismissed") + { + optionIndex = msg.ReadByte(); + } + // Does the dismiss order have a specified target? + else if (msg.ReadBoolean()) + { + int identifierCount = msg.ReadByte(); + if (identifierCount > 0) + { + int dismissedOrderIndex = msg.ReadByte(); + Order dismissedOrderPrefab = null; + if (dismissedOrderIndex >= 0 && dismissedOrderIndex < Order.PrefabList.Count) + { + dismissedOrderPrefab = Order.PrefabList[dismissedOrderIndex]; + orderOption = dismissedOrderPrefab.Identifier; + } + if (identifierCount > 1) + { + int dismissedOrderOptionIndex = msg.ReadByte(); + if (dismissedOrderPrefab != null) + { + var options = dismissedOrderPrefab.Options; + if (options != null && dismissedOrderOptionIndex >= 0 && dismissedOrderOptionIndex < options.Length) + { + orderOption += $".{options[dismissedOrderOptionIndex]}"; + } + } + } + } + } + } + else + { + optionIndex = msg.ReadByte(); + } + + int orderPriority = msg.ReadByte(); OrderTarget orderTargetPosition = null; Order.OrderTargetType orderTargetType = (Order.OrderTargetType)msg.ReadByte(); int wallSectionIndex = 0; @@ -64,22 +120,18 @@ namespace Barotrauma.Networking wallSectionIndex = msg.ReadByte(); } - Order orderPrefab; if (orderIndex < 0 || orderIndex >= Order.PrefabList.Count) { DebugConsole.ThrowError("Invalid order message - order index out of bounds."); - if (NetIdUtils.IdMoreRecent(ID, LastID)) { LastID = ID; } + if (NetIdUtils.IdMoreRecent(id, LastID)) { LastID = id; } return; } else { - orderPrefab = Order.PrefabList[orderIndex]; - } - string orderOption = ""; - if (optionIndex >= 0 && optionIndex < orderPrefab.Options.Length) - { - orderOption = orderPrefab.Options[optionIndex]; + orderPrefab ??= Order.PrefabList[orderIndex]; } + + orderOption ??= optionIndex.HasValue && optionIndex >= 0 && optionIndex < orderPrefab.Options.Length ? orderPrefab.Options[optionIndex.Value] : ""; txt = orderPrefab.GetChatMessage(targetCharacter?.Name, senderCharacter?.CurrentHull?.DisplayName, givingOrderToSelf: targetCharacter == senderCharacter, orderOption: orderOption); if (GameMain.Client.GameStarted && Screen.Selected == GameMain.GameScreen) @@ -107,16 +159,16 @@ namespace Barotrauma.Networking } else if (targetCharacter != null) { - targetCharacter.SetOrder(order, orderOption, senderCharacter); + targetCharacter.SetOrder(order, orderOption, orderPriority, senderCharacter); } } } - if (NetIdUtils.IdMoreRecent(ID, LastID)) + if (NetIdUtils.IdMoreRecent(id, LastID)) { GameMain.Client.AddChatMessage( - new OrderChatMessage(orderPrefab, orderOption, txt, orderTargetPosition ?? targetEntity as ISpatialEntity, targetCharacter, senderCharacter)); - LastID = ID; + new OrderChatMessage(orderPrefab, orderOption, orderPriority, txt, orderTargetPosition ?? targetEntity as ISpatialEntity, targetCharacter, senderCharacter)); + LastID = id; } return; case ChatMessageType.ServerMessageBox: @@ -128,7 +180,7 @@ namespace Barotrauma.Networking break; } - if (NetIdUtils.IdMoreRecent(ID, LastID)) + if (NetIdUtils.IdMoreRecent(id, LastID)) { switch (type) { @@ -154,10 +206,10 @@ namespace Barotrauma.Networking GameMain.Client.ServerSettings.ServerLog?.WriteLine(txt, messageType); break; default: - GameMain.Client.AddChatMessage(txt, type, senderName, senderCharacter, changeType); + GameMain.Client.AddChatMessage(txt, type, senderName, senderClient, senderCharacter, changeType); break; } - LastID = ID; + LastID = id; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs index 93b716257..bd3111221 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs @@ -114,7 +114,7 @@ namespace Barotrauma.Networking return; } Permissions = permissions; - PermittedConsoleCommands = new List(permittedConsoleCommands); + PermittedConsoleCommands.Clear(); PermittedConsoleCommands.AddRange(permittedConsoleCommands); } public void GivePermission(ClientPermissions permission) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 0d9f1ab69..4cf5cde4c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -56,6 +56,11 @@ namespace Barotrauma.Networking public readonly NetStats NetStats; protected GUITickBox cameraFollowsSub; + public GUITickBox FollowSubTickBox => cameraFollowsSub; + + public bool IsFollowSubTickBoxVisible => + gameStarted && Screen.Selected == GameMain.GameScreen && + cameraFollowsSub != null && cameraFollowsSub.Visible; public CameraTransition EndCinematic; @@ -159,6 +164,12 @@ namespace Barotrauma.Networking get { return entityEventManager; } } + public bool? WaitForNextRoundRespawn + { + get; + set; + } + private readonly object serverEndpoint; private readonly int ownerKey; private readonly bool steamP2POwner; @@ -185,10 +196,10 @@ namespace Barotrauma.Networking CanBeFocused = false }; - cameraFollowsSub = new GUITickBox(new RectTransform(new Vector2(0.05f, 0.05f), inGameHUD.RectTransform, anchor: Anchor.TopCenter) + cameraFollowsSub = new GUITickBox(new RectTransform(new Vector2(0.05f, 0.05f), inGameHUD.RectTransform, anchor: Anchor.TopCenter, pivot: Pivot.CenterLeft) { - AbsoluteOffset = new Point(0, 5), - MaxSize = new Point(25, 25) + AbsoluteOffset = new Point(0, HUDLayoutSettings.ButtonAreaTop.Y + HUDLayoutSettings.ButtonAreaTop.Height / 2), + MaxSize = new Point(GUI.IntScale(25)) }, TextManager.Get("CamFollowSubmarine")) { Selected = Camera.FollowSub, @@ -857,12 +868,25 @@ namespace Barotrauma.Networking string endMessage = string.Empty; endMessage = inc.ReadString(); - bool missionSuccessful = inc.ReadBoolean(); + byte missionCount = inc.ReadByte(); + for (int i = 0; i < missionCount; i++) + { + bool missionSuccessful = inc.ReadBoolean(); + var mission = GameMain.GameSession?.GetMission(i); + if (mission != null) + { + mission.Completed = missionSuccessful; + } + } CharacterTeamType winningTeam = (CharacterTeamType)inc.ReadByte(); - if (missionSuccessful && GameMain.GameSession?.Mission != null) + if (winningTeam != CharacterTeamType.None) { GameMain.GameSession.WinningTeam = winningTeam; - GameMain.GameSession.Mission.Completed = true; + var combatMission = GameMain.GameSession.Missions.FirstOrDefault(m => m is CombatMission); + if (combatMission != null) + { + combatMission.Completed = true; + } } byte traitorCount = inc.ReadByte(); @@ -928,7 +952,11 @@ namespace Barotrauma.Networking ReadTraitorMessage(inc); break; case ServerPacketHeader.MISSION: - GameMain.GameSession?.Mission?.ClientRead(inc); + { + int missionIndex = inc.ReadByte(); + Mission mission = GameMain.GameSession?.GetMission(missionIndex); + mission?.ClientRead(inc); + } break; case ServerPacketHeader.EVENTACTION: GameMain.GameSession?.EventManager.ClientRead(inc); @@ -959,17 +987,26 @@ namespace Barotrauma.Networking throw new Exception(errorMsg); } - string missionIdentifier = inc.ReadString() ?? ""; - if (missionIdentifier != (GameMain.GameSession.Mission?.Prefab.Identifier ?? "")) + byte missionCount = inc.ReadByte(); + if (missionCount != GameMain.GameSession.Missions.Count()) { - string errorMsg = $"Mission equality check failed. The mission selected at your end doesn't match the one loaded by the server (server: {missionIdentifier ?? "null"}, client: {GameMain.GameSession.Mission?.Prefab.Identifier ?? ""})"; - GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:MissionsDontMatch" + Level.Loaded.Seed, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + string errorMsg = $"Mission equality check failed. Mission count doesn't match the server (server: {missionCount}, client: {GameMain.GameSession.Missions.Count()})"; throw new Exception(errorMsg); } + foreach (Mission mission in GameMain.GameSession.Missions) + { + string missionIdentifier = inc.ReadString() ?? ""; + if (missionIdentifier != mission.Prefab.Identifier) + { + string errorMsg = $"Mission equality check failed. The mission selected at your end doesn't match the one loaded by the server (server: {missionIdentifier ?? "null"}, client: {mission.Prefab.Identifier})"; + GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:MissionsDontMatch" + Level.Loaded.Seed, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + throw new Exception(errorMsg); + } + } byte equalityCheckValueCount = inc.ReadByte(); List levelEqualityCheckValues = new List(); - for (int i = 0; i missionIndices = new List(); + int missionCount = inc.ReadByte(); + for (int i = 0; i < missionCount; i++) + { + missionIndices.Add(inc.ReadInt16()); + } if (!GameMain.NetLobbyScreen.TrySelectSub(subName, subHash, GameMain.NetLobbyScreen.SubList)) { roundInitStatus = RoundInitStatus.Interrupted; @@ -1486,7 +1534,9 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Failure; } - GameMain.GameSession = new GameSession(GameMain.NetLobbyScreen.SelectedSub, gameMode, missionPrefab: missionIndex < 0 ? null : MissionPrefab.List[missionIndex]); + var selectedMissions = missionIndices.Select(i => MissionPrefab.List[i]); + + GameMain.GameSession = new GameSession(GameMain.NetLobbyScreen.SelectedSub, gameMode, missionPrefabs: selectedMissions); GameMain.GameSession.StartRound(levelSeed, levelDifficulty); } else @@ -1653,13 +1703,15 @@ namespace Barotrauma.Networking } foreach (Submarine sub in Submarine.MainSubs[i].DockedTo) { + if (sub.Info.Type == SubmarineType.Outpost) { continue; } sub.TeamID = teamID; } } - if (respawnAllowed) - { - respawnManager = new RespawnManager(this, GameMain.NetLobbyScreen.UsingShuttle && gameMode != GameModePreset.MultiPlayerCampaign ? GameMain.NetLobbyScreen.SelectedShuttle : null); + if (respawnAllowed) + { + bool isOutpost = GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign && Level.Loaded?.Type == LevelData.LevelType.Outpost; + respawnManager = new RespawnManager(this, GameMain.NetLobbyScreen.UsingShuttle && !isOutpost ? GameMain.NetLobbyScreen.SelectedShuttle : null); } gameStarted = true; @@ -1702,6 +1754,7 @@ namespace Barotrauma.Networking gameStarted = false; Character.Controlled = null; + WaitForNextRoundRespawn = null; SpawnAsTraitor = false; GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; GameMain.LightManager.LosEnabled = false; @@ -1712,7 +1765,7 @@ namespace Barotrauma.Networking // Enable characters near the main sub for the endCinematic foreach (Character c in Character.CharacterList) { - if (Vector2.DistanceSquared(Submarine.MainSub.WorldPosition, c.WorldPosition) < NetConfig.EnableCharacterDistSqr) + if (Vector2.DistanceSquared(Submarine.MainSub.WorldPosition, c.WorldPosition) < MathUtils.Pow2(c.Params.DisableDistance)) { c.Enabled = true; } @@ -1757,8 +1810,10 @@ namespace Barotrauma.Networking var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.Hash == subHash); if (matchingSub == null) { - matchingSub = new SubmarineInfo(Path.Combine(SubmarineInfo.SavePath, subName) + ".sub", subHash, tryLoad: false); - matchingSub.SubmarineClass = (SubmarineClass)subClass; + matchingSub = new SubmarineInfo(Path.Combine(SubmarineInfo.SavePath, subName) + ".sub", subHash, tryLoad: false) + { + SubmarineClass = (SubmarineClass)subClass + }; } matchingSub.RequiredContentPackagesInstalled = requiredContentPackagesInstalled; ServerSubmarines.Add(matchingSub); @@ -1928,14 +1983,19 @@ namespace Barotrauma.Networking } foreach (Client client in ConnectedClients) { - if (!previouslyConnectedClients.Any(c => c.ID == client.ID)) + int index = previouslyConnectedClients.FindIndex(c => c.ID == client.ID); + if (index < 0) { - while (previouslyConnectedClients.Count > 100) + if (previouslyConnectedClients.Count > 100) { - previouslyConnectedClients.RemoveAt(0); + previouslyConnectedClients.RemoveRange(0, previouslyConnectedClients.Count - 100); } - previouslyConnectedClients.Add(client); } + else + { + previouslyConnectedClients.RemoveAt(index); + } + previouslyConnectedClients.Add(client); } if (updateClientListId) { LastClientListUpdateID = listId; } @@ -2028,6 +2088,8 @@ namespace Barotrauma.Networking bool autoRestartEnabled = inc.ReadBoolean(); float autoRestartTimer = autoRestartEnabled ? inc.ReadSingle() : 0.0f; + bool radiationEnabled = inc.ReadBoolean(); + //ignore the message if we already a more up-to-date one //or if we're still waiting for the initial update if (NetIdUtils.IdMoreRecent(updateID, GameMain.NetLobbyScreen.LastUpdateID) && @@ -2038,8 +2100,14 @@ namespace Barotrauma.Networking serverSettings.ClientRead(settingsBuf); if (!IsServerOwner) { - ServerInfo info = GameMain.ServerListScreen.UpdateServerInfoWithServerSettings(serverEndpoint, serverSettings); + ServerInfo info = serverSettings.GetServerListInfo(); GameMain.ServerListScreen.AddToRecentServers(info); + GameMain.NetLobbyScreen.Favorite.Visible = true; + GameMain.NetLobbyScreen.Favorite.Selected = GameMain.ServerListScreen.IsFavorite(info); + } + else + { + GameMain.NetLobbyScreen.Favorite.Visible = false; } GameMain.NetLobbyScreen.LastUpdateID = updateID; @@ -2083,8 +2151,9 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.SetAllowSpectating(allowSpectating); GameMain.NetLobbyScreen.LevelSeed = levelSeed; GameMain.NetLobbyScreen.SetLevelDifficulty(levelDifficulty); - GameMain.NetLobbyScreen.SetBotCount(botCount); + GameMain.NetLobbyScreen.SetRadiationEnabled(radiationEnabled); GameMain.NetLobbyScreen.SetBotSpawnMode(botSpawnMode); + GameMain.NetLobbyScreen.SetBotCount(botCount); GameMain.NetLobbyScreen.SetAutoRestart(autoRestartEnabled, autoRestartTimer); serverSettings.VoiceChatEnabled = voiceChatEnabled; @@ -2325,7 +2394,7 @@ namespace Barotrauma.Networking if (outmsg.LengthBytes > MsgConstants.MTU) { - DebugConsole.ThrowError("Maximum packet size exceeded (" + outmsg.LengthBytes + " > " + MsgConstants.MTU); + DebugConsole.ThrowError($"Maximum packet size exceeded ({outmsg.LengthBytes} > {MsgConstants.MTU})"); } clientPeer.Send(outmsg, DeliveryMethod.Unreliable); @@ -2376,7 +2445,7 @@ namespace Barotrauma.Networking if (outmsg.LengthBytes > MsgConstants.MTU) { - DebugConsole.ThrowError("Maximum packet size exceeded (" + outmsg.LengthBytes + " > " + MsgConstants.MTU); + DebugConsole.ThrowError($"Maximum packet size exceeded ({outmsg.LengthBytes} > {MsgConstants.MTU})"); } clientPeer.Send(outmsg, DeliveryMethod.Unreliable); @@ -2406,6 +2475,15 @@ namespace Barotrauma.Networking chatMsgQueue.Add(chatMessage); } + public void SendRespawnPromptResponse(bool waitForNextRoundRespawn) + { + WaitForNextRoundRespawn = waitForNextRoundRespawn; + IWriteMessage msg = new WriteOnlyMessage(); + msg.Write((byte)ClientPacketHeader.READY_TO_SPAWN); + msg.Write((bool)waitForNextRoundRespawn); + clientPeer?.Send(msg, DeliveryMethod.Reliable); + } + public void RequestFile(FileTransferType fileType, string file, string fileHash) { IWriteMessage msg = new WriteOnlyMessage(); @@ -2528,7 +2606,7 @@ namespace Barotrauma.Networking if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.CampaignID != campaignID) { string savePath = transfer.FilePath; - GameMain.GameSession = new GameSession(null, savePath, GameModePreset.MultiPlayerCampaign); + GameMain.GameSession = new GameSession(null, savePath, GameModePreset.MultiPlayerCampaign, CampaignSettings.Unsure); campaign = (MultiPlayerCampaign)GameMain.GameSession.GameMode; campaign.CampaignID = campaignID; GameMain.NetLobbyScreen.ToggleCampaignMode(true); @@ -2876,7 +2954,7 @@ namespace Barotrauma.Networking clientPeer.Send(msg, DeliveryMethod.Reliable); } - public void SetupNewCampaign(SubmarineInfo sub, string saveName, string mapSeed) + public void SetupNewCampaign(SubmarineInfo sub, string saveName, string mapSeed, CampaignSettings settings) { GameMain.NetLobbyScreen.CampaignSetupFrame.Visible = false; GameMain.NetLobbyScreen.CampaignFrame.Visible = false; @@ -2891,6 +2969,7 @@ namespace Barotrauma.Networking msg.Write(mapSeed); msg.Write(sub.Name); msg.Write(sub.MD5Hash.Hash); + settings.Serialize(msg); clientPeer.Send(msg, DeliveryMethod.Reliable); } @@ -3096,11 +3175,11 @@ namespace Barotrauma.Networking cameraFollowsSub.Visible = Character.Controlled == null; } - if (Character.Controlled == null || Character.Controlled.IsDead) + /*if (Character.Controlled == null || Character.Controlled.IsDead) { GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; GameMain.LightManager.LosEnabled = false; - } + }*/ } //tab doesn't autoselect the chatbox when debug console is open, @@ -3208,9 +3287,13 @@ namespace Barotrauma.Networking if (respawnManager != null) { - string respawnText = ""; - float textScale = 1.0f; + string respawnText = string.Empty; Color textColor = Color.White; + bool canChooseRespawn = + GameMain.GameSession.GameMode is CampaignMode && + Character.Controlled == null && + Level.Loaded?.Type != LevelData.LevelType.Outpost && + (characterInfo == null || HasSpawned); if (respawnManager.CurrentState == RespawnManager.State.Waiting && respawnManager.RespawnCountdownStarted) { @@ -3228,18 +3311,18 @@ namespace Barotrauma.Networking { //oscillate between 0-1 float phase = (float)(Math.Sin(timeLeft * MathHelper.Pi) + 1.0f) * 0.5f; - textScale = 1.0f + phase * 0.5f; + //textScale = 1.0f + phase * 0.5f; textColor = Color.Lerp(GUI.Style.Red, Color.White, 1.0f - phase); } + canChooseRespawn = false; } - - if (!string.IsNullOrEmpty(respawnText)) - { - GUI.SmallFont.DrawString(spriteBatch, respawnText, new Vector2(120.0f, 10), textColor, 0.0f, Vector2.Zero, textScale, Microsoft.Xna.Framework.Graphics.SpriteEffects.None, 0.0f); - } + + GameMain.GameSession?.SetRespawnInfo( + visible: !string.IsNullOrEmpty(respawnText) || canChooseRespawn, text: respawnText, textColor: textColor, + buttonsVisible: canChooseRespawn, waitForNextRoundRespawn: (WaitForNextRoundRespawn ?? true)); } - if (!ShowNetStats) return; + if (!ShowNetStats) { return; } NetStats.Draw(spriteBatch, new Rectangle(300, 10, 300, 150)); @@ -3367,9 +3450,12 @@ namespace Barotrauma.Networking { var banReasonPrompt = new GUIMessageBox( TextManager.Get(ban ? "BanReasonPrompt" : "KickReasonPrompt"), - "", new string[] { TextManager.Get("OK"), TextManager.Get("Cancel") }, new Vector2(0.25f, 0.22f), new Point(400, 220)); + "", new string[] { TextManager.Get("OK"), TextManager.Get("Cancel") }, new Vector2(0.25f, 0.25f), new Point(400, 260)); - var content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.6f), banReasonPrompt.InnerFrame.RectTransform, Anchor.Center)); + var content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.6f), banReasonPrompt.InnerFrame.RectTransform, Anchor.Center)) + { + AbsoluteSpacing = GUI.IntScale(5) + }; var banReasonBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.3f), content.RectTransform)) { Wrap = true, @@ -3380,10 +3466,9 @@ namespace Barotrauma.Networking GUITickBox permaBanTickBox = null; if (ban) - { - + { var labelContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.25f), content.RectTransform), isHorizontal: false); - new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), labelContainer.RectTransform), TextManager.Get("BanDuration")) { Padding = Vector4.Zero }; + new GUITextBlock(new RectTransform(new Vector2(1f, 0.0f), labelContainer.RectTransform), TextManager.Get("BanDuration"), font: GUI.SubHeadingFont) { Padding = Vector4.Zero }; var buttonContent = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), labelContainer.RectTransform), isHorizontal: true); permaBanTickBox = new GUITickBox(new RectTransform(new Vector2(0.4f, 0.15f), buttonContent.RectTransform), TextManager.Get("BanPermanent")) { @@ -3494,12 +3579,19 @@ namespace Barotrauma.Networking errorLines.Add("Campaign ID: " + campaign.CampaignID); errorLines.Add("Campaign save ID: " + campaign.LastSaveID + "(pending: " + campaign.PendingSaveID + ")"); } - errorLines.Add("Mission: " + (GameMain.GameSession?.Mission?.Prefab.Identifier ?? "none")); + foreach (Mission mission in GameMain.GameSession.Missions) + { + errorLines.Add("Mission: " + mission.Prefab.Identifier); + } } if (GameMain.GameSession?.Submarine != null) { errorLines.Add("Submarine: " + GameMain.GameSession.Submarine.Info.Name); } + if (GameMain.NetworkMember?.RespawnManager?.RespawnShuttle != null) + { + errorLines.Add("Respawn shuttle: " + GameMain.NetworkMember.RespawnManager.RespawnShuttle.Info.Name); + } if (Level.Loaded != null) { errorLines.Add("Level: " + Level.Loaded.Seed + ", " + string.Join(", ", Level.Loaded.EqualityCheckValues.Select(cv => cv.ToString("X")))); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs index 172b124ca..82ce2130f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs @@ -1,6 +1,4 @@ -using System; - -namespace Barotrauma.Networking +namespace Barotrauma.Networking { partial class OrderChatMessage : ChatMessage { @@ -9,26 +7,7 @@ namespace Barotrauma.Networking msg.Write((byte)ClientNetObject.CHAT_MESSAGE); msg.Write(NetStateID); msg.Write((byte)ChatMessageType.Order); - msg.Write((byte)Order.PrefabList.IndexOf(Order.Prefab)); - msg.Write(TargetCharacter == null ? (UInt16)0 : TargetCharacter.ID); - msg.Write(TargetEntity is Entity ? (TargetEntity as Entity).ID : (UInt16)0); - msg.Write((byte)Array.IndexOf(Order.Prefab.Options, OrderOption)); - msg.Write((byte)Order.TargetType); - if (Order.TargetType == Order.OrderTargetType.Position && TargetEntity is OrderTarget orderTarget) - { - msg.Write(true); - msg.Write(orderTarget.Position.X); - msg.Write(orderTarget.Position.Y); - msg.Write(orderTarget.Hull == null ? (UInt16)0 : orderTarget.Hull.ID); - } - else - { - msg.Write(false); - if (Order.TargetType == Order.OrderTargetType.WallSection) - { - msg.Write((byte)(WallSectionIndex ?? Order.WallSectionIndex ?? 0)); - } - } + WriteOrder(msg); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs index 3e1a48b58..3f4f5ab1c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs @@ -147,13 +147,36 @@ namespace Barotrauma.Networking if (missingPackages.Count > 0) { var nonDownloadable = missingPackages.Where(p => p.WorkshopId == 0); + var mismatchedButDownloaded = missingPackages.Where(p => + { + var localMatching = ContentPackage.RegularPackages.Find(l => l.SteamWorkshopId != 0 && p.WorkshopId == l.SteamWorkshopId); + localMatching ??= ContentPackage.CorePackages.Find(l => l.SteamWorkshopId != 0 && p.WorkshopId == l.SteamWorkshopId); - if (nonDownloadable.Any()) + return localMatching != null; + }); + + if (mismatchedButDownloaded.Any()) + { + string disconnectMsg; + if (mismatchedButDownloaded.Count() == 1) + { + disconnectMsg = $"DisconnectMessage.MismatchedWorkshopMod~[incompatiblecontentpackage]={GetPackageStr(mismatchedButDownloaded.First())}"; + } + else + { + List packageStrs = new List(); + mismatchedButDownloaded.ForEach(cp => packageStrs.Add(GetPackageStr(cp))); + disconnectMsg = $"DisconnectMessage.MismatchedWorkshopMods~[incompatiblecontentpackages]={string.Join(", ", packageStrs)}"; + } + Close(disconnectMsg, disableReconnect: true); + OnDisconnectMessageReceived?.Invoke(DisconnectReason.MissingContentPackage + "/" + disconnectMsg); + } + else if (nonDownloadable.Any()) { string disconnectMsg; if (nonDownloadable.Count() == 1) { - disconnectMsg = $"DisconnectMessage.MissingContentPackage~[missingcontentpackage]={GetPackageStr(missingPackages[0])}"; + disconnectMsg = $"DisconnectMessage.MissingContentPackage~[missingcontentpackage]={GetPackageStr(nonDownloadable.First())}"; } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs index cad9bc5ec..76974601b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs @@ -56,6 +56,7 @@ namespace Barotrauma.Networking Steamworks.SteamNetworking.AllowP2PPacketRelay(true); ServerConnection = new SteamP2PConnection("Server", hostSteamId); + ServerConnection.SetOwnerSteamIDIfUnknown(hostSteamId); incomingInitializationMessages = new List(); incomingDataMessages = new List(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs index d6f35f5bf..0cbcc298c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs @@ -17,6 +17,7 @@ namespace Barotrauma.Networking class RemotePeer { public UInt64 SteamID; + public UInt64 OwnerSteamID; public double? DisconnectTime; public bool Authenticating; public bool Authenticated; @@ -31,6 +32,7 @@ namespace Barotrauma.Networking public RemotePeer(UInt64 steamId) { SteamID = steamId; + OwnerSteamID = 0; DisconnectTime = null; Authenticating = false; Authenticated = false; @@ -90,10 +92,19 @@ namespace Barotrauma.Networking if (status == Steamworks.AuthResponse.OK) { + remotePeer.OwnerSteamID = ownerID; remotePeer.Authenticated = true; remotePeer.Authenticating = false; foreach (var msg in remotePeer.UnauthedMessages) { + //rewrite the owner id before + //forwarding the messages to + //the server, since it's only + //known now + int prevBitPosition = msg.Message.BitPosition; + msg.Message.BitPosition = sizeof(ulong) * 8; + msg.Message.Write(ownerID); + msg.Message.BitPosition = prevBitPosition; byte[] msgToSend = (byte[])msg.Message.Buffer.Clone(); Array.Resize(ref msgToSend, msg.Message.LengthBytes); ChildServerRelay.Write(msgToSend); @@ -131,6 +142,7 @@ namespace Barotrauma.Networking IWriteMessage outMsg = new WriteOnlyMessage(); outMsg.Write(steamId); + outMsg.Write(remotePeer.OwnerSteamID); outMsg.Write(data, 1, dataLength - 1); DeliveryMethod deliveryMethod = (DeliveryMethod)data[0]; @@ -142,34 +154,27 @@ namespace Barotrauma.Networking bool isServerMessage = (incByte & (byte)PacketHeader.IsServerMessage) != 0; bool isHeartbeatMessage = (incByte & (byte)PacketHeader.IsHeartbeatMessage) != 0; - if (!remotePeer.Authenticated) + if (!remotePeer.Authenticated & !remotePeer.Authenticating && isConnectionInitializationStep) { - if (!remotePeer.Authenticating) + remotePeer.DisconnectTime = null; + + IReadMessage authMsg = new ReadOnlyMessage(data, isCompressed, 2, dataLength - 2, null); + ConnectionInitialization initializationStep = (ConnectionInitialization)authMsg.ReadByte(); + if (initializationStep == ConnectionInitialization.SteamTicketAndVersion) { - if (isConnectionInitializationStep) + remotePeer.Authenticating = true; + + authMsg.ReadString(); //skip name + authMsg.ReadInt32(); //skip owner key + authMsg.ReadUInt64(); //skip steamid + UInt16 ticketLength = authMsg.ReadUInt16(); + byte[] ticket = authMsg.ReadBytes(ticketLength); + + Steamworks.BeginAuthResult authSessionStartState = Steam.SteamManager.StartAuthSession(ticket, steamId); + if (authSessionStartState != Steamworks.BeginAuthResult.OK) { - remotePeer.DisconnectTime = null; - - IReadMessage authMsg = new ReadOnlyMessage(data, isCompressed, 2, dataLength - 2, null); - ConnectionInitialization initializationStep = (ConnectionInitialization)authMsg.ReadByte(); - //Console.WriteLine("received init step from "+steamId.ToString()+" ("+initializationStep.ToString()+")"); - if (initializationStep == ConnectionInitialization.SteamTicketAndVersion) - { - remotePeer.Authenticating = true; - - authMsg.ReadString(); //skip name - authMsg.ReadInt32(); //skip owner key - authMsg.ReadUInt64(); //skip steamid - UInt16 ticketLength = authMsg.ReadUInt16(); - byte[] ticket = authMsg.ReadBytes(ticketLength); - - Steamworks.BeginAuthResult authSessionStartState = Steam.SteamManager.StartAuthSession(ticket, steamId); - if (authSessionStartState != Steamworks.BeginAuthResult.OK) - { - DisconnectPeer(remotePeer, DisconnectReason.SteamAuthenticationFailed.ToString() + "/ Steam auth session failed to start: " + authSessionStartState.ToString()); - return; - } - } + DisconnectPeer(remotePeer, DisconnectReason.SteamAuthenticationFailed.ToString() + "/ Steam auth session failed to start: " + authSessionStartState.ToString()); + return; } } } @@ -336,6 +341,7 @@ namespace Barotrauma.Networking { IWriteMessage outMsg = new WriteOnlyMessage(); outMsg.Write(selfSteamID); + outMsg.Write(selfSteamID); outMsg.Write((byte)(PacketHeader.IsConnectionInitializationStep)); outMsg.Write(Name); @@ -428,6 +434,7 @@ namespace Barotrauma.Networking byte[] msgData = new byte[msg.LengthBytes]; msg.PrepareForSending(ref msgData, out bool isCompressed, out int length); msgToSend.Write(selfSteamID); + msgToSend.Write(selfSteamID); msgToSend.Write((byte)(isCompressed ? PacketHeader.IsCompressed : PacketHeader.None)); msgToSend.Write((UInt16)length); msgToSend.Write(msgData, 0, length); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs index b3e012d75..931cd5d44 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs @@ -159,6 +159,20 @@ namespace Barotrauma.Networking color: ServerListScreen.PlayStyleColors[(int)playStyle], style: "GUISlopedHeader"); playStyleName.RectTransform.NonScaledSize = (playStyleName.Font.MeasureString(playStyleName.Text) + new Vector2(20, 5) * GUI.Scale).ToPoint(); playStyleName.RectTransform.IsFixedSize = true; + + var serverTypeContainer = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.2f), playStyleBanner.RectTransform, Anchor.BottomLeft, Pivot.BottomLeft), + "MainMenuNotifBackground", Color.Black) + { + CanBeFocused = false, + }; + + var serverType = new GUITextBlock(new RectTransform(Vector2.One, serverTypeContainer.RectTransform, Anchor.CenterLeft), + TextManager.Get((OwnerID != 0 || LobbyID != 0) ? "SteamP2PServer" : "DedicatedServer"), textAlignment: Alignment.CenterLeft); + } + else + { + var serverType = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), previewContainer.RectTransform, Anchor.CenterLeft), + TextManager.Get((OwnerID != 0 || LobbyID != 0) ? "SteamP2PServer" : "DedicatedServer"), textAlignment: Alignment.CenterLeft); } var content = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.6f), previewContainer.RectTransform)) @@ -553,5 +567,10 @@ namespace Barotrauma.Networking (other.LobbyID == LobbyID || other.LobbyID == 0 || LobbyID == 0) && ((OwnerID == 0) ? (other.IP == IP && other.Port == Port) : true); } + + public bool MatchesByEndpoint(ServerInfo other) + { + return OwnerID == other.OwnerID && (OwnerID != 0 ? true : (IP == other.IP && Port == other.Port)); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs index b4e942b20..03e0618e0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs @@ -242,16 +242,7 @@ namespace Barotrauma.Networking textBlock.ClickableAreas.Add(new GUITextBlock.ClickableArea() { Data = data, - OnClick = (component, area) => - { - if (!UInt64.TryParse(area.Data.Metadata, out UInt64 id)) { return; } - Client client = GameMain.Client.ConnectedClients.Find(c => c.SteamID == id) - ?? GameMain.Client.ConnectedClients.Find(c => c.ID == id) - ?? GameMain.Client.PreviouslyConnectedClients.FirstOrDefault(c => c.SteamID == id) - ?? GameMain.Client.PreviouslyConnectedClients.FirstOrDefault(c => c.ID == id); - if (client == null) { return; } - GameMain.NetLobbyScreen.SelectPlayer(client); - } + OnClick = GameMain.NetLobbyScreen.SelectPlayer }); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index 313e4a394..341085ff1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -126,11 +126,15 @@ namespace Barotrauma.Networking public void ClientRead(IReadMessage incMsg) { + cachedServerListInfo = null; + ServerName = incMsg.ReadString(); ServerMessageText = incMsg.ReadString(); MaxPlayers = incMsg.ReadByte(); HasPassword = incMsg.ReadBoolean(); IsPublic = incMsg.ReadBoolean(); + GameMain.NetLobbyScreen.SetPublic(IsPublic); + AllowFileTransfers = incMsg.ReadBoolean(); incMsg.ReadPadBits(); TickRate = incMsg.ReadRangedInteger(1, 60); GameMain.NetworkMember.TickRate = TickRate; @@ -147,7 +151,7 @@ namespace Barotrauma.Networking } } - public void ClientAdminWrite(NetFlags dataToSend, int? missionTypeOr = null, int? missionTypeAnd = null, float? levelDifficulty = null, bool? autoRestart = null, int traitorSetting = 0, int botCount = 0, int botSpawnMode = 0, bool? useRespawnShuttle = null) + public void ClientAdminWrite(NetFlags dataToSend, int? missionTypeOr = null, int? missionTypeAnd = null, float? levelDifficulty = null, bool? autoRestart = null, int traitorSetting = 0, int botCount = 0, int botSpawnMode = 0, bool? radiationEnabled = null, bool? useRespawnShuttle = null) { if (!GameMain.Client.HasPermission(Networking.ClientPermissions.ManageSettings)) return; @@ -212,6 +216,7 @@ namespace Barotrauma.Networking outMsg.Write(autoRestart != null); outMsg.Write(autoRestart ?? false); + outMsg.Write(radiationEnabled ?? RadiationEnabled); outMsg.WritePadBits(); } @@ -274,7 +279,7 @@ namespace Barotrauma.Networking if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) { ToggleSettingsFrame(btn, userData); } return true; }; - + new GUIButton(new RectTransform(Vector2.One, settingsFrame.RectTransform), "", style: null) { OnClicked = ToggleSettingsFrame @@ -500,8 +505,8 @@ namespace Barotrauma.Networking CreateLabeledSlider(roundsTab, "ServerSettingsRespawnInterval", out slider, out sliderLabel); string intervalLabel = sliderLabel.Text; - slider.Step = 0.05f; slider.Range = new Vector2(10.0f, 600.0f); + slider.StepValue = 10.0f; GetPropertyData("RespawnInterval").AssignGUIComponent(slider); slider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => { @@ -646,7 +651,14 @@ namespace Barotrauma.Networking foreach (ItemPrefab ip in ItemPrefab.Prefabs) { - if (!ip.CanBeBought && !ip.Tags.Contains("smallitem")) continue; + if (ip.AllowAsExtraCargo.HasValue) + { + if (!ip.AllowAsExtraCargo.Value) { continue; } + } + else + { + if (!ip.CanBeBought) { continue; } + } var itemFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), cargoFrame.Content.RectTransform) { MinSize = new Point(0, 30) }, isHorizontal: true) { @@ -725,6 +737,10 @@ namespace Barotrauma.Networking TextManager.Get("ServerSettingsDestructibleOutposts")); GetPropertyData("DestructibleOutposts").AssignGUIComponent(destructibleOutposts); + var lockAllDefaultWires = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), + TextManager.Get("ServerSettingsLockAllDefaultWires")); + GetPropertyData("LockAllDefaultWires").AssignGUIComponent(lockAllDefaultWires); + var allowRewiring = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), TextManager.Get("ServerSettingsAllowRewiring")); GetPropertyData("AllowRewiring").AssignGUIComponent(allowRewiring); @@ -915,6 +931,7 @@ namespace Barotrauma.Networking public bool ToggleSettingsFrame(GUIButton button, object obj) { + if (GameMain.NetworkMember == null) { return false; } if (settingsFrame == null) { CreateSettingsFrame(); @@ -936,5 +953,12 @@ namespace Barotrauma.Networking return false; } + + private ServerInfo cachedServerListInfo = null; + public ServerInfo GetServerListInfo() + { + cachedServerListInfo ??= GameMain.ServerListScreen.UpdateServerInfoWithServerSettings(GameMain.Client.ClientPeer.ServerConnection, this); + return cachedServerListInfo; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs index be3b37eea..f9ca5c84c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs @@ -1,6 +1,8 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Sounds; +using Microsoft.Xna.Framework; using OpenAL; using System; +using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using System.Threading; @@ -56,7 +58,7 @@ namespace Barotrauma.Networking public bool Disconnected { get; private set; } - public static void Create(string deviceName, UInt16? storedBufferID=null) + public static void Create(string deviceName, UInt16? storedBufferID = null) { if (Instance != null) { @@ -84,7 +86,7 @@ namespace Barotrauma.Networking if (captureDevice == IntPtr.Zero) { - DebugConsole.NewMessage("Alc.CaptureOpenDevice attempt 1 failed: error code " + Alc.GetError(IntPtr.Zero).ToString(),Color.Orange); + DebugConsole.NewMessage("Alc.CaptureOpenDevice attempt 1 failed: error code " + Alc.GetError(IntPtr.Zero).ToString(), Color.Orange); //attempt using a smaller buffer size captureDevice = Alc.CaptureOpenDevice(deviceName, VoipConfig.FREQUENCY, Al.FormatMono16, VoipConfig.BUFFER_SIZE * 2); } @@ -162,6 +164,7 @@ namespace Barotrauma.Networking } } + IntPtr nativeBuffer; short[] uncompressedBuffer = new short[VoipConfig.BUFFER_SIZE]; short[] prevUncompressedBuffer = new short[VoipConfig.BUFFER_SIZE]; bool prevCaptured = true; @@ -171,143 +174,198 @@ namespace Barotrauma.Networking { Array.Copy(uncompressedBuffer, 0, prevUncompressedBuffer, 0, VoipConfig.BUFFER_SIZE); Array.Clear(uncompressedBuffer, 0, VoipConfig.BUFFER_SIZE); - while (capturing && !Disconnected) + nativeBuffer = Marshal.AllocHGlobal(VoipConfig.BUFFER_SIZE * 2); + try { - int alcError; - - if (CanDetectDisconnect) + while (capturing) { - Alc.GetInteger(captureDevice, Alc.EnumConnected, out int isConnected); + int alcError; + + if (CanDetectDisconnect) + { + Alc.GetInteger(captureDevice, Alc.EnumConnected, out int isConnected); + alcError = Alc.GetError(captureDevice); + if (alcError != Alc.NoError) + { + throw new Exception("Failed to determine if capture device is connected: " + alcError.ToString()); + } + + if (isConnected == 0) + { + DebugConsole.ThrowError("Capture device has been disconnected. You can select another available device in the settings."); + Disconnected = true; + break; + } + } + + FillBuffer(); + alcError = Alc.GetError(captureDevice); if (alcError != Alc.NoError) { - throw new Exception("Failed to determine if capture device is connected: " + alcError.ToString()); + throw new Exception("Failed to capture samples: " + alcError.ToString()); } - if (isConnected == 0) + double maxAmplitude = 0.0f; + for (int i = 0; i < VoipConfig.BUFFER_SIZE; i++) { - DebugConsole.ThrowError("Capture device has been disconnected. You can select another available device in the settings."); - Disconnected = true; - break; + uncompressedBuffer[i] = (short)MathHelper.Clamp((uncompressedBuffer[i] * Gain), -short.MaxValue, short.MaxValue); + double sampleVal = uncompressedBuffer[i] / (double)short.MaxValue; + maxAmplitude = Math.Max(maxAmplitude, Math.Abs(sampleVal)); } - } + double dB = Math.Min(20 * Math.Log10(maxAmplitude), 0.0); - Alc.GetInteger(captureDevice, Alc.EnumCaptureSamples, out int sampleCount); + LastdB = dB; + LastAmplitude = maxAmplitude; - alcError = Alc.GetError(captureDevice); - if (alcError != Alc.NoError) - { - throw new Exception("Failed to determine sample count: " + alcError.ToString()); - } - - if (sampleCount < VoipConfig.BUFFER_SIZE) - { - int sleepMs = (VoipConfig.BUFFER_SIZE - sampleCount) * 800 / VoipConfig.FREQUENCY; - if (sleepMs < 5) sleepMs = 5; - Thread.Sleep(sleepMs); - continue; - } - - GCHandle handle = GCHandle.Alloc(uncompressedBuffer, GCHandleType.Pinned); - try - { - Alc.CaptureSamples(captureDevice, handle.AddrOfPinnedObject(), VoipConfig.BUFFER_SIZE); - } - finally - { - handle.Free(); - } - - alcError = Alc.GetError(captureDevice); - if (alcError != Alc.NoError) - { - throw new Exception("Failed to capture samples: " + alcError.ToString()); - } - - double maxAmplitude = 0.0f; - for (int i = 0; i < VoipConfig.BUFFER_SIZE; i++) - { - uncompressedBuffer[i] = (short)MathHelper.Clamp((uncompressedBuffer[i] * Gain), -short.MaxValue, short.MaxValue); - double sampleVal = uncompressedBuffer[i] / (double)short.MaxValue; - maxAmplitude = Math.Max(maxAmplitude, Math.Abs(sampleVal)); - } - double dB = Math.Min(20 * Math.Log10(maxAmplitude), 0.0); - - LastdB = dB; - LastAmplitude = maxAmplitude; - - bool allowEnqueue = false; - if (GameMain.WindowActive) - { - ForceLocal = captureTimer > 0 ? ForceLocal : GameMain.Config.UseLocalVoiceByDefault; - bool pttDown = false; - if ((PlayerInput.KeyDown(InputType.Voice) || PlayerInput.KeyDown(InputType.LocalVoice)) && - GUI.KeyboardDispatcher.Subscriber == null) + bool allowEnqueue = overrideSound != null; + if (GameMain.WindowActive) { - pttDown = true; - if (PlayerInput.KeyDown(InputType.LocalVoice)) + ForceLocal = captureTimer > 0 ? ForceLocal : GameMain.Config.UseLocalVoiceByDefault; + bool pttDown = false; + if ((PlayerInput.KeyDown(InputType.Voice) || PlayerInput.KeyDown(InputType.LocalVoice)) && + GUI.KeyboardDispatcher.Subscriber == null) { - ForceLocal = true; + pttDown = true; + if (PlayerInput.KeyDown(InputType.LocalVoice)) + { + ForceLocal = true; + } + else + { + ForceLocal = false; + } } - else + if (GameMain.Config.VoiceSetting == GameSettings.VoiceMode.Activity) { - ForceLocal = false; + if (dB > GameMain.Config.NoiseGateThreshold) + { + allowEnqueue = true; + } + } + else if (GameMain.Config.VoiceSetting == GameSettings.VoiceMode.PushToTalk) + { + if (pttDown) + { + allowEnqueue = true; + } } } - if (GameMain.Config.VoiceSetting == GameSettings.VoiceMode.Activity) + + if (allowEnqueue || captureTimer > 0) { - if (dB > GameMain.Config.NoiseGateThreshold) + LastEnqueueAudio = DateTime.Now; + if (GameMain.Client?.Character != null) { - allowEnqueue = true; + var messageType = !ForceLocal && ChatMessage.CanUseRadio(GameMain.Client.Character, out _) ? ChatMessageType.Radio : ChatMessageType.Default; + GameMain.Client.Character.ShowSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]); } - } - else if (GameMain.Config.VoiceSetting == GameSettings.VoiceMode.PushToTalk) - { - if (pttDown) + //encode audio and enqueue it + lock (buffers) { - allowEnqueue = true; + if (!prevCaptured) //enqueue the previous buffer if not sent to avoid cutoff + { + int compressedCountPrev = VoipConfig.Encoder.Encode(prevUncompressedBuffer, 0, VoipConfig.BUFFER_SIZE, BufferToQueue, 0, VoipConfig.MAX_COMPRESSED_SIZE); + EnqueueBuffer(compressedCountPrev); + } + int compressedCount = VoipConfig.Encoder.Encode(uncompressedBuffer, 0, VoipConfig.BUFFER_SIZE, BufferToQueue, 0, VoipConfig.MAX_COMPRESSED_SIZE); + EnqueueBuffer(compressedCount); + } + captureTimer -= (VoipConfig.BUFFER_SIZE * 1000) / VoipConfig.FREQUENCY; + if (allowEnqueue) + { + captureTimer = GameMain.Config.VoiceChatCutoffPrevention; + } + prevCaptured = true; + } + else + { + captureTimer = 0; + prevCaptured = false; + //enqueue silence + lock (buffers) + { + EnqueueBuffer(0); } } } + } + catch (Exception e) + { + DebugConsole.ThrowError($"VoipCapture threw an exception. Disabling capture...", e); + capturing = false; + } + finally + { + Marshal.FreeHGlobal(nativeBuffer); + } + } - if (allowEnqueue || captureTimer > 0) + private Sound overrideSound; + private int overridePos; + private short[] overrideBuf = new short[VoipConfig.BUFFER_SIZE]; + + private void FillBuffer() + { + if (overrideSound != null) + { + int totalSampleCount = 0; + while (totalSampleCount < VoipConfig.BUFFER_SIZE) { - LastEnqueueAudio = DateTime.Now; - if (GameMain.Client?.Character != null) + int sampleCount = overrideSound.FillStreamBuffer(overridePos, overrideBuf); + overridePos += sampleCount * 2; + Array.Copy(overrideBuf, 0, uncompressedBuffer, totalSampleCount, sampleCount); + totalSampleCount += sampleCount; + + if (sampleCount == 0) { - var messageType = !ForceLocal && ChatMessage.CanUseRadio(GameMain.Client.Character, out _) ? ChatMessageType.Radio : ChatMessageType.Default; - GameMain.Client.Character.ShowSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]); + overridePos = 0; } - //encode audio and enqueue it - lock (buffers) + } + int sleepMs = VoipConfig.BUFFER_SIZE * 800 / VoipConfig.FREQUENCY; + Thread.Sleep(sleepMs - 1); + } + else + { + int sampleCount = 0; + + while (sampleCount < VoipConfig.BUFFER_SIZE) + { + Alc.GetInteger(captureDevice, Alc.EnumCaptureSamples, out sampleCount); + + int alcError = Alc.GetError(captureDevice); + if (alcError != Alc.NoError) { - if (!prevCaptured) //enqueue the previous buffer if not sent to avoid cutoff + throw new Exception("Failed to determine sample count: " + alcError.ToString()); + } + + if (sampleCount < VoipConfig.BUFFER_SIZE) + { + int sleepMs = (VoipConfig.BUFFER_SIZE - sampleCount) * 800 / VoipConfig.FREQUENCY; + if (sleepMs >= 1) { - int compressedCountPrev = VoipConfig.Encoder.Encode(prevUncompressedBuffer, 0, VoipConfig.BUFFER_SIZE, BufferToQueue, 0, VoipConfig.MAX_COMPRESSED_SIZE); - EnqueueBuffer(compressedCountPrev); + Thread.Sleep(sleepMs); } - int compressedCount = VoipConfig.Encoder.Encode(uncompressedBuffer, 0, VoipConfig.BUFFER_SIZE, BufferToQueue, 0, VoipConfig.MAX_COMPRESSED_SIZE); - EnqueueBuffer(compressedCount); - } - captureTimer -= (VoipConfig.BUFFER_SIZE * 1000) / VoipConfig.FREQUENCY; - if (allowEnqueue) - { - captureTimer = GameMain.Config.VoiceChatCutoffPrevention; - } - prevCaptured = true; - } - else - { - captureTimer = 0; - prevCaptured = false; - //enqueue silence - lock (buffers) - { - EnqueueBuffer(0); } + + if (!capturing) { return; } } - Thread.Sleep(10); + Alc.CaptureSamples(captureDevice, nativeBuffer, VoipConfig.BUFFER_SIZE); + Marshal.Copy(nativeBuffer, uncompressedBuffer, 0, uncompressedBuffer.Length); + } + } + + public void SetOverrideSound(string fileName) + { + overrideSound?.Dispose(); + if (string.IsNullOrEmpty(fileName)) + { + overrideSound = null; + } + else + { + overrideSound = GameMain.SoundManager.LoadSound(fileName, true); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs index b4fd76f45..0d360bd80 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs @@ -94,6 +94,7 @@ namespace Barotrauma.Networking DebugConsole.Log("Recreating voipsound " + queueId); client.VoipSound = new VoipSound(client.Name, GameMain.SoundManager, client.VoipQueue); } + GameMain.SoundManager.ForceStreamUpdate(); if (client.Character != null && !client.Character.IsDead && !client.Character.Removed && client.Character.SpeechImpediment <= 100.0f) { @@ -102,7 +103,7 @@ namespace Barotrauma.Networking client.Character.ShowSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]); client.VoipSound.UseRadioFilter = messageType == ChatMessageType.Radio && !GameMain.Config.DisableVoiceChatFilters; - if (client.VoipSound.UseRadioFilter) + if (messageType == ChatMessageType.Radio) { client.VoipSound.SetRange(radio.Range * 0.8f, radio.Range); } @@ -110,7 +111,7 @@ namespace Barotrauma.Networking { client.VoipSound.SetRange(ChatMessage.SpeakRange * 0.4f, ChatMessage.SpeakRange); } - if (!client.VoipSound.UseRadioFilter && Character.Controlled != null && !GameMain.Config.DisableVoiceChatFilters) + if (messageType != ChatMessageType.Radio && Character.Controlled != null && !GameMain.Config.DisableVoiceChatFilters) { client.VoipSound.UseMuffleFilter = SoundPlayer.ShouldMuffleSound(Character.Controlled, client.Character.WorldPosition, ChatMessage.SpeakRange, client.Character.CurrentHull); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipConfig.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipConfig.cs index ac32f0345..bd907694a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipConfig.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipConfig.cs @@ -12,8 +12,8 @@ namespace Barotrauma.Networking { public static bool Ready = false; - public const int FREQUENCY = 48000; //not amazing, but not bad audio quality - public const int BUFFER_SIZE = 2880; //60ms window, the max Opus seems to support + public const int FREQUENCY = 48000; + public const int BUFFER_SIZE = 960; //20ms window public static OpusEncoder Encoder { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs index ceec59032..31dc44d22 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs @@ -112,14 +112,17 @@ namespace Barotrauma switch (voteType) { - case VoteType.Sub: - SubmarineInfo sub = data as SubmarineInfo; - if (sub == null) { return; } + case VoteType.Sub: + if (!(data is SubmarineInfo sub)) { return; } msg.Write(sub.EqualityCheckVal); + if (sub.EqualityCheckVal == 0) + { + //sub doesn't exist client-side, use hash to let the server know which one we voted for + msg.Write(sub.MD5Hash.Hash); + } break; case VoteType.Mode: - GameModePreset gameMode = data as GameModePreset; - if (gameMode == null) { return; } + if (!(data is GameModePreset gameMode)) { return; } msg.Write(gameMode.Identifier); break; case VoteType.EndRound: @@ -127,8 +130,7 @@ namespace Barotrauma msg.Write((bool)data); break; case VoteType.Kick: - Client votedClient = data as Client; - if (votedClient == null) return; + if (!(data is Client votedClient)) { return; } msg.Write(votedClient.ID); break; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs index 959915944..55dc6e385 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs @@ -27,7 +27,8 @@ namespace Barotrauma.Particles private float angularVelocity; private Vector2 dragVec = Vector2.Zero; - private int dragWait = 0; + private float dragWait = 0; + private float collisionIgnoreTimer = 0; private Vector2 size; private Vector2 sizeChange; @@ -103,7 +104,7 @@ namespace Barotrauma.Particles return debugName; } - public void Init(ParticlePrefab prefab, Vector2 position, Vector2 speed, float rotation, Hull hullGuess = null, bool drawOnTop = false) + public void Init(ParticlePrefab prefab, Vector2 position, Vector2 speed, float rotation, Hull hullGuess = null, bool drawOnTop = false, float collisionIgnoreTimer = 0f) { this.prefab = prefab; debugName = $"Particle ({prefab.Name})"; @@ -174,14 +175,22 @@ namespace Barotrauma.Particles } DrawOnTop = drawOnTop; + + this.collisionIgnoreTimer = collisionIgnoreTimer; } - public bool Update(float deltaTime) + public enum UpdateResult + { + Normal, + Delete + } + + public UpdateResult Update(float deltaTime) { if (startDelay > 0.0f) { startDelay -= deltaTime; - return true; + return UpdateResult.Normal; } prevPosition = position; @@ -251,7 +260,7 @@ namespace Barotrauma.Particles } lifeTime -= deltaTime; - if (lifeTime <= 0.0f || color.A <= 0 || size.X <= 0.0f || size.Y <= 0.0f) { return false; } + if (lifeTime <= 0.0f || color.A <= 0 || size.X <= 0.0f || size.Y <= 0.0f) { return UpdateResult.Delete; } if (hasSubEmitters) { @@ -261,7 +270,13 @@ namespace Barotrauma.Particles } } - if (!prefab.UseCollision) { return true; } + if (collisionIgnoreTimer > 0f) + { + collisionIgnoreTimer -= deltaTime; + if (collisionIgnoreTimer <= 0f) { currentHull ??= Hull.FindHull(position); } + return UpdateResult.Normal; + } + if (!prefab.UseCollision) { return UpdateResult.Normal; } if (HighQualityCollisionDetection) { @@ -278,17 +293,17 @@ namespace Barotrauma.Particles } } - return true; + return UpdateResult.Normal; } - private bool CollisionUpdate() + private UpdateResult CollisionUpdate() { if (currentHull == null) { Hull collidedHull = Hull.FindHull(position); if (collidedHull != null) { - if (prefab.DeleteOnCollision) return false; + if (prefab.DeleteOnCollision) return UpdateResult.Delete; OnWallCollisionOutside(collidedHull); } } @@ -298,12 +313,12 @@ namespace Barotrauma.Particles Vector2 collisionNormal = Vector2.Zero; if (velocity.Y < 0.0f && position.Y - prefab.CollisionRadius * size.Y < hullRect.Y - hullRect.Height) { - if (prefab.DeleteOnCollision) { return false; } + if (prefab.DeleteOnCollision) { return UpdateResult.Delete; } collisionNormal = new Vector2(0.0f, 1.0f); } else if (velocity.Y > 0.0f && position.Y + prefab.CollisionRadius * size.Y > hullRect.Y) { - if (prefab.DeleteOnCollision) { return false; } + if (prefab.DeleteOnCollision) { return UpdateResult.Delete; } collisionNormal = new Vector2(0.0f, -1.0f); } @@ -328,12 +343,12 @@ namespace Barotrauma.Particles if (velocity.X < 0.0f && position.X - prefab.CollisionRadius * size.X < hullRect.X) { - if (prefab.DeleteOnCollision) { return false; } + if (prefab.DeleteOnCollision) { return UpdateResult.Delete; } collisionNormal = new Vector2(1.0f, 0.0f); } else if (velocity.X > 0.0f && position.X + prefab.CollisionRadius * size.X > hullRect.Right) { - if (prefab.DeleteOnCollision) { return false; } + if (prefab.DeleteOnCollision) { return UpdateResult.Delete; } collisionNormal = new Vector2(-1.0f, 0.0f); } @@ -374,7 +389,7 @@ namespace Barotrauma.Particles } } - return true; + return UpdateResult.Normal; } private void ApplyDrag(float dragCoefficient, float deltaTime) @@ -389,10 +404,10 @@ namespace Barotrauma.Particles //TODO: some better way to handle particle drag //this doesn't work that well because the drag vector is only updated every 0.5 seconds, allowing the particle to accelerate way more than it should //(e.g. a falling particle can freely accelerate for 0.5 seconds before the drag takes effect) - dragWait--; - if (dragWait <= 0) + dragWait-=deltaTime; + if (dragWait <= 0f) { - dragWait = 30; + dragWait = 0.5f; float speed = velocity.Length(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs index 68e4cc78c..bfc43ae7b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs @@ -115,12 +115,12 @@ namespace Barotrauma.Particles Prefabs.RemoveByFile(configFile); } - public Particle CreateParticle(string prefabName, Vector2 position, float angle, float speed, Hull hullGuess = null) + public Particle CreateParticle(string prefabName, Vector2 position, float angle, float speed, Hull hullGuess = null, float collisionIgnoreTimer = 0f) { - return CreateParticle(prefabName, position, new Vector2((float)Math.Cos(angle), (float)-Math.Sin(angle)) * speed, angle, hullGuess); + return CreateParticle(prefabName, position, new Vector2((float)Math.Cos(angle), (float)-Math.Sin(angle)) * speed, angle, hullGuess, collisionIgnoreTimer); } - public Particle CreateParticle(string prefabName, Vector2 position, Vector2 velocity, float rotation = 0.0f, Hull hullGuess = null) + public Particle CreateParticle(string prefabName, Vector2 position, Vector2 velocity, float rotation = 0.0f, Hull hullGuess = null, float collisionIgnoreTimer = 0f) { ParticlePrefab prefab = FindPrefab(prefabName); @@ -130,10 +130,10 @@ namespace Barotrauma.Particles return null; } - return CreateParticle(prefab, position, velocity, rotation, hullGuess); + return CreateParticle(prefab, position, velocity, rotation, hullGuess, collisionIgnoreTimer: collisionIgnoreTimer); } - public Particle CreateParticle(ParticlePrefab prefab, Vector2 position, Vector2 velocity, float rotation = 0.0f, Hull hullGuess = null, bool drawOnTop = false) + public Particle CreateParticle(ParticlePrefab prefab, Vector2 position, Vector2 velocity, float rotation = 0.0f, Hull hullGuess = null, bool drawOnTop = false, float collisionIgnoreTimer = 0f) { if (particleCount >= MaxParticles || prefab == null || prefab.Sprites.Count == 0) { return null; } @@ -149,7 +149,7 @@ namespace Barotrauma.Particles if (particles[particleCount] == null) particles[particleCount] = new Particle(); - particles[particleCount].Init(prefab, position, velocity, rotation, hullGuess, drawOnTop); + particles[particleCount].Init(prefab, position, velocity, rotation, hullGuess, drawOnTop, collisionIgnoreTimer); particleCount++; @@ -181,10 +181,10 @@ namespace Barotrauma.Particles for (int i = 0; i < particleCount; i++) { - bool remove = false; + bool remove; try { - remove = !particles[i].Update(deltaTime); + remove = particles[i].Update(deltaTime) == Particle.UpdateResult.Delete; } catch (Exception e) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs b/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs index f4a86f565..a2a3d770e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs @@ -476,6 +476,11 @@ namespace Barotrauma #endif } + public static bool IsAltDown() + { + return KeyDown(Keys.LeftAlt) || KeyDown(Keys.RightAlt); + } + public static void Update(double deltaTime) { timeSinceClick += deltaTime; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs index 7602d3873..19d9c2348 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs @@ -23,7 +23,7 @@ namespace Barotrauma private GUIButton loadGameButton, deleteMpSaveButton; - public Action StartNewGame; + public Action StartNewGame; public Action LoadGame; private enum CategoryFilter { All = 0, Vanilla = 1, Custom = 2 }; @@ -40,6 +40,8 @@ namespace Barotrauma get; private set; } + + public GUITickBox EnableRadiationToggle { get; set; } private readonly bool isMultiplayer; @@ -171,6 +173,16 @@ namespace Barotrauma string savePath = SaveUtil.CreateSavePath(isMultiplayer ? SaveUtil.SaveType.Multiplayer : SaveUtil.SaveType.Singleplayer, saveNameBox.Text); bool hasRequiredContentPackages = selectedSub.RequiredContentPackagesInstalled; + CampaignSettings settings = new CampaignSettings(); + if (isMultiplayer) + { + settings.RadiationEnabled = GameMain.NetLobbyScreen.IsRadiationEnabled(); + } + else + { + settings.RadiationEnabled = EnableRadiationToggle?.Selected ?? false; + } + if (selectedSub.HasTag(SubmarineTag.Shuttle) || !hasRequiredContentPackages) { if (!hasRequiredContentPackages) @@ -184,7 +196,7 @@ namespace Barotrauma { if (GUIMessageBox.MessageBoxes.Count == 0) { - StartNewGame?.Invoke(selectedSub, savePath, seedBox.Text); + StartNewGame?.Invoke(selectedSub, savePath, seedBox.Text, settings); if (isMultiplayer) { CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup"); @@ -204,7 +216,7 @@ namespace Barotrauma msgBox.Buttons[0].OnClicked = (button, obj) => { - StartNewGame?.Invoke(selectedSub, savePath, seedBox.Text); + StartNewGame?.Invoke(selectedSub, savePath, seedBox.Text, settings); if (isMultiplayer) { CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup"); @@ -219,7 +231,7 @@ namespace Barotrauma } else { - StartNewGame?.Invoke(selectedSub, savePath, seedBox.Text); + StartNewGame?.Invoke(selectedSub, savePath, seedBox.Text, settings); if (isMultiplayer) { CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup"); @@ -230,8 +242,7 @@ namespace Barotrauma } }; - InitialMoneyText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1f), buttonContainer.RectTransform), "", - font: isMultiplayer ? GUI.Style.SmallFont : GUI.Style.Font, textColor: GUI.Style.Green) + InitialMoneyText = new GUITextBlock(new RectTransform(new Vector2(isMultiplayer ? 0.6f : 0.3f, 1f), buttonContainer.RectTransform), "", font: isMultiplayer ? GUI.Style.SmallFont : GUI.Style.Font, textColor: GUI.Style.Green) { TextGetter = () => { @@ -254,6 +265,15 @@ namespace Barotrauma if (!isMultiplayer) { + if (MapGenerationParams.Instance.RadiationParams != null) + { + EnableRadiationToggle = new GUITickBox(new RectTransform(new Vector2(0.3f, 1f), buttonContainer.RectTransform), TextManager.Get("CampaignOption.EnableRadiation"), font: GUI.Style.Font) + { + Selected = true, + ToolTip = TextManager.Get("campaignoption.enableradiation.tooltip") + }; + } + var disclaimerBtn = new GUIButton(new RectTransform(new Vector2(1.0f, 0.8f), rightColumn.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point(5) }, style: "GUINotificationButton") { IgnoreLayoutGroups = true, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index 7f72b71d3..7bc5bd0eb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -53,7 +53,7 @@ namespace Barotrauma campaign.Map.OnLocationSelected += SelectLocation; campaign.Map.OnMissionSelected += (connection, mission) => { - missionList.Select(mission); + missionList?.Select(mission); }; } @@ -306,7 +306,7 @@ namespace Barotrauma { var map = GameMain.GameSession?.Map; if (map == null) { return; } - if (selectedLocation != null && selectedLocation == map.CurrentDisplayLocation) + if (selectedLocation != null && selectedLocation == GameMain.GameSession.Campaign.GetCurrentDisplayLocation()) { map.SelectLocation(-1); } @@ -394,6 +394,42 @@ namespace Barotrauma var difficultyLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), textContent.RectTransform), TextManager.Get("LevelDifficulty"), font: GUI.SubHeadingFont, textAlignment: Alignment.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), difficultyLabel.RectTransform), ((int)connection.LevelData.Difficulty) + " %", textAlignment: Alignment.CenterRight); + + if (connection.LevelData.HasBeaconStation) + { + var beaconStationContent = new GUILayoutGroup(new RectTransform(biomeLabel.RectTransform.NonScaledSize, textContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + string style = connection.LevelData.IsBeaconActive ? "BeaconStationActive" : "BeaconStationInactive"; + var icon = new GUIImage(new RectTransform(new Point((int)(beaconStationContent.Rect.Height * 1.2f)), beaconStationContent.RectTransform), + style, scaleToFit: true) + { + Color = MapGenerationParams.Instance.IndicatorColor, + HoverColor = Color.Lerp(MapGenerationParams.Instance.IndicatorColor, Color.White, 0.5f), + ToolTip = TextManager.Get(connection.LevelData.IsBeaconActive ? "BeaconStationActiveTooltip" : "BeaconStationInactiveTooltip") + }; + new GUITextBlock(new RectTransform(Vector2.One, beaconStationContent.RectTransform), + TextManager.Get("submarinetype.beaconstation"), font: GUI.SubHeadingFont, textAlignment: Alignment.CenterLeft) + { + Padding = Vector4.Zero, + ToolTip = icon.ToolTip + }; + } + if (connection.LevelData.HasHuntingGrounds) + { + var huntingGroundsContent = new GUILayoutGroup(new RectTransform(biomeLabel.RectTransform.NonScaledSize, textContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + var icon = new GUIImage(new RectTransform(new Point((int)(huntingGroundsContent.Rect.Height * 1.5f)), huntingGroundsContent.RectTransform), + "HuntingGrounds", scaleToFit: true) + { + Color = MapGenerationParams.Instance.IndicatorColor, + HoverColor = Color.Lerp(MapGenerationParams.Instance.IndicatorColor, Color.White, 0.5f), + ToolTip = TextManager.Get("HuntingGroundsTooltip") + }; + new GUITextBlock(new RectTransform(Vector2.One, huntingGroundsContent.RectTransform), + TextManager.Get("missionname.huntinggrounds"), font: GUI.SubHeadingFont, textAlignment: Alignment.CenterLeft) + { + Padding = Vector4.Zero, + ToolTip = icon.ToolTip + }; + } } missionList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.4f), content.RectTransform)) @@ -402,7 +438,7 @@ namespace Barotrauma }; SelectedLevel = connection?.LevelData; - Location currentDisplayLocation = Campaign.CurrentDisplayLocation; + Location currentDisplayLocation = Campaign.GetCurrentDisplayLocation(); if (connection != null && connection.Locations.Contains(currentDisplayLocation)) { List availableMissions = currentDisplayLocation.GetMissionsInConnection(connection).ToList(); @@ -437,11 +473,44 @@ namespace Barotrauma SelectedColor = MapGenerationParams.Instance.IndicatorColor, HoverColor = Color.Lerp(MapGenerationParams.Instance.IndicatorColor, Color.White, 0.5f) }; - missionName.Padding = new Vector4(missionName.Padding.X + icon.Rect.Width * 1.5f, missionName.Padding.Y, missionName.Padding.Z, missionName.Padding.W); + icon.RectTransform.IsFixedSize = true; + + GUILayoutGroup difficultyIndicatorGroup = null; + if (mission.Difficulty.HasValue) + { + difficultyIndicatorGroup = new GUILayoutGroup(new RectTransform(Vector2.One * 0.9f, missionName.RectTransform, anchor: Anchor.CenterRight, scaleBasis: ScaleBasis.Smallest) { AbsoluteOffset = new Point((int)missionName.Padding.Z, 0) }, + isHorizontal: true, childAnchor: Anchor.CenterRight) + { + AbsoluteSpacing = 1, + UserData = "difficulty" + }; + var difficultyColor = mission.GetDifficultyColor(); + for (int i = 0; i < mission.Difficulty; i++) + { + new GUIImage(new RectTransform(Vector2.One, difficultyIndicatorGroup.RectTransform, scaleBasis: ScaleBasis.Smallest) { IsFixedSize = true }, "DifficultyIndicator", scaleToFit: true) + { + Color = difficultyColor * 0.5f, + SelectedColor = difficultyColor, + HoverColor = Color.Lerp(difficultyColor, Color.White, 0.5f) + }; + } + } + + float extraPadding = 0.5f * icon.Rect.Width; + float extraZPadding = difficultyIndicatorGroup != null ? mission.Difficulty.Value * (difficultyIndicatorGroup.Children.First().Rect.Width + difficultyIndicatorGroup.AbsoluteSpacing) : 0; + missionName.Padding = new Vector4(missionName.Padding.X + icon.Rect.Width + extraPadding, + missionName.Padding.Y, + missionName.Padding.Z + extraZPadding + extraPadding, + missionName.Padding.W); + missionName.CalculateHeightFromText(); } - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), - TextManager.GetWithVariable("missionreward", "[reward]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", mission.Reward)), wrap: true); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), mission.Description, wrap: true); + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), mission.GetMissionRewardText(), wrap: true, parseRichText: true); + + string reputationText = mission.GetReputationRewardText(mission.Locations[0]); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), reputationText, wrap: true, parseRichText: true); + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), mission.Description, wrap: true, parseRichText: true); } missionPanel.RectTransform.MinSize = new Point(0, (int)(missionTextContent.Children.Sum(c => c.Rect.Height) / missionTextContent.RectTransform.RelativeSize.Y) + GUI.IntScale(20)); foreach (GUIComponent child in missionTextContent.Children) @@ -454,6 +523,10 @@ namespace Barotrauma missionPanel.OnAddedToGUIUpdateList = (c) => { missionTextContent.Children.ForEach(child => child.State = c.State); + if (missionTextContent.FindChild("difficulty", recursive: true) is GUILayoutGroup group) + { + group.State = c.State; + } }; if (mission != availableMissions.Last()) @@ -491,7 +564,25 @@ namespace Barotrauma StartButton = new GUIButton(new RectTransform(new Vector2(0.5f, 0.1f), content.RectTransform), TextManager.Get("StartCampaignButton"), style: "GUIButtonLarge") { - OnClicked = (GUIButton btn, object obj) => { StartRound?.Invoke(); return true; }, + OnClicked = (GUIButton btn, object obj) => + { + if (missionList.Content.Children.Any(c => c.UserData is Mission) && !(missionList.SelectedData is Mission)) + { + var noMissionVerification = new GUIMessageBox(string.Empty, TextManager.Get("nomissionprompt"), new string[] { TextManager.Get("yes"), TextManager.Get("no") }); + noMissionVerification.Buttons[0].OnClicked = (btn, userdata) => + { + StartRound?.Invoke(); + noMissionVerification.Close(); + return true; + }; + noMissionVerification.Buttons[1].OnClicked = noMissionVerification.Close; + } + else + { + StartRound?.Invoke(); + } + return true; + }, Enabled = true, Visible = Campaign.AllowedToEndRound() }; @@ -507,6 +598,11 @@ namespace Barotrauma public void SelectTab(CampaignMode.InteractionType tab) { + if (Campaign.ShowCampaignUI || (Campaign.ForceMapUI && tab == CampaignMode.InteractionType.Map)) + { + HintManager.OnShowCampaignInterface(tab); + } + selectedTab = tab; for (int i = 0; i < tabs.Length; i++) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index d0aba1404..6041af895 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -711,7 +711,7 @@ namespace Barotrauma.CharacterEditor } } // Camera - Cam.MoveCamera((float)deltaTime, allowMove: false); + Cam.MoveCamera((float)deltaTime, allowMove: false, allowZoom: GUI.MouseOn == null); Vector2 targetPos = character.WorldPosition; if (PlayerInput.MidButtonHeld()) { @@ -766,7 +766,7 @@ namespace Barotrauma.CharacterEditor if (editLimbs && !spriteSheetRect.Contains(PlayerInput.MousePosition) && MathUtils.RectangleContainsPoint(GetLimbPhysicRect(limb), PlayerInput.MousePosition)) { return CursorState.Hand; } // spritesheet - if (GetLimbSpritesheetRect(limb).Contains(PlayerInput.MousePosition)) { return CursorState.Hand; } + if (showSpritesheet && GetLimbSpritesheetRect(limb).Contains(PlayerInput.MousePosition)) { return CursorState.Hand; } } return CursorState.Default; } @@ -1761,9 +1761,9 @@ namespace Barotrauma.CharacterEditor #endif // Add to the selected content package contentPackage.AddFile(configFilePath, ContentType.Character); - Barotrauma.IO.Validation.DevException = true; + Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; contentPackage.Save(contentPackage.Path); - Barotrauma.IO.Validation.DevException = false; + Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; DebugConsole.NewMessage(GetCharacterEditorTranslation("ContentPackageSaved").Replace("[path]", contentPackage.Path)); // Ragdoll diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorImage.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorImage.cs index 1b9b5d89f..4563b2ff6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorImage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorImage.cs @@ -256,14 +256,14 @@ namespace Barotrauma } } - public void CreateImageWizard(Vector2 positon) + public void CreateImageWizard() { string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); if (!Directory.Exists(home)) { return; } FileSelection.OnFileSelected = file => { - Vector2 pos = Screen.Selected.Cam.ScreenToWorld(positon); + Vector2 pos = Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition); pos.Y = -pos.Y; Images.Add(new EditorImage(file, pos) { DrawTarget = EditorImage.DrawTargetType.World }); UpdateImageCategories(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs index 80361fede..1a891e878 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs @@ -17,8 +17,6 @@ namespace Barotrauma { private GUIFrame GuiFrame = null!; - private GUIListBox? contextMenu; - public override Camera Cam { get; } public static string? DrawnTooltip { get; set; } @@ -538,7 +536,6 @@ namespace Barotrauma public override void AddToGUIUpdateList() { GuiFrame.AddToGUIUpdateList(); - contextMenu?.AddToGUIUpdateList(); } private XElement? ExportXML() @@ -597,7 +594,7 @@ namespace Barotrauma foreach (var (node, text, end) in options) { XElement optionElement = new XElement("Option"); - optionElement.Add(new XAttribute("text", text)); + optionElement.Add(new XAttribute("text", text ?? "")); if (end) { optionElement.Add(new XAttribute("endconversation", true)); } if (node is EventNode eventNode) @@ -674,82 +671,37 @@ namespace Barotrauma private void CreateContextMenu(EditorNode node, NodeConnection? connection = null) { - contextMenu = new GUIListBox(new RectTransform(new Vector2(0.1f, 0.1f), GUI.Canvas) { ScreenSpaceOffset = PlayerInput.MousePosition.ToPoint() }, style: "GUIToolTip") { Padding = new Vector4(5) }; + if (GUIContextMenu.CurrentContextMenu != null) { return; } - new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), - TextManager.Get("EventEditor.Edit"), font: GUI.SmallFont) { UserData = "edit", Enabled = node is ValueNode || connection?.Type == NodeConnectionType.Value || connection?.Type == NodeConnectionType.Option }; - - new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), - TextManager.Get("EventEditor.MarkEnding"), font: GUI.SmallFont) { UserData = "markend", Enabled = connection != null && connection.Type == NodeConnectionType.Option }; - - new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), - TextManager.Get("EventEditor.RemoveConnection"), font: GUI.SmallFont) { UserData = "remcon", Enabled = connection != null }; - - new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), - TextManager.Get("EventEditor.AddOption"), font: GUI.SmallFont) { UserData = "addoption", Enabled = node.CanAddConnections }; - - new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), - TextManager.Get("EventEditor.RemoveOption"), font: GUI.SmallFont) { UserData = "removeoption", Enabled = connection != null && node.RemovableTypes.Contains(connection.Type) }; - - new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), - TextManager.Get("EventEditor.Delete"), font: GUI.SmallFont) { UserData = "delete", Enabled = true }; - - foreach (var guiComponent in contextMenu.Content.Children) - { - if (guiComponent is GUITextBlock child) + GUIContextMenu.CreateContextMenu( + new ContextMenuOption("EventEditor.Edit", isEnabled: node is ValueNode || connection?.Type == NodeConnectionType.Value || connection?.Type == NodeConnectionType.Option, onSelected: delegate { - if (!child.Enabled) - { - child.TextColor *= 0.5f; - } - } - } - - foreach (GUIComponent c in contextMenu.Content.Children) - { - if (c is GUITextBlock block) + CreateEditMenu(node as ValueNode, connection); + }), + new ContextMenuOption("EventEditor.MarkEnding", isEnabled: connection != null && connection.Type == NodeConnectionType.Option, onSelected: delegate { - block.RectTransform.NonScaledSize = new Point((int) (block.TextSize.X + block.Padding.X * 2), (int) (18 * GUI.Scale)); - } - } + if (connection == null) { return; } - int biggestSize = contextMenu.Content.Children.Max(c => c.Rect.Width + (int) contextMenu.Padding.X * 2); - contextMenu.Content.Children.ForEach(c => c.RectTransform.MinSize = new Point(biggestSize, c.Rect.Height)); - contextMenu.RectTransform.NonScaledSize = new Point(biggestSize, (int) (contextMenu.Content.Children.Sum(c => c.Rect.Height) + (contextMenu.Padding.X * 2))); - - contextMenu.OnSelected = (component, obj) => - { - if (!component.Enabled) { return false; } - - switch (obj as string) + connection.EndConversation = !connection.EndConversation; + }), + new ContextMenuOption("EventEditor.RemoveConnection", isEnabled: connection != null, onSelected: delegate { - case "edit": - CreateEditMenu(node as ValueNode, connection); - break; - case "markend" when connection != null: - connection.EndConversation = !connection.EndConversation; - break; - case "remcon" when connection != null: - connection.ClearConnections(); - connection.OverrideValue = null; - connection.OptionText = connection.OptionText; - break; - case "addoption": - node.AddOption(); - break; - case "removeoption": - connection?.Parent.RemoveOption(connection); - break; - case "delete": - nodeList.Remove(node); - node.ClearConnections(); + if (connection == null) { return; } - break; - } - - contextMenu = null; - return true; - }; + connection.ClearConnections(); + connection.OverrideValue = null; + connection.OptionText = connection.OptionText; + }), + new ContextMenuOption("EventEditor.AddOption", isEnabled: node.CanAddConnections, onSelected: node.AddOption), + new ContextMenuOption("EventEditor.RemoveOption", isEnabled: connection != null && node.RemovableTypes.Contains(connection.Type), onSelected: delegate + { + connection?.Parent.RemoveOption(connection); + }), + new ContextMenuOption("EventEditor.Delete", isEnabled: true, onSelected: delegate + { + nodeList.Remove(node); + node.ClearConnections(); + })); } private bool CreateTestSetupMenu() @@ -930,7 +882,7 @@ namespace Barotrauma return false; } - GameSession gameSession = new GameSession(subInfo, "", GameModePreset.TestMode, null); + GameSession gameSession = new GameSession(subInfo, "", GameModePreset.TestMode, CampaignSettings.Empty, null); TestGameMode gameMode = (TestGameMode) gameSession.GameMode; gameMode.SpawnOutpost = true; @@ -1001,7 +953,7 @@ namespace Barotrauma CreateGUI(); } - Cam.MoveCamera((float) deltaTime, true, true); + Cam.MoveCamera((float) deltaTime, allowMove: true, allowZoom: GUI.MouseOn == null); Vector2 mousePos = Cam.ScreenToWorld(PlayerInput.MousePosition); mousePos.Y = -mousePos.Y; @@ -1142,16 +1094,6 @@ namespace Barotrauma DraggingPosition = Vector2.Zero; } - if (contextMenu != null) - { - Rectangle expandedRect = contextMenu.Rect; - expandedRect.Inflate(20, 20); - if (!expandedRect.Contains(PlayerInput.MousePosition)) - { - contextMenu = null; - } - } - if (PlayerInput.MidButtonHeld()) { Vector2 moveSpeed = PlayerInput.MouseSpeed * (float) deltaTime * 60.0f / Cam.Zoom; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index 42076beb3..636daf0f3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -24,6 +24,7 @@ namespace Barotrauma public Effect PostProcessEffect { get; private set; } public Effect GradientEffect { get; private set; } + public Effect GrainEffect { get; private set; } public GameScreen(GraphicsDevice graphics, ContentManager content) { @@ -41,11 +42,13 @@ namespace Barotrauma damageEffect = content.Load("Effects/damageshader_opengl"); PostProcessEffect = content.Load("Effects/postprocess_opengl"); GradientEffect = content.Load("Effects/gradientshader_opengl"); + GrainEffect = content.Load("Effects/grainshader_opengl"); #else //var blurEffect = content.Load("Effects/blurshader"); damageEffect = content.Load("Effects/damageshader"); PostProcessEffect = content.Load("Effects/postprocess"); GradientEffect = content.Load("Effects/gradientshader"); + GrainEffect = content.Load("Effects/grainshader"); #endif damageStencil = TextureLoader.FromFile("Content/Map/walldamage.png"); @@ -168,10 +171,7 @@ namespace Barotrauma Character.Controlled.ObstructVision && (Character.Controlled.ViewTarget == Character.Controlled || Character.Controlled.ViewTarget == null); - if (Character.Controlled != null) - { - GameMain.LightManager.UpdateObstructVision(graphics, spriteBatch, cam, Character.Controlled.CursorWorldPosition); - } + GameMain.LightManager.UpdateObstructVision(graphics, spriteBatch, cam, Character.Controlled?.CursorWorldPosition ?? Vector2.Zero); //------------------------------------------------------------------------ graphics.SetRenderTarget(renderTarget); @@ -331,12 +331,13 @@ namespace Barotrauma } spriteBatch.End(); - if (GameMain.LightManager.LosEnabled && GameMain.LightManager.LosMode != LosMode.None && Character.Controlled != null) + if (GameMain.LightManager.LosEnabled && GameMain.LightManager.LosMode != LosMode.None && Lights.LightManager.ViewTarget != null) { GameMain.LightManager.LosEffect.CurrentTechnique = GameMain.LightManager.LosEffect.Techniques["LosShader"]; GameMain.LightManager.LosEffect.Parameters["xTexture"].SetValue(renderTargetBackground); GameMain.LightManager.LosEffect.Parameters["xLosTexture"].SetValue(GameMain.LightManager.LosTexture); + GameMain.LightManager.LosEffect.Parameters["xLosAlpha"].SetValue(GameMain.LightManager.LosAlpha); Color losColor; if (GameMain.LightManager.LosMode == LosMode.Transparent) @@ -362,6 +363,17 @@ namespace Barotrauma GameMain.LightManager.LosEffect.CurrentTechnique.Passes[0].Apply(); Quad.Render(); } + + float grainStrength = Character.Controlled?.GrainStrength ?? 0; + if (grainStrength > 0) + { + Rectangle screenRect = new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight); + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, effect: GrainEffect); + GUI.DrawRectangle(spriteBatch, screenRect, Color.White * grainStrength, isFilled: true); + GrainEffect.Parameters["seed"].SetValue(Rand.Range(0f, 1f, Rand.RandSync.Unsynced)); + spriteBatch.End(); + } + graphics.SetRenderTarget(null); float BlurStrength = 0.0f; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index 657f66ea8..62fcc7ba7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -296,7 +296,7 @@ namespace Barotrauma subInfo ??= SubmarineInfo.SavedSubmarines.GetRandom(s => s.IsPlayer && !s.HasTag(SubmarineTag.Shuttle) && !nonPlayerFiles.Any(f => f.Path.CleanUpPath().Equals(s.FilePath.CleanUpPath(), StringComparison.InvariantCultureIgnoreCase))); - GameSession gameSession = new GameSession(subInfo, "", GameModePreset.TestMode, null); + GameSession gameSession = new GameSession(subInfo, "", GameModePreset.TestMode, CampaignSettings.Empty, null); gameSession.StartRound(Level.Loaded.LevelData); (gameSession.GameMode as TestGameMode).OnRoundEnd = () => { @@ -489,7 +489,7 @@ namespace Barotrauma { MinValueFloat = 0, MaxValueFloat = 100, - FloatValue = caveGenerationParams.GetCommonness(selectedParams), + FloatValue = caveGenerationParams.GetCommonness(selectedParams, abyss: false), OnValueChanged = (numberInput) => { caveGenerationParams.OverrideCommonness[selectedParams.Identifier] = numberInput.FloatValue; @@ -514,7 +514,7 @@ namespace Barotrauma } foreach (var caveParam in CaveGenerationParams.CaveParams) { - if (selectedParams != null && caveParam.GetCommonness(selectedParams) <= 0.0f) { continue; } + if (selectedParams != null && caveParam.GetCommonness(selectedParams, abyss: false) <= 0.0f) { continue; } availableIdentifiers.Add(caveParam.Identifier); } availableIdentifiers.Reverse(); @@ -811,6 +811,19 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, new Vector2(0, crushDepthScreen), new Vector2(GameMain.GraphicsWidth, crushDepthScreen), GUI.Style.Red * 0.25f, width: 5); GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, crushDepthScreen), "Crush depth", GUI.Style.Red, backgroundColor: Color.Black); } + + float abyssStartScreen = cam.WorldToScreen(new Vector2(0.0f, Level.Loaded.AbyssArea.Bottom)).Y; + if (abyssStartScreen > 0.0f && abyssStartScreen < GameMain.GraphicsHeight) + { + GUI.DrawLine(spriteBatch, new Vector2(0, abyssStartScreen), new Vector2(GameMain.GraphicsWidth, abyssStartScreen), GUI.Style.Blue * 0.25f, width: 5); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, abyssStartScreen), "Abyss start", GUI.Style.Blue, backgroundColor: Color.Black); + } + float abyssEndScreen = cam.WorldToScreen(new Vector2(0.0f, Level.Loaded.AbyssArea.Y)).Y; + if (abyssEndScreen > 0.0f && abyssEndScreen < GameMain.GraphicsHeight) + { + GUI.DrawLine(spriteBatch, new Vector2(0, abyssEndScreen), new Vector2(GameMain.GraphicsWidth, abyssEndScreen), GUI.Style.Blue * 0.25f, width: 5); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, abyssEndScreen), "Abyss end", GUI.Style.Blue, backgroundColor: Color.Black); + } } GUI.Draw(Cam, spriteBatch); spriteBatch.End(); @@ -830,7 +843,7 @@ namespace Barotrauma pointerLightSource.Position = cam.ScreenToWorld(PlayerInput.MousePosition); pointerLightSource.Enabled = cursorLightEnabled.Selected; pointerLightSource.IsBackground = true; - cam.MoveCamera((float)deltaTime); + cam.MoveCamera((float)deltaTime, allowZoom: GUI.MouseOn == null); cam.UpdateTransform(); Level.Loaded?.Update((float)deltaTime, cam); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index 8fa32e1b4..0a990cf75 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -15,12 +15,26 @@ using System.Linq; using System.Net; using System.Threading; using System.Xml.Linq; +using Barotrauma.Steam; namespace Barotrauma { class MainMenuScreen : Screen { - public enum Tab { NewGame = 1, LoadGame = 2, HostServer = 3, Settings = 4, Tutorials = 5, JoinServer = 6, CharacterEditor = 7, SubmarineEditor = 8, QuickStartDev = 9, ProfilingTestBench = 10, SteamWorkshop = 11, Credits = 12, Empty = 13 } + public enum Tab + { + NewGame = 0, + LoadGame = 1, + HostServer = 2, + Settings = 3, + Tutorials = 4, + JoinServer = 5, + CharacterEditor = 6, + SubmarineEditor = 7, + SteamWorkshop = 8, + Credits = 9, + Empty = 10 + } private readonly GUIComponent buttonsParent; @@ -29,8 +43,7 @@ namespace Barotrauma private CampaignSetupUI campaignSetupUI; private GUITextBox serverNameBox, /*portBox, queryPortBox,*/ passwordBox, maxPlayersBox; - private GUITickBox isPublicBox, wrongPasswordBanBox, karmaEnabledBox; - private GUIDropDown karmaPresetDD; + private GUITickBox isPublicBox, wrongPasswordBanBox, karmaBox; private readonly GUIFrame downloadingModsContainer, enableModsContainer; private readonly GUIButton joinServerButton, hostServerButton, steamWorkshopButton; private readonly GameMain game; @@ -41,7 +54,7 @@ namespace Barotrauma private GUIComponent remoteContentContainer; private XDocument remoteContentDoc; - private Tab selectedTab; + private Tab selectedTab = Tab.Empty; private Sprite backgroundSprite; @@ -335,6 +348,16 @@ namespace Barotrauma CanBeFocused = false }; + new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), optionList.RectTransform), TextManager.Get("EditorDisclaimerWikiLink"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") + { + ForceUpperCase = true, + OnClicked = (button, userData) => + { + string url = TextManager.Get("EditorDisclaimerWikiUrl", returnNull: true) ?? "https://barotraumagame.com/wiki"; + GameMain.Instance.ShowOpenUrlInWebBrowserPrompt(url, promptExtensionTag: "wikinotice"); + return true; + } + }; new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), optionList.RectTransform), TextManager.Get("CreditsButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { ForceUpperCase = true, @@ -353,10 +376,13 @@ namespace Barotrauma "Quickstart (dev)", style: "GUIButtonLarge", color: GUI.Style.Red) { IgnoreLayoutGroups = true, - UserData = Tab.QuickStartDev, + UserData = Tab.Empty, OnClicked = (tb, userdata) => { SelectTab(tb, userdata); + + QuickStart(); + return true; } }; @@ -364,11 +390,32 @@ namespace Barotrauma "Profiling", style: "GUIButtonLarge", color: GUI.Style.Red) { IgnoreLayoutGroups = true, - UserData = Tab.ProfilingTestBench, + UserData = Tab.Empty, ToolTip = "Enables performance indicators and starts the game with a fixed sub, crew and level to make it easier to compare the performance between sessions.", OnClicked = (tb, userdata) => { SelectTab(tb, userdata); + + QuickStart(fixedSeed: true); + GameMain.ShowPerf = true; + GameMain.ShowFPS = true; + + return true; + } + }; + new GUIButton(new RectTransform(new Point(300, 30), Frame.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point(40, 180) }, + "Join Localhost", style: "GUIButtonLarge", color: GUI.Style.Red) + { + IgnoreLayoutGroups = true, + UserData = Tab.Empty, + ToolTip = "Connects to a locally hosted dedicated server, assuming default port.", + OnClicked = (tb, userdata) => + { + SelectTab(tb, userdata); + + GameMain.Client = new GameClient(string.IsNullOrEmpty(GameMain.Config.PlayerName) ? SteamManager.GetUsername() : GameMain.Config.PlayerName, + IPAddress.Loopback.ToString(), 0, "localhost", 0, false); + return true; } }; @@ -538,13 +585,13 @@ namespace Barotrauma case Tab.NewGame: if (GameMain.Config.ShowTutorialSkipWarning) { - selectedTab = 0; + selectedTab = Tab.Empty; ShowTutorialSkipWarning(Tab.NewGame); return true; } if (!GameMain.Config.CampaignDisclaimerShown) { - selectedTab = 0; + selectedTab = Tab.Empty; GameMain.Instance.ShowCampaignDisclaimer(() => { SelectTab(null, Tab.NewGame); }); return true; } @@ -564,13 +611,13 @@ namespace Barotrauma case Tab.JoinServer: if (GameMain.Config.ShowTutorialSkipWarning) { - selectedTab = 0; + selectedTab = Tab.Empty; ShowTutorialSkipWarning(Tab.JoinServer); return true; } if (!GameMain.Config.CampaignDisclaimerShown) { - selectedTab = 0; + selectedTab = Tab.Empty; GameMain.Instance.ShowCampaignDisclaimer(() => { SelectTab(null, Tab.JoinServer); }); return true; } @@ -580,18 +627,18 @@ namespace Barotrauma if (GameMain.Config.ContentPackageSelectionDirty) { new GUIMessageBox(TextManager.Get("RestartRequiredLabel"), TextManager.Get("ServerRestartRequiredContentPackage", fallBackTag: "RestartRequiredGeneric")); - selectedTab = 0; + selectedTab = Tab.Empty; return false; } if (GameMain.Config.ShowTutorialSkipWarning) { - selectedTab = 0; + selectedTab = Tab.Empty; ShowTutorialSkipWarning(tab); return true; } if (!GameMain.Config.CampaignDisclaimerShown) { - selectedTab = 0; + selectedTab = Tab.Empty; GameMain.Instance.ShowCampaignDisclaimer(() => { SelectTab(null, Tab.HostServer); }); return true; } @@ -599,7 +646,7 @@ namespace Barotrauma case Tab.Tutorials: if (!GameMain.Config.CampaignDisclaimerShown) { - selectedTab = 0; + selectedTab = Tab.Empty; GameMain.Instance.ShowCampaignDisclaimer(() => { SelectTab(null, Tab.Tutorials); }); return true; } @@ -612,14 +659,6 @@ namespace Barotrauma case Tab.SubmarineEditor: CoroutineManager.StartCoroutine(SelectScreenWithWaitCursor(GameMain.SubEditorScreen)); break; - case Tab.QuickStartDev: - QuickStart(); - break; - case Tab.ProfilingTestBench: - QuickStart(fixedSeed: true); - GameMain.ShowPerf = true; - GameMain.ShowFPS = true; - break; case Tab.SteamWorkshop: if (!Steam.SteamManager.IsInitialized) return false; CoroutineManager.StartCoroutine(SelectScreenWithWaitCursor(GameMain.SteamWorkshopScreen)); @@ -630,7 +669,7 @@ namespace Barotrauma break; case Tab.Empty: titleText.Visible = true; - selectedTab = 0; + selectedTab = Tab.Empty; break; } @@ -709,7 +748,7 @@ namespace Barotrauma var gamesession = new GameSession( selectedSub, GameModePreset.DevSandbox, - missionPrefab: null); + missionPrefabs: null); //(gamesession.GameMode as SinglePlayerCampaign).GenerateMap(ToolBox.RandomSeed(8)); gamesession.StartRound(fixedSeed ? "abcd" : ToolBox.RandomSeed(8), difficulty: 40); GameMain.GameScreen.Select(); @@ -886,8 +925,8 @@ namespace Barotrauma " -public " + isPublicBox.Selected.ToString() + " -playstyle " + ((PlayStyle)playstyleBanner.UserData).ToString() + " -banafterwrongpassword " + wrongPasswordBanBox.Selected.ToString() + - " -karmaenabled " + karmaEnabledBox.Selected.ToString() + - " -karmapreset " + (karmaPresetDD.SelectedData?.ToString() ?? "default") + + " -karmaenabled " + (!karmaBox.Selected).ToString() + + " -karmapreset default" + " -maxplayers " + maxPlayersBox.Text; if (!string.IsNullOrWhiteSpace(passwordBox.Text)) @@ -946,7 +985,7 @@ namespace Barotrauma public override void AddToGUIUpdateList() { Frame.AddToGUIUpdateList(); - if (selectedTab > 0 && menuTabs[(int)selectedTab] != null) + if (selectedTab < Tab.Empty && menuTabs[(int)selectedTab] != null) { menuTabs[(int)selectedTab].AddToGUIUpdateList(); } @@ -1043,7 +1082,7 @@ namespace Barotrauma spriteBatch.End(); } - private void StartGame(SubmarineInfo selectedSub, string saveName, string mapSeed) + private void StartGame(SubmarineInfo selectedSub, string saveName, string mapSeed, CampaignSettings settings) { if (string.IsNullOrEmpty(saveName)) return; @@ -1082,7 +1121,7 @@ namespace Barotrauma selectedSub = new SubmarineInfo(Path.Combine(SaveUtil.TempPath, selectedSub.Name + ".sub")); - GameMain.GameSession = new GameSession(selectedSub, saveName, GameModePreset.SinglePlayerCampaign, mapSeed); + GameMain.GameSession = new GameSession(selectedSub, saveName, GameModePreset.SinglePlayerCampaign, settings, mapSeed); ((SinglePlayerCampaign)GameMain.GameSession.GameMode).LoadNewLevel(); } @@ -1134,6 +1173,10 @@ namespace Barotrauma (int)(campaignSetupUI.StartButton.TextBlock.TextSize.X * 1.5f), campaignSetupUI.StartButton.RectTransform.MinSize.Y); startButtonContainer.RectTransform.MinSize = new Point(0, campaignSetupUI.StartButton.RectTransform.MinSize.Y); + if (campaignSetupUI.EnableRadiationToggle != null) + { + campaignSetupUI.EnableRadiationToggle.RectTransform.Parent = startButtonContainer.RectTransform; + } campaignSetupUI.InitialMoneyText.RectTransform.Parent = startButtonContainer.RectTransform; } @@ -1141,9 +1184,11 @@ namespace Barotrauma { menuTabs[(int)Tab.HostServer].ClearChildren(); - int port = NetConfig.DefaultPort; - int queryPort = NetConfig.DefaultQueryPort; + string name = ""; + string password = ""; int maxPlayers = 8; + bool isPublic = true; + bool banAfterWrongPassword = false; bool karmaEnabled = true; string selectedKarmaPreset = ""; PlayStyle selectedPlayStyle = PlayStyle.Casual; @@ -1152,8 +1197,10 @@ namespace Barotrauma XDocument settingsDoc = XMLExtensions.TryLoadXml(ServerSettings.SettingsFile); if (settingsDoc != null) { - port = settingsDoc.Root.GetAttributeInt("port", port); - queryPort = settingsDoc.Root.GetAttributeInt("queryport", queryPort); + name = settingsDoc.Root.GetAttributeString("name", name); + password = settingsDoc.Root.GetAttributeString("password", password); + isPublic = settingsDoc.Root.GetAttributeBool("public", isPublic); + banAfterWrongPassword = settingsDoc.Root.GetAttributeBool("banafterwrongpassword", banAfterWrongPassword); int maxPlayersElement = settingsDoc.Root.GetAttributeInt("maxplayers", maxPlayers); if (maxPlayersElement > NetConfig.MaxPlayers) @@ -1255,30 +1302,12 @@ namespace Barotrauma new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), content.RectTransform), style: null); var label = new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), TextManager.Get("ServerName"), textAlignment: textAlignment); - serverNameBox = new GUITextBox(new RectTransform(textFieldSize, label.RectTransform, Anchor.CenterRight), textAlignment: textAlignment) + serverNameBox = new GUITextBox(new RectTransform(textFieldSize, label.RectTransform, Anchor.CenterRight), text: name, textAlignment: textAlignment) { MaxTextLength = NetConfig.ServerNameMaxLength, OverflowClip = true }; label.RectTransform.MaxSize = serverNameBox.RectTransform.MaxSize; - - /* TODO: allow lidgren servers from client? - label = new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), TextManager.Get("ServerPort"), textAlignment: textAlignment); - portBox = new GUITextBox(new RectTransform(textFieldSize, label.RectTransform, Anchor.CenterRight), textAlignment: textAlignment) - { - Text = port.ToString(), - ToolTip = TextManager.Get("ServerPortToolTip") - }; - -#if USE_STEAM - label = new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), TextManager.Get("ServerQueryPort"), textAlignment: textAlignment); - queryPortBox = new GUITextBox(new RectTransform(textFieldSize, label.RectTransform, Anchor.CenterRight), textAlignment: textAlignment) - { - Text = queryPort.ToString(), - ToolTip = TextManager.Get("ServerQueryPortToolTip") - }; -#endif - */ var maxPlayersLabel = new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), TextManager.Get("MaxPlayers"), textAlignment: textAlignment); var buttonContainer = new GUILayoutGroup(new RectTransform(textFieldSize, maxPlayersLabel.RectTransform, Anchor.CenterRight), isHorizontal: true) @@ -1304,7 +1333,7 @@ namespace Barotrauma maxPlayersLabel.RectTransform.MaxSize = maxPlayersBox.RectTransform.MaxSize; label = new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), TextManager.Get("Password"), textAlignment: textAlignment); - passwordBox = new GUITextBox(new RectTransform(textFieldSize, label.RectTransform, Anchor.CenterRight), textAlignment: textAlignment) + passwordBox = new GUITextBox(new RectTransform(textFieldSize, label.RectTransform, Anchor.CenterRight), text: password, textAlignment: textAlignment) { Censor = true }; @@ -1316,10 +1345,14 @@ namespace Barotrauma isPublicBox = new GUITickBox(new RectTransform(new Vector2(0.5f, 1.0f), tickboxAreaUpper.RectTransform), TextManager.Get("PublicServer")) { + Selected = isPublic, ToolTip = TextManager.Get("PublicServerToolTip") }; - wrongPasswordBanBox = new GUITickBox(new RectTransform(new Vector2(0.5f, 1.0f), tickboxAreaUpper.RectTransform), TextManager.Get("ServerSettingsBanAfterWrongPassword")); + wrongPasswordBanBox = new GUITickBox(new RectTransform(new Vector2(0.5f, 1.0f), tickboxAreaUpper.RectTransform), TextManager.Get("ServerSettingsBanAfterWrongPassword")) + { + Selected = banAfterWrongPassword + }; tickboxAreaUpper.RectTransform.MaxSize = isPublicBox.RectTransform.MaxSize; @@ -1327,31 +1360,13 @@ namespace Barotrauma var tickboxAreaLower = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, tickBoxSize.Y), parent.RectTransform), isHorizontal: true); - karmaEnabledBox = new GUITickBox(new RectTransform(new Vector2(0.5f, 1.0f), tickboxAreaLower.RectTransform), TextManager.Get("ServerSettingsUseKarma")) + karmaBox = new GUITickBox(new RectTransform(new Vector2(0.5f, 1.0f), tickboxAreaLower.RectTransform), TextManager.Get("HostServerKarmaSetting")) { - ToolTip = TextManager.Get("karmaexplanation"), - OnSelected = (tb) => - { - karmaPresetDD.Enabled = karmaPresetDD.ButtonEnabled = tb.Selected; - return true; - } + Selected = !karmaEnabled, + ToolTip = TextManager.Get("hostserverkarmasettingtooltip") }; - karmaPresetDD = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1.0f), tickboxAreaLower.RectTransform)) - { - ButtonEnabled = false, - Enabled = false - }; - var tempKarmaManager = new KarmaManager(); - foreach (string karmaPreset in tempKarmaManager.Presets.Keys) - { - karmaPresetDD.AddItem(TextManager.Get("KarmaPreset." + karmaPreset), karmaPreset); - if (karmaPreset == selectedKarmaPreset) { karmaPresetDD.SelectItem(karmaPreset); } - } - if (karmaPresetDD.SelectedIndex == -1) { karmaPresetDD.Select(0); } - karmaEnabledBox.Selected = karmaEnabled; - - tickboxAreaLower.RectTransform.MaxSize = karmaEnabledBox.RectTransform.MaxSize; + tickboxAreaLower.RectTransform.MaxSize = karmaBox.RectTransform.MaxSize; //spacing new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), content.RectTransform), style: null); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index cc0a43c17..f32b61d06 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -27,6 +27,8 @@ namespace Barotrauma private GUIComponent jobVariantTooltip; + private SubmarinePreview submarinePreview; + private readonly GUITextBox chatInput; private readonly GUITextBox serverLogFilter; public GUITextBox ChatInput @@ -41,6 +43,8 @@ namespace Barotrauma private readonly GUIScrollBar levelDifficultyScrollBar; + private readonly GUITickBox radiationEnabledTickBox; + private readonly GUIButton[] traitorProbabilityButtons; private readonly GUITextBlock traitorProbabilityText; @@ -128,6 +132,12 @@ namespace Barotrauma get; private set; } + + public GUITickBox Favorite + { + get; + private set; + } public GUITextBox ServerMessage { @@ -144,6 +154,8 @@ namespace Barotrauma private readonly GUIButton showChatButton; private readonly GUIButton showLogButton; + private readonly GUITextBlock publicOrPrivate; + public GUIListBox SubList { get { return subList; } @@ -473,6 +485,7 @@ namespace Barotrauma if (socialHolder != null) { socialHolder.Visible = false; } if (!(serverLogHolder?.Visible ?? true)) { + if (GameMain.Client?.ServerSettings?.ServerLog == null) { return false; } serverLogHolder.Visible = true; GameMain.Client.ServerSettings.ServerLog.AssignLogFrame(serverLogReverseButton, serverLogBox, serverLogFilterTicks.Content, serverLogFilter); } @@ -663,6 +676,27 @@ namespace Barotrauma }; clientReadonlyElements.Add(ServerName); + Favorite = new GUITickBox(new RectTransform(new Vector2(1.0f, 1.0f), lobbyHeader.RectTransform, scaleBasis: ScaleBasis.BothHeight), + "", null, "GUIServerListFavoriteTickBox") + { + Selected = false, + ToolTip = TextManager.Get("addtofavorites"), + OnSelected = (tickbox) => + { + ServerInfo info = GameMain.Client.ServerSettings.GetServerListInfo(); + if (tickbox.Selected) + { + GameMain.ServerListScreen.AddToFavoriteServers(info); + } + else + { + GameMain.ServerListScreen.RemoveFromFavoriteServers(info); + } + tickbox.ToolTip = TextManager.Get(tickbox.Selected ? "removefromfavorites" : "addtofavorites"); + return true; + } + }; + SettingsButton = new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), lobbyHeader.RectTransform, Anchor.TopRight), TextManager.Get("ServerSettingsButton")); clientHiddenElements.Add(SettingsButton); @@ -691,6 +725,12 @@ namespace Barotrauma CanBeFocused = false }; + publicOrPrivate = new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), serverBanner.RectTransform, Anchor.BottomRight, Pivot.BottomRight), + "", font: GUI.SmallFont, textAlignment: Alignment.Center, textColor: Color.White, style: "GUISlopedHeader") + { + CanBeFocused = false + }; + var serverMessageContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.75f), serverInfoHolder.RectTransform)); ServerMessage = new GUITextBox(new RectTransform(Vector2.One, serverMessageContainer.Content.RectTransform), style: "GUITextBoxNoBorder") { @@ -1095,6 +1135,20 @@ namespace Barotrauma } }; + if (MapGenerationParams.Instance.RadiationParams != null) + { + radiationEnabledTickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), TextManager.Get("CampaignOption.EnableRadiation"), font: GUI.Style.Font) + { + Selected = true, + OnSelected = box => + { + GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, radiationEnabled: box.Selected); + return true; + } + }; + } + + List settingsElements = settingsContent.Children.ToList(); for (int i = 0; i < settingsElements.Count; i++) { @@ -1226,6 +1280,11 @@ namespace Barotrauma base.Select(); } + + public void SetPublic(bool isPublic) + { + publicOrPrivate.Text = isPublic ? TextManager.Get("PublicLobbyTag") : TextManager.Get("PrivateLobbyTag"); + } public void RefreshEnabledElements() { @@ -1238,6 +1297,10 @@ namespace Barotrauma } SeedBox.Enabled = !CampaignFrame.Visible && !CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.ManageSettings); levelDifficultyScrollBar.Enabled = !CampaignFrame.Visible && !CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + if (radiationEnabledTickBox != null) + { + radiationEnabledTickBox.Enabled = CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + } traitorProbabilityButtons[0].Enabled = traitorProbabilityButtons[1].Enabled = traitorProbabilityText.Enabled = !CampaignFrame.Visible && !CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.ManageSettings); botCountButtons[0].Enabled = botCountButtons[1].Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); @@ -1250,9 +1313,9 @@ namespace Barotrauma StartButton.Visible = GameMain.Client.HasPermission(ClientPermissions.ManageRound) && !GameMain.Client.GameStarted && !CampaignSetupFrame.Visible && !CampaignFrame.Visible; ServerName.Readonly = !GameMain.Client.HasPermission(ClientPermissions.ManageSettings); ServerMessage.Readonly = !GameMain.Client.HasPermission(ClientPermissions.ManageSettings); - shuttleTickBox.Enabled = !CampaignFrame.Visible && !CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + shuttleTickBox.Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); SubList.Enabled = !CampaignFrame.Visible && (GameMain.Client.ServerSettings.Voting.AllowSubVoting || GameMain.Client.HasPermission(ClientPermissions.SelectSub)); - shuttleList.Enabled = shuttleList.ButtonEnabled = shuttleTickBox.Enabled = !CampaignFrame.Visible && !CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.SelectSub); + shuttleList.Enabled = shuttleList.ButtonEnabled = GameMain.Client.HasPermission(ClientPermissions.SelectSub); ModeList.Enabled = GameMain.Client.ServerSettings.Voting.AllowModeVoting || GameMain.Client.HasPermission(ClientPermissions.SelectMode); LogButtons.Visible = GameMain.Client.HasPermission(ClientPermissions.ServerLog); GameMain.Client.ShowLogButton.Visible = GameMain.Client.HasPermission(ClientPermissions.ServerLog); @@ -1298,7 +1361,7 @@ namespace Barotrauma public void CreatePlayerFrame(GUIComponent parent) { UpdatePlayerFrame( - playerInfoContainer.Children?.First().UserData as CharacterInfo, + Character.Controlled?.Info ?? playerInfoContainer.Children?.First().UserData as CharacterInfo, allowEditing: campaignCharacterInfo == null, parent: parent); } @@ -1962,6 +2025,17 @@ namespace Barotrauma if (child != null) { playerList.RemoveChild(child); } } + public void SelectPlayer(GUITextBlock component, GUITextBlock.ClickableArea area) + { + if (!UInt64.TryParse(area.Data.Metadata, out UInt64 id)) { return; } + Client client = GameMain.Client.ConnectedClients.Find(c => c.SteamID == id) + ?? GameMain.Client.ConnectedClients.Find(c => c.ID == id) + ?? GameMain.Client.PreviouslyConnectedClients.FirstOrDefault(c => c.SteamID == id) + ?? GameMain.Client.PreviouslyConnectedClients.FirstOrDefault(c => c.ID == id); + if (client == null) { return; } + GameMain.NetLobbyScreen.SelectPlayer(client); + } + public bool SelectPlayer(Client selectedClient) { bool myClient = selectedClient.ID == GameMain.Client.ID; @@ -2460,6 +2534,8 @@ namespace Barotrauma component.ToolTip = TextManager.Get("servertagdescription." + playStyle); } + + publicOrPrivate.RectTransform.NonScaledSize = (publicOrPrivate.Font.MeasureString(publicOrPrivate.Text) + new Vector2(25, 8) * GUI.Scale).ToPoint(); } private void DrawJobVariantItems(SpriteBatch spriteBatch, GUICustomComponent component, Pair jobPrefab, int itemsPerRow) @@ -2537,11 +2613,24 @@ namespace Barotrauma text: ChatMessage.GetTimeStamp() + (message.Type == ChatMessageType.Private ? TextManager.Get("PrivateMessageTag") + " " : "") + message.TextWithSender, textColor: message.Color, color: ((chatBox.CountChildren % 2) == 0) ? Color.Transparent : Color.Black * 0.1f, - wrap: true, font: GUI.SmallFont) + wrap: true, font: GUI.SmallFont, + parseRichText: true) { UserData = message, CanBeFocused = false }; + msg.CalculateHeightFromText(); + if (msg.RichTextData != null) + { + foreach (var data in msg.RichTextData) + { + msg.ClickableAreas.Add(new GUITextBlock.ClickableArea() + { + Data = data, + OnClick = GameMain.NetLobbyScreen.SelectPlayer + }); + } + } msg.RectTransform.SizeChanged += Recalculate; void Recalculate() { @@ -3463,29 +3552,35 @@ namespace Barotrauma new string[3] { sub.Name, sub.MD5Hash.ShortHash, Md5Hash.GetShortHash(md5Hash) }) + " "; } - errorMsg += TextManager.Get("DownloadSubQuestion"); - //already showing a message about the same sub if (GUIMessageBox.MessageBoxes.Any(mb => mb.UserData as string == "request" + subName)) { return false; } - var requestFileBox = new GUIMessageBox(TextManager.Get("DownloadSubLabel"), errorMsg, - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }) + if (GameMain.Client.ServerSettings.AllowFileTransfers) { - UserData = "request" + subName - }; - requestFileBox.Buttons[0].UserData = new string[] { subName, md5Hash }; - requestFileBox.Buttons[0].OnClicked += requestFileBox.Close; - requestFileBox.Buttons[0].OnClicked += (GUIButton button, object userdata) => - { - string[] fileInfo = (string[])userdata; - GameMain.Client?.RequestFile(FileTransferType.Submarine, fileInfo[0], fileInfo[1]); - return true; - }; - requestFileBox.Buttons[1].OnClicked += requestFileBox.Close; + errorMsg += TextManager.Get("DownloadSubQuestion"); + var requestFileBox = new GUIMessageBox(TextManager.Get("DownloadSubLabel"), errorMsg, + new string[] { TextManager.Get("Yes"), TextManager.Get("No") }) + { + UserData = "request" + subName + }; + requestFileBox.Buttons[0].UserData = new string[] { subName, md5Hash }; + requestFileBox.Buttons[0].OnClicked += requestFileBox.Close; + requestFileBox.Buttons[0].OnClicked += (GUIButton button, object userdata) => + { + string[] fileInfo = (string[])userdata; + GameMain.Client?.RequestFile(FileTransferType.Submarine, fileInfo[0], fileInfo[1]); + return true; + }; + requestFileBox.Buttons[1].OnClicked += requestFileBox.Close; + } + else + { + new GUIMessageBox(TextManager.Get("DownloadSubLabel"), errorMsg); + } return false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs index 80e3a1c02..9924096cf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs @@ -311,7 +311,7 @@ namespace Barotrauma public override void Update(double deltaTime) { - cam.MoveCamera((float)deltaTime, true); + cam.MoveCamera((float)deltaTime, allowMove: true, allowZoom: GUI.MouseOn == null); if (selectedPrefab != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs index f408c64ba..7c8aa1a6f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs @@ -43,6 +43,13 @@ namespace Barotrauma private Dictionary pendingWorkshopDownloads = null; private string autoConnectName; private string autoConnectEndpoint; + private enum TernaryOption + { + Any, + Enabled, + Disabled + } + private class FriendInfo { public UInt64 SteamID; @@ -131,7 +138,7 @@ namespace Barotrauma private bool masterServerResponded; private IRestResponse masterServerResponse; - + private readonly float[] columnRelativeWidth = new float[] { 0.1f, 0.1f, 0.7f, 0.12f, 0.08f, 0.08f }; private readonly string[] columnLabel = new string[] { "ServerListCompatible", "ServerListHasPassword", "ServerListName", "ServerListRoundStarted", "ServerListPlayers", "ServerListPing" }; @@ -146,16 +153,19 @@ namespace Barotrauma private GUITickBox filterFull; private GUITickBox filterEmpty; private GUITickBox filterWhitelisted; - private GUITickBox filterFriendlyFire; - private GUITickBox filterKarma; - private GUITickBox filterTraitor; - private GUITickBox filterModded; - private GUITickBox filterVoip; + private Dictionary ternaryFilters; private Dictionary filterTickBoxes; private Dictionary playStyleTickBoxes; private Dictionary gameModeTickBoxes; private GUITickBox filterOffensive; + //GUIDropDown sends the OnSelected event before SelectedData is set, so we have to cache it manually. + private TernaryOption filterFriendlyFireValue = TernaryOption.Any; + private TernaryOption filterKarmaValue = TernaryOption.Any; + private TernaryOption filterTraitorValue = TernaryOption.Any; + private TernaryOption filterVoipValue = TernaryOption.Any; + private TernaryOption filterModdedValue = TernaryOption.Any; + private string sortedBy; private GUIButton serverPreviewToggleButton; @@ -173,6 +183,49 @@ namespace Barotrauma CreateUI(); } + private void AddTernaryFilter(RectTransform parent, float elementHeight, string tag, Action valueSetter) + { + var filterLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, elementHeight), parent), isHorizontal: true) + { + Stretch = true + }; + + var box = new GUIFrame(new RectTransform(Vector2.One, filterLayoutGroup.RectTransform, Anchor.CenterLeft, scaleBasis: ScaleBasis.BothHeight) + { + IsFixedSize = true, + }, null) + { + HoverColor = Color.Gray, + SelectedColor = Color.DarkGray, + CanBeFocused = false + }; + if (box.RectTransform.MinSize.Y > 0) + { + box.RectTransform.MinSize = new Point(box.RectTransform.MinSize.Y); + box.RectTransform.Resize(box.RectTransform.MinSize); + } + Vector2 textBlockScale = new Vector2((float)(filterLayoutGroup.Rect.Width - filterLayoutGroup.Rect.Height) / (float)Math.Max(filterLayoutGroup.Rect.Width, 1.0), 1.0f); + + var filterLabel = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f) * textBlockScale, filterLayoutGroup.RectTransform, Anchor.CenterLeft), TextManager.Get("servertag." + tag + ".label"), textAlignment: Alignment.CenterLeft) + { + UserData = TextManager.Get("servertag." + tag + ".label") + }; + GUI.Style.Apply(filterLabel, "GUITextBlock", null); + + var dropDown = new GUIDropDown(new RectTransform(new Vector2(0.4f, 1.0f) * textBlockScale, filterLayoutGroup.RectTransform, Anchor.CenterLeft), elementCount: 3); + dropDown.AddItem(TextManager.Get("any"), TernaryOption.Any); + dropDown.AddItem(TextManager.Get("servertag." + tag + ".true"), TernaryOption.Enabled, TextManager.Get("servertagdescription." + tag + ".true")); + dropDown.AddItem(TextManager.Get("servertag." + tag + ".false"), TernaryOption.Disabled, TextManager.Get("servertagdescription." + tag + ".false")); + dropDown.SelectItem(TernaryOption.Any); + dropDown.OnSelected = (_, data) => { + valueSetter((TernaryOption)data); + FilterServers(); + return true; + }; + + ternaryFilters.Add(tag, dropDown); + } + private void CreateUI() { menu = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.85f), GUI.Canvas, Anchor.Center) { MinSize = new Point(GameMain.GraphicsHeight, 0) }); @@ -323,6 +376,7 @@ namespace Barotrauma }; filterToggle.Children.ForEach(c => c.SpriteEffects = SpriteEffects.FlipHorizontally); + ternaryFilters = new Dictionary(); filterTickBoxes = new Dictionary(); filterSameVersion = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterSameVersion")) @@ -382,40 +436,11 @@ namespace Barotrauma CanBeFocused = false }; - filterKarma = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("servertag.karma.true")) - { - UserData = TextManager.Get("servertag.karma.true"), - OnSelected = (tickBox) => { FilterServers(); return true; } - }; - filterTickBoxes.Add("servertag.karma", filterKarma); - - filterTraitor = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("servertag.traitors.true")) - { - UserData = TextManager.Get("servertag.traitors.true"), - OnSelected = (tickBox) => { FilterServers(); return true; } - }; - filterTickBoxes.Add("servertag.traitors", filterTraitor); - - filterFriendlyFire = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("servertag.friendlyfire.false")) - { - UserData = TextManager.Get("servertag.friendlyfire.false"), - OnSelected = (tickBox) => { FilterServers(); return true; } - }; - filterTickBoxes.Add("servertag.friendlyfire", filterFriendlyFire); - - filterVoip = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("servertag.voip.false")) - { - UserData = TextManager.Get("servertag.voip.false"), - OnSelected = (tickBox) => { FilterServers(); return true; } - }; - filterTickBoxes.Add("servertag.voip", filterVoip); - - filterModded = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("servertag.modded.true")) - { - UserData = TextManager.Get("servertag.modded.true"), - OnSelected = (tickBox) => { FilterServers(); return true; } - }; - filterTickBoxes.Add("servertag.modded", filterModded); + AddTernaryFilter(filters.Content.RectTransform, elementHeight, "karma", (value) => { filterKarmaValue = value; }); + AddTernaryFilter(filters.Content.RectTransform, elementHeight, "traitors", (value) => { filterTraitorValue = value; }); + AddTernaryFilter(filters.Content.RectTransform, elementHeight, "friendlyfire", (value) => { filterFriendlyFireValue = value; }); + AddTernaryFilter(filters.Content.RectTransform, elementHeight, "voip", (value) => { filterVoipValue = value; }); + AddTernaryFilter(filters.Content.RectTransform, elementHeight, "modded", (value) => { filterModdedValue = value; }); // Play Style Selection new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("ServerSettingsPlayStyle"), font: GUI.SubHeadingFont) @@ -462,7 +487,10 @@ namespace Barotrauma filterTickBoxes.ForEach(t => t.Value.Text = t.Value.UserData as string); gameModeTickBoxes.ForEach(tb => tb.Value.Text = tb.Value.ToolTip); playStyleTickBoxes.ForEach(tb => tb.Value.Text = tb.Value.ToolTip); - GUITextBlock.AutoScaleAndNormalize(filterTickBoxes.Values.Select(tb => tb.TextBlock), defaultScale: 1.0f); + GUITextBlock.AutoScaleAndNormalize( + filterTickBoxes.Values.Select(tb => tb.TextBlock) + .Concat(ternaryFilters.Values.Select(dd => dd.Parent.GetChild())), + defaultScale: 1.0f); if (filterTickBoxes.Values.First().TextBlock.TextScale < 0.8f) { filterTickBoxes.ForEach(t => t.Value.TextBlock.TextScale = 1.0f); @@ -751,28 +779,19 @@ namespace Barotrauma doc.SaveSafe(file); } - public ServerInfo UpdateServerInfoWithServerSettings(object endpoint, ServerSettings serverSettings) + public ServerInfo UpdateServerInfoWithServerSettings(NetworkConnection endpoint, ServerSettings serverSettings) { UInt64 steamId = 0; string ip = ""; string port = ""; - if (endpoint is UInt64 id) { steamId = id; } - else if (endpoint is string strEndpoint) + if (endpoint is SteamP2PConnection steamP2PConnection) { steamId = steamP2PConnection.SteamID; } + else if (endpoint is LidgrenConnection lidgrenConnection) { - string[] address = strEndpoint.Split(':'); - if (address.Length == 1) - { - ip = strEndpoint; - port = NetConfig.DefaultPort.ToString(); - } - else - { - ip = string.Join(":", address.Take(address.Length - 1)); - port = address[address.Length - 1]; - } + ip = lidgrenConnection.IPString; + port = lidgrenConnection.Port.ToString(); } bool isInfoNew = false; - ServerInfo info = serverList.Content.FindChild(d => (d.UserData is ServerInfo serverInfo) && serverInfo != null && + ServerInfo info = serverList.Content.FindChild(d => (d.UserData is ServerInfo serverInfo) && (steamId != 0 ? steamId == serverInfo.OwnerID : (ip == serverInfo.IP && port == serverInfo.Port)))?.UserData as ServerInfo; if (info == null) { @@ -821,7 +840,7 @@ namespace Barotrauma } info.Recent = true; - ServerInfo existingInfo = recentServers.Find(serverInfo => info.OwnerID == serverInfo.OwnerID && (info.OwnerID != 0 ? true : (info.IP == serverInfo.IP && info.Port == serverInfo.Port))); + ServerInfo existingInfo = recentServers.Find(info.MatchesByEndpoint); if (existingInfo == null) { recentServers.Add(info); @@ -835,10 +854,15 @@ namespace Barotrauma WriteServerMemToFile(recentServersFile, recentServers); } + public bool IsFavorite(ServerInfo info) + { + return favoriteServers.Any(info.MatchesByEndpoint); + } + public void AddToFavoriteServers(ServerInfo info) { info.Favorite = true; - ServerInfo existingInfo = favoriteServers.Find(serverInfo => info.OwnerID == serverInfo.OwnerID && (info.OwnerID != 0 ? true : (info.IP == serverInfo.IP && info.Port == serverInfo.Port))); + ServerInfo existingInfo = favoriteServers.Find(info.MatchesByEndpoint); if (existingInfo == null) { favoriteServers.Add(info); @@ -855,7 +879,7 @@ namespace Barotrauma public void RemoveFromFavoriteServers(ServerInfo info) { info.Favorite = false; - ServerInfo existingInfo = favoriteServers.Find(serverInfo => info.OwnerID == serverInfo.OwnerID && (info.OwnerID != 0 ? true : (info.IP == serverInfo.IP && info.Port == serverInfo.Port))); + ServerInfo existingInfo = favoriteServers.Find(info.MatchesByEndpoint); if (existingInfo != null) { favoriteServers.Remove(existingInfo); @@ -1076,9 +1100,15 @@ namespace Barotrauma else { bool incompatible = - (!serverInfo.ContentPackageHashes.Any() && serverInfo.ContentPackagesMatch()) || + (serverInfo.ContentPackageHashes.Any() && !serverInfo.ContentPackagesMatch()) || (remoteVersion != null && !NetworkMember.IsCompatible(GameMain.Version, remoteVersion)); + var karmaFilterPassed = filterKarmaValue == TernaryOption.Any|| (filterKarmaValue == TernaryOption.Enabled) == serverInfo.KarmaEnabled; + var friendlyFireFilterPassed = filterFriendlyFireValue == TernaryOption.Any || (filterFriendlyFireValue == TernaryOption.Enabled) == serverInfo.FriendlyFireEnabled; + var traitorsFilterPassed = filterTraitorValue == TernaryOption.Any || (filterTraitorValue == TernaryOption.Enabled) == (serverInfo.TraitorsEnabled == YesNoMaybe.Yes || serverInfo.TraitorsEnabled == YesNoMaybe.Maybe); + var voipFilterPassed = filterVoipValue == TernaryOption.Any || (filterVoipValue == TernaryOption.Enabled) == serverInfo.VoipEnabled; + var moddedFilterPassed = filterModdedValue == TernaryOption.Any || (filterModdedValue == TernaryOption.Enabled) == serverInfo.GetPlayStyleTags().Any(t => t.Contains("modded.true")); + child.Visible = serverInfo.OwnerVerified && serverInfo.ServerName.Contains(searchBox.Text, StringComparison.OrdinalIgnoreCase) && @@ -1089,11 +1119,11 @@ namespace Barotrauma (!filterEmpty.Selected || serverInfo.PlayerCount > 0) && (!filterWhitelisted.Selected || serverInfo.UsingWhiteList == true) && (!filterOffensive.Selected || !ForbiddenWordFilter.IsForbidden(serverInfo.ServerName)) && - (!filterKarma.Selected || serverInfo.KarmaEnabled == true) && - (!filterFriendlyFire.Selected || serverInfo.FriendlyFireEnabled == false) && - (!filterTraitor.Selected || serverInfo.TraitorsEnabled == YesNoMaybe.Yes || serverInfo.TraitorsEnabled == YesNoMaybe.Maybe) && - (!filterVoip.Selected || serverInfo.VoipEnabled == false) && - (!filterModded.Selected || serverInfo.GetPlayStyleTags().Any(t => t.Contains("modded.true"))) && + karmaFilterPassed && + friendlyFireFilterPassed && + traitorsFilterPassed && + voipFilterPassed && + moddedFilterPassed && ((selectedTab == ServerListTab.All && (serverInfo.LobbyID != 0 || !string.IsNullOrWhiteSpace(serverInfo.Port))) || (selectedTab == ServerListTab.Recent && serverInfo.Recent) || (selectedTab == ServerListTab.Favorites && serverInfo.Favorite)); @@ -1224,8 +1254,7 @@ namespace Barotrauma }; var serverFrame = serverList.Content.FindChild(d => (d.UserData is ServerInfo info) && - info.OwnerID == serverInfo.OwnerID && - (serverInfo.OwnerID != 0 ? true : (info.IP == serverInfo.IP && info.Port == serverInfo.Port))); + info.MatchesByEndpoint(serverInfo)); if (serverFrame != null) { @@ -2334,6 +2363,10 @@ namespace Barotrauma { element.Add(new XAttribute(filterBox.Key, filterBox.Value.Selected.ToString())); } + foreach (KeyValuePair ternaryFilter in ternaryFilters) + { + element.Add(new XAttribute(ternaryFilter.Key, ternaryFilter.Value.SelectedData.ToString())); + } } public void LoadServerFilters(XElement element) @@ -2344,6 +2377,15 @@ namespace Barotrauma { filterBox.Value.Selected = element.GetAttributeBool(filterBox.Key, filterBox.Value.Selected); } + foreach (KeyValuePair ternaryFilter in ternaryFilters) + { + string valueStr = element.GetAttributeString(ternaryFilter.Key, ""); + TernaryOption ternaryOption = (TernaryOption)ternaryFilter.Value.SelectedData; + Enum.TryParse(valueStr, true, out ternaryOption); + + var child = ternaryFilter.Value.ListBox.Content.GetChildByUserData(ternaryOption); + ternaryFilter.Value.Select(ternaryFilter.Value.ListBox.Content.GetChildIndex(child)); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs index 274a3da32..74ccd5434 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs @@ -410,9 +410,9 @@ namespace Barotrauma if (sub.HasTag(SubmarineTag.HideInMenus)) { continue; } string subPath = Path.GetFullPath(sub.FilePath); - //ignore subs that are part of the vanilla content package + //ignore files that are part of the vanilla content package if (GameMain.VanillaContent != null && - GameMain.VanillaContent.GetFilesOfType(ContentType.Submarine).Any(s => Path.GetFullPath(s) == subPath)) + GameMain.VanillaContent.Files.Any(s => Path.GetFullPath(s.Path) == subPath)) { continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 539bed59b..f6c80e961 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -64,6 +64,7 @@ namespace Barotrauma public GUIComponent TopPanel; private GUIComponent showEntitiesPanel, entityCountPanel; private readonly List showEntitiesTickBoxes = new List(); + private readonly Dictionary hiddenSubCategories = new Dictionary(); private GUITextBlock subNameLabel; @@ -74,7 +75,7 @@ namespace Barotrauma private string lastFilter; public GUIComponent EntityMenu; private GUITextBox entityFilterBox; - private GUIListBox entityList; + private GUIListBox categorizedEntityList, allEntityList; private GUIButton toggleEntityMenuButton; public GUIButton ToggleEntityMenuButton => toggleEntityMenuButton; @@ -110,6 +111,8 @@ namespace Barotrauma public static bool TransparentWiringMode = true; + public static bool SkipInventorySlotUpdate; + private static object bulkItemBufferinUse; public static object BulkItemBufferInUse @@ -166,8 +169,6 @@ namespace Barotrauma private GUIImage previewImage; private GUILayoutGroup previewImageButtonHolder; - private GUIListBox contextMenu; - private const int submarineNameLimit = 30; private GUITextBlock submarineNameCharacterCount; @@ -514,15 +515,15 @@ namespace Barotrauma //----------------------------------------------- - showEntitiesPanel = new GUIFrame(new RectTransform(new Vector2(0.08f, 0.5f), GUI.Canvas) + showEntitiesPanel = new GUIFrame(new RectTransform(new Vector2(0.1f, 0.5f), GUI.Canvas) { - MinSize = new Point(170, 0) + MinSize = new Point(190, 0) }) { Visible = false }; - GUILayoutGroup paddedShowEntitiesPanel = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), showEntitiesPanel.RectTransform, Anchor.Center)) + GUILayoutGroup paddedShowEntitiesPanel = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.98f), showEntitiesPanel.RectTransform, Anchor.Center)) { Stretch = true }; @@ -534,20 +535,6 @@ namespace Barotrauma OnSelected = (GUITickBox obj) => { lightingEnabled = obj.Selected; - if (lightingEnabled) - { - //turn off lights that are inside containers - foreach (Item item in Item.ItemList) - { - foreach (LightComponent lightComponent in item.GetComponents()) - { - lightComponent.Light.Color = item.Container != null || (item.body != null && !item.body.Enabled) ? - Color.Transparent : - lightComponent.LightColor; - lightComponent.Light.LightSpriteEffect = lightComponent.Item.SpriteEffects; - } - } - } return true; } }; @@ -605,15 +592,38 @@ namespace Barotrauma Selected = Gap.ShowGaps, OnSelected = (GUITickBox obj) => { Gap.ShowGaps = obj.Selected; return true; }, }; - new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedShowEntitiesPanel.RectTransform), TextManager.Get("mapentitycategory.thalamus")) - { - UserData = "thalamus", - Selected = ShowThalamus, - OnSelected = (GUITickBox obj) => { ShowThalamus = obj.Selected; return true; }, - }; - showEntitiesTickBoxes.AddRange(paddedShowEntitiesPanel.Children.Select(c => c as GUITickBox)); + var subcategoryHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedShowEntitiesPanel.RectTransform), TextManager.Get("subcategories"), font: GUI.SubHeadingFont); + subcategoryHeader.RectTransform.MinSize = new Point(0, (int)(subcategoryHeader.Rect.Height * 1.5f)); + + var subcategoryList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedShowEntitiesPanel.RectTransform) { MinSize = new Point(0, showEntitiesPanel.Rect.Height / 3) }); + List availableSubcategories = new List(); + foreach (var prefab in MapEntityPrefab.List) + { + if (!string.IsNullOrEmpty(prefab.Subcategory) && !availableSubcategories.Contains(prefab.Subcategory)) + { + availableSubcategories.Add(prefab.Subcategory); + } + } + foreach (string subcategory in availableSubcategories) + { + var tb = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), subcategoryList.Content.RectTransform), + TextManager.Get("subcategory." + subcategory, returnNull: true) ?? subcategory, font: GUI.SmallFont) + { + UserData = subcategory, + Selected = !IsSubcategoryHidden(subcategory), + OnSelected = (GUITickBox obj) => { hiddenSubCategories[(string)obj.UserData] = !obj.Selected; return true; }, + }; + if (tb.TextBlock.TextSize.X > tb.TextBlock.Rect.Width * 1.25f) + { + tb.ToolTip = tb.Text; + tb.Text = ToolBox.LimitString(tb.Text, tb.Font, (int)(tb.TextBlock.Rect.Width * 1.25f)); + } + } + + GUITextBlock.AutoScaleAndNormalize(subcategoryList.Content.Children.Where(c => c is GUITickBox).Select(c => ((GUITickBox)c).TextBlock)); + showEntitiesPanel.RectTransform.NonScaledSize = new Point( (int)(paddedShowEntitiesPanel.RectTransform.Children.Max(c => (int)((c.GUIComponent as GUITickBox)?.TextBlock.TextSize.X ?? 0)) / paddedShowEntitiesPanel.RectTransform.RelativeSize.X), @@ -809,13 +819,18 @@ namespace Barotrauma new GUIFrame(new RectTransform(new Vector2(0.8f, 0.01f), paddedTab.RectTransform), style: "HorizontalLine"); - entityList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.9f), paddedTab.RectTransform), useMouseDownToSelect: true) + var entityListContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.9f), paddedTab.RectTransform), style: null); + categorizedEntityList = new GUIListBox(new RectTransform(Vector2.One, entityListContainer.RectTransform), useMouseDownToSelect: true); + allEntityList = new GUIListBox(new RectTransform(Vector2.One, entityListContainer.RectTransform), useMouseDownToSelect: true) { OnSelected = SelectPrefab, UseGridLayout = true, - CheckSelected = MapEntityPrefab.GetSelected + CheckSelected = MapEntityPrefab.GetSelected, + Visible = false }; - + + paddedTab.Recalculate(); + screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); } @@ -843,7 +858,7 @@ namespace Barotrauma GameMain.GameScreen.Select(); - GameSession gameSession = new GameSession(backedUpSubInfo, "", GameModePreset.TestMode, null); + GameSession gameSession = new GameSession(backedUpSubInfo, "", GameModePreset.TestMode, CampaignSettings.Empty, null); gameSession.StartRound(null, false); (gameSession.GameMode as TestGameMode).OnRoundEnd = () => { @@ -861,143 +876,249 @@ namespace Barotrauma private void UpdateEntityList() { - entityList.Content.ClearChildren(); + categorizedEntityList.Content.ClearChildren(); + allEntityList.Content.ClearChildren(); - int entitiesPerRow = (int)Math.Ceiling(entityList.Content.Rect.Width / Math.Max(125 * GUI.Scale, 60)); + int maxTextWidth = (int)(GUI.SubHeadingFont.MeasureString(TextManager.Get("mapentitycategory.misc")).X + GUI.IntScale(50)); + Dictionary> entityLists = new Dictionary>(); + Dictionary categoryKeys = new Dictionary(); + + foreach (MapEntityCategory category in Enum.GetValues(typeof(MapEntityCategory))) + { + string categoryName = TextManager.Get("MapEntityCategory." + category); + maxTextWidth = (int)Math.Max(maxTextWidth, GUI.SubHeadingFont.MeasureString(categoryName.Replace(' ', '\n')).X + GUI.IntScale(50)); + foreach (MapEntityPrefab ep in MapEntityPrefab.List) + { + if (!ep.Category.HasFlag(category)) { continue; } + + if (!entityLists.ContainsKey(category + ep.Subcategory)) + { + entityLists[category + ep.Subcategory] = new List(); + } + entityLists[category + ep.Subcategory].Add(ep); + categoryKeys[category + ep.Subcategory] = category; + string subcategoryName = TextManager.Get("subcategory." + ep.Subcategory, returnNull: true) ?? ep.Subcategory; + if (subcategoryName != null) + { + maxTextWidth = (int)Math.Max(maxTextWidth, GUI.SubHeadingFont.MeasureString(subcategoryName.Replace(' ', '\n')).X + GUI.IntScale(50)); + } + } + } + + categorizedEntityList.Content.ClampMouseRectToParent = true; + int entitiesPerRow = (int)Math.Ceiling(categorizedEntityList.Content.Rect.Width / Math.Max(125 * GUI.Scale, 60)); + foreach (string categoryKey in entityLists.Keys) + { + var categoryFrame = new GUIFrame(new RectTransform(Vector2.One, categorizedEntityList.Content.RectTransform), style: null) + { + ClampMouseRectToParent = true, + UserData = categoryKeys[categoryKey] + }; + + new GUIFrame(new RectTransform(Vector2.One, categoryFrame.RectTransform), style: "HorizontalLine"); + + string categoryName = TextManager.Get("MapEntityCategory." + entityLists[categoryKey].First().Category); + string subCategoryName = entityLists[categoryKey].First().Subcategory; + if (string.IsNullOrEmpty(subCategoryName)) + { + new GUITextBlock(new RectTransform(new Point(maxTextWidth, categoryFrame.Rect.Height), categoryFrame.RectTransform, Anchor.TopLeft), + categoryName, textAlignment: Alignment.TopLeft, font: GUI.SubHeadingFont, wrap: true) + { + Padding = new Vector4(GUI.IntScale(10)) + }; + + } + else + { + subCategoryName = string.IsNullOrEmpty(subCategoryName) ? + TextManager.Get("mapentitycategory.misc") : + (TextManager.Get("subcategory." + subCategoryName, returnNull: true) ?? subCategoryName); + var categoryTitle = new GUITextBlock(new RectTransform(new Point(maxTextWidth, categoryFrame.Rect.Height), categoryFrame.RectTransform, Anchor.TopLeft), + categoryName, textAlignment: Alignment.TopLeft, font: GUI.Font, wrap: true) + { + Padding = new Vector4(GUI.IntScale(10)) + }; + new GUITextBlock(new RectTransform(new Point(maxTextWidth, categoryFrame.Rect.Height), categoryFrame.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(0, (int)(categoryTitle.TextSize.Y + GUI.IntScale(10))) }, + subCategoryName, textAlignment: Alignment.TopLeft, font: GUI.SubHeadingFont, wrap: true) + { + Padding = new Vector4(GUI.IntScale(10)) + }; + } + + var entityListInner = new GUIListBox(new RectTransform(new Point(categoryFrame.Rect.Width - maxTextWidth, categoryFrame.Rect.Height), categoryFrame.RectTransform, Anchor.CenterRight), + style: null, + useMouseDownToSelect: true) + { + ScrollBarVisible = false, + AutoHideScrollBar = false, + OnSelected = SelectPrefab, + UseGridLayout = true, + CheckSelected = MapEntityPrefab.GetSelected, + ClampMouseRectToParent = true + }; + entityListInner.ContentBackground.ClampMouseRectToParent = true; + entityListInner.Content.ClampMouseRectToParent = true; + + foreach (MapEntityPrefab ep in entityLists[categoryKey]) + { +#if !DEBUG + if (ep.HideInMenus) { continue; } +#endif + CreateEntityElement(ep, entitiesPerRow, entityListInner.Content); + } + + entityListInner.UpdateScrollBarSize(); + int contentHeight = (int)(entityListInner.TotalSize + entityListInner.Padding.Y + entityListInner.Padding.W); + categoryFrame.RectTransform.NonScaledSize = new Point(categoryFrame.Rect.Width, contentHeight); + categoryFrame.RectTransform.MinSize = new Point(0, contentHeight); + entityListInner.RectTransform.NonScaledSize = new Point(entityListInner.Rect.Width, contentHeight); + entityListInner.RectTransform.MinSize = new Point(0, contentHeight); + + entityListInner.Content.RectTransform.SortChildren((i1, i2) => + string.Compare(((MapEntityPrefab)i1.GUIComponent.UserData). Name, (i2.GUIComponent.UserData as MapEntityPrefab)?.Name, StringComparison.Ordinal)); + } foreach (MapEntityPrefab ep in MapEntityPrefab.List) { #if !DEBUG - if (ep.HideInMenus) { continue; } + if (ep.HideInMenus) { continue; } #endif + CreateEntityElement(ep, entitiesPerRow, allEntityList.Content); + } + } - bool legacy = ep.Category.HasFlag(MapEntityCategory.Legacy); + private void CreateEntityElement(MapEntityPrefab ep, int entitiesPerRow, GUIComponent parent) + { + bool legacy = ep.Category.HasFlag(MapEntityCategory.Legacy); - float relWidth = 1.0f / entitiesPerRow; - GUIFrame frame = new GUIFrame(new RectTransform( - new Vector2(relWidth, relWidth * ((float)entityList.Content.Rect.Width / entityList.Content.Rect.Height)), - entityList.Content.RectTransform) { MinSize = new Point(0, 50) }, - style: "GUITextBox") - { - UserData = ep, - }; - frame.RectTransform.MinSize = new Point(0, frame.Rect.Width); - frame.RectTransform.MaxSize = new Point(int.MaxValue, frame.Rect.Width); + float relWidth = 1.0f / entitiesPerRow; + GUIFrame frame = new GUIFrame(new RectTransform( + new Vector2(relWidth, relWidth * ((float)parent.Rect.Width / parent.Rect.Height)), + parent.RectTransform) + { MinSize = new Point(0, 50) }, + style: "GUITextBox") + { + UserData = ep, + ClampMouseRectToParent = true + }; + frame.RectTransform.MinSize = new Point(0, frame.Rect.Width); + frame.RectTransform.MaxSize = new Point(int.MaxValue, frame.Rect.Width); - string name = legacy ? TextManager.GetWithVariable("legacyitemformat", "[name]", ep.Name) : ep.Name; - frame.ToolTip = string.IsNullOrEmpty(ep.Description) ? name : name + '\n' + ep.Description; + string name = legacy ? TextManager.GetWithVariable("legacyitemformat", "[name]", ep.Name) : ep.Name; + frame.ToolTip = string.IsNullOrEmpty(ep.Description) ? name : name + '\n' + ep.Description; - if (ep.HideInMenus) - { - frame.Color = Color.Red; - name = "[HIDDEN] " + name; - } - - GUILayoutGroup paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.8f), frame.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) - { - Stretch = true, - RelativeSpacing = 0.03f, - CanBeFocused = false - }; - - Sprite icon = ep.sprite; - Color iconColor = Color.White; - if (ep is ItemPrefab itemPrefab) - { - if (itemPrefab.InventoryIcon != null) - { - icon = itemPrefab.InventoryIcon; - iconColor = itemPrefab.InventoryIconColor; - } - else - { - iconColor = itemPrefab.SpriteColor; - } - } - GUIImage img = null; - if (ep.sprite != null) - { - img = new GUIImage(new RectTransform(new Vector2(1.0f, 0.8f), - paddedFrame.RectTransform, Anchor.TopCenter), icon) - { - CanBeFocused = false, - LoadAsynchronously = true, - Color = legacy ? iconColor * 0.6f : iconColor - }; - } - - if (ep is ItemAssemblyPrefab itemAssemblyPrefab) - { - new GUICustomComponent(new RectTransform(new Vector2(1.0f, 0.75f), - paddedFrame.RectTransform, Anchor.TopCenter), onDraw: (sb, customComponent) => - { - if (GUIImage.LoadingTextures) { return; } - itemAssemblyPrefab.DrawIcon(sb, customComponent); - }) - { - HideElementsOutsideFrame = true, - ToolTip = frame.RawToolTip - }; - } - - GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform, Anchor.BottomCenter), - text: name, textAlignment: Alignment.Center, font: GUI.SmallFont) - { - CanBeFocused = false - }; - if (legacy) textBlock.TextColor *= 0.6f; - textBlock.Text = ToolBox.LimitString(textBlock.Text, textBlock.Font, textBlock.Rect.Width); - - if (ep.Category == MapEntityCategory.ItemAssembly) - { - var deleteButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform, Anchor.BottomCenter) { MinSize = new Point(0, 20) }, - TextManager.Get("Delete"), style: "GUIButtonSmall") - { - UserData = ep, - OnClicked = (btn, userData) => - { - ItemAssemblyPrefab assemblyPrefab = (ItemAssemblyPrefab) userData; - if (assemblyPrefab != null) - { - var msgBox = new GUIMessageBox( - TextManager.Get("DeleteDialogLabel"), - TextManager.GetWithVariable("DeleteDialogQuestion", "[file]", assemblyPrefab.Name), - new[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); - msgBox.Buttons[0].OnClicked += (deleteBtn, userData2) => - { - try - { - assemblyPrefab.Delete(); - UpdateEntityList(); - OpenEntityMenu(MapEntityCategory.ItemAssembly); - } - catch (Exception e) - { - DebugConsole.ThrowError(TextManager.GetWithVariable("DeleteFileError", "[file]", assemblyPrefab.Name), e); - } - return true; - }; - msgBox.Buttons[0].OnClicked += msgBox.Close; - msgBox.Buttons[1].OnClicked += msgBox.Close; - } - - return true; - } - }; - } - paddedFrame.Recalculate(); - if (img != null) - { - img.Scale = Math.Min(Math.Min(img.Rect.Width / img.Sprite.size.X, img.Rect.Height / img.Sprite.size.Y), 1.5f); - img.RectTransform.NonScaledSize = new Point((int)(img.Sprite.size.X * img.Scale), img.Rect.Height); - } + if (ep.HideInMenus) + { + frame.Color = Color.Red; + name = "[HIDDEN] " + name; } - entityList.Content.RectTransform.SortChildren((i1, i2) => - string.Compare(((MapEntityPrefab) i1.GUIComponent.UserData). Name, (i2.GUIComponent.UserData as MapEntityPrefab)?.Name, StringComparison.Ordinal)); + GUILayoutGroup paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.8f), frame.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) + { + Stretch = true, + RelativeSpacing = 0.03f, + CanBeFocused = false + }; + + Sprite icon = ep.sprite; + Color iconColor = Color.White; + if (ep is ItemPrefab itemPrefab) + { + if (itemPrefab.InventoryIcon != null) + { + icon = itemPrefab.InventoryIcon; + iconColor = itemPrefab.InventoryIconColor; + } + else + { + iconColor = itemPrefab.SpriteColor; + } + } + GUIImage img = null; + if (ep.sprite != null) + { + img = new GUIImage(new RectTransform(new Vector2(1.0f, 0.8f), + paddedFrame.RectTransform, Anchor.TopCenter), icon) + { + CanBeFocused = false, + LoadAsynchronously = true, + Color = legacy ? iconColor * 0.6f : iconColor + }; + } + + if (ep is ItemAssemblyPrefab itemAssemblyPrefab) + { + new GUICustomComponent(new RectTransform(new Vector2(1.0f, 0.75f), + paddedFrame.RectTransform, Anchor.TopCenter), onDraw: (sb, customComponent) => + { + if (GUIImage.LoadingTextures) { return; } + itemAssemblyPrefab.DrawIcon(sb, customComponent); + }) + { + HideElementsOutsideFrame = true, + ToolTip = frame.RawToolTip + }; + } + + GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform, Anchor.BottomCenter), + text: name, textAlignment: Alignment.Center, font: GUI.SmallFont) + { + CanBeFocused = false + }; + if (legacy) textBlock.TextColor *= 0.6f; + textBlock.Text = ToolBox.LimitString(textBlock.Text, textBlock.Font, textBlock.Rect.Width); + + if (ep.Category == MapEntityCategory.ItemAssembly) + { + var deleteButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform, Anchor.BottomCenter) { MinSize = new Point(0, 20) }, + TextManager.Get("Delete"), style: "GUIButtonSmall") + { + UserData = ep, + OnClicked = (btn, userData) => + { + ItemAssemblyPrefab assemblyPrefab = (ItemAssemblyPrefab)userData; + if (assemblyPrefab != null) + { + var msgBox = new GUIMessageBox( + TextManager.Get("DeleteDialogLabel"), + TextManager.GetWithVariable("DeleteDialogQuestion", "[file]", assemblyPrefab.Name), + new[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); + msgBox.Buttons[0].OnClicked += (deleteBtn, userData2) => + { + try + { + assemblyPrefab.Delete(); + UpdateEntityList(); + OpenEntityMenu(MapEntityCategory.ItemAssembly); + } + catch (Exception e) + { + DebugConsole.ThrowError(TextManager.GetWithVariable("DeleteFileError", "[file]", assemblyPrefab.Name), e); + } + return true; + }; + msgBox.Buttons[0].OnClicked += msgBox.Close; + msgBox.Buttons[1].OnClicked += msgBox.Close; + } + + return true; + } + }; + } + paddedFrame.Recalculate(); + if (img != null) + { + img.Scale = Math.Min(Math.Min(img.Rect.Width / img.Sprite.size.X, img.Rect.Height / img.Sprite.size.Y), 1.5f); + img.RectTransform.NonScaledSize = new Point((int)(img.Sprite.size.X * img.Scale), img.Rect.Height); + } } public override void Select() + { + Select(enableAutoSave: true); + } + + public void Select(bool enableAutoSave = true) { base.Select(); @@ -1090,7 +1211,7 @@ namespace Barotrauma CreateDummyCharacter(); - if (GameSettings.EnableSubmarineAutoSave) + if (GameSettings.EnableSubmarineAutoSave && enableAutoSave) { CoroutineManager.StartCoroutine(AutoSaveCoroutine(), "SubEditorAutoSave"); } @@ -1240,7 +1361,7 @@ namespace Barotrauma { try { - Barotrauma.IO.Validation.DevException = true; + Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; TimeSpan time = DateTime.UtcNow - DateTime.MinValue; string filePath = Path.Combine(autoSavePath, $"AutoSave_{(ulong)time.TotalMilliseconds}.sub"); SaveUtil.CompressStringToFile(filePath, doc.ToString()); @@ -1276,7 +1397,7 @@ namespace Barotrauma } }); - Barotrauma.IO.Validation.DevException = false; + Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; CrossThread.RequestExecutionOnMainThread(DisplayAutoSavePrompt); } catch (Exception e) @@ -1444,7 +1565,9 @@ namespace Barotrauma msgBox.Buttons[0].OnClicked = (bt, userdata) => { contentPackage.AddFile(savePath, ContentType.OutpostModule); + Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; contentPackage.Save(contentPackage.Path, reload: false); + Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; msgBox.Close(); return true; }; @@ -1519,7 +1642,7 @@ namespace Barotrauma if (Submarine.MainSub != null) { - Barotrauma.IO.Validation.DevException = true; + Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; if (previewImage?.Sprite?.Texture != null && !previewImage.Sprite.Texture.IsDisposed && Submarine.MainSub.Info.Type != SubmarineType.OutpostModule) { bool savePreviewImage = true; @@ -1539,7 +1662,7 @@ namespace Barotrauma { Submarine.MainSub.SaveAs(savePath); } - Barotrauma.IO.Validation.DevException = false; + Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; Submarine.MainSub.CheckForErrors(); @@ -2346,9 +2469,9 @@ namespace Barotrauma new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, loadFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); - var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.25f, 0.5f), loadFrame.RectTransform, Anchor.Center) { MinSize = new Point(350, 500) }); + var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.75f), loadFrame.RectTransform, Anchor.Center) { MinSize = new Point(350, 500) }); - var paddedLoadFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), innerFrame.RectTransform, Anchor.Center)) { Stretch = true, RelativeSpacing = 0.02f }; + var paddedLoadFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), innerFrame.RectTransform, Anchor.Center)) { Stretch = true, RelativeSpacing = 0.01f }; var deleteButtonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.2f), paddedLoadFrame.RectTransform, Anchor.Center)) { @@ -2396,7 +2519,7 @@ namespace Barotrauma { if (prevSub == null || prevSub.Type != sub.Type) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), subList.Content.RectTransform) { MinSize = new Point(0, 35) }, + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), subList.Content.RectTransform) { MinSize = new Point(0, 35) }, TextManager.Get("SubmarineType." + sub.Type), font: GUI.LargeFont, textAlignment: Alignment.Center, style: "ListBoxElement") { CanBeFocused = false @@ -2404,7 +2527,7 @@ namespace Barotrauma prevSub = sub; } - GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), subList.Content.RectTransform) { MinSize = new Point(0, 30) }, + GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), subList.Content.RectTransform) { MinSize = new Point(0, 30) }, ToolBox.LimitString(sub.Name, GUI.Font, subList.Rect.Width - 80)) { UserData = sub, @@ -2521,6 +2644,8 @@ namespace Barotrauma { OnClicked = LoadSub }; + + controlBtnHolder.RectTransform.MaxSize = new Point(int.MaxValue, controlBtnHolder.Children.First().Rect.Height); } private void FilterSubs(GUIListBox subList, string filter) @@ -2567,16 +2692,6 @@ namespace Barotrauma cam.Position = Submarine.MainSub.Position + Submarine.MainSub.HiddenSubPosition; loadFrame = null; - - //turn off lights that are inside an inventory (cabinet for example) - foreach (Item item in Item.ItemList) - { - var lightComponent = item.GetComponent(); - if (lightComponent != null) - { - lightComponent.Light.Enabled = item.ParentInventory == null; - } - } } private bool LoadSub(GUIButton button, object obj) @@ -2611,16 +2726,6 @@ namespace Barotrauma loadFrame = null; - //turn off lights that are inside an inventory (cabinet for example) - foreach (Item item in Item.ItemList) - { - var lightComponent = item.GetComponent(); - if (lightComponent != null) - { - lightComponent.Light.Enabled = item.ParentInventory == null; - } - } - if (selectedSub.Info.GameVersion < new Version("0.8.9.0")) { var adjustLightsPrompt = new GUIMessageBox(TextManager.Get("Warning"), TextManager.Get("AdjustLightsPrompt"), @@ -2707,9 +2812,9 @@ namespace Barotrauma child.SpriteEffects = entityMenuOpen ? SpriteEffects.None : SpriteEffects.FlipVertically; } - foreach (GUIComponent child in entityList.Content.Children) + foreach (GUIComponent child in categorizedEntityList.Content.Children) { - child.Visible = !entityCategory.HasValue || ((MapEntityPrefab) child.UserData).Category.HasFlag(entityCategory); + child.Visible = !entityCategory.HasValue || (MapEntityCategory)child.UserData == entityCategory; if (child.Visible && dummyCharacter?.SelectedConstruction?.OwnInventory != null) { child.Visible = child.UserData is MapEntityPrefab item && IsItemPrefab(item); @@ -2718,50 +2823,60 @@ namespace Barotrauma if (!string.IsNullOrEmpty(entityFilterBox.Text)) { FilterEntities(entityFilterBox.Text); } - entityList.UpdateScrollBarSize(); - entityList.BarScroll = 0.0f; + categorizedEntityList.UpdateScrollBarSize(); + categorizedEntityList.BarScroll = 0.0f; + // categorizedEntityList.Visible = true; + // allEntityList.Visible = false; } private void FilterEntities(string filter) { if (string.IsNullOrWhiteSpace(filter)) { - entityList.Content.Children.ForEach(c => + allEntityList.Visible = false; + categorizedEntityList.Visible = true; + + foreach (GUIComponent child in categorizedEntityList.Content.Children) { - c.Visible = !selectedCategory.HasValue || selectedCategory == ((MapEntityPrefab) c.UserData).Category; - if (c.Visible && dummyCharacter?.SelectedConstruction?.OwnInventory != null) + child.Visible = !selectedCategory.HasValue || selectedCategory == (MapEntityCategory)child.UserData; + if (!child.Visible) { return; } + var innerList = child.GetChild(); + foreach (GUIComponent grandChild in innerList.Content.Children) { - c.Visible = c.UserData is MapEntityPrefab item && IsItemPrefab(item); + grandChild.Visible = ((MapEntityPrefab)grandChild.UserData).Name.ToLower().Contains(filter); + if (grandChild.Visible && dummyCharacter?.SelectedConstruction?.OwnInventory != null) + { + grandChild.Visible = grandChild.UserData is MapEntityPrefab item && IsItemPrefab(item); + } } - }); - entityList.UpdateScrollBarSize(); - entityList.BarScroll = 0.0f; - + }; + categorizedEntityList.UpdateScrollBarSize(); + categorizedEntityList.BarScroll = 0.0f; return; } + allEntityList.Visible = true; + categorizedEntityList.Visible = false; filter = filter.ToLower(); - foreach (GUIComponent child in entityList.Content.Children) + foreach (GUIComponent child in allEntityList.Content.Children) { - var textBlock = child.GetChild(); - child.Visible = - (!selectedCategory.HasValue || ((MapEntityPrefab) child.UserData).Category.HasFlag(selectedCategory)) && - ((MapEntityPrefab) child.UserData).Name.ToLower().Contains(filter); - + child.Visible = + (!selectedCategory.HasValue || ((MapEntityPrefab)child.UserData).Category.HasFlag(selectedCategory)) && + ((MapEntityPrefab)child.UserData).Name.ToLower().Contains(filter); ; if (child.Visible && dummyCharacter?.SelectedConstruction?.OwnInventory != null) { child.Visible = child.UserData is MapEntityPrefab item && IsItemPrefab(item); } } - entityList.UpdateScrollBarSize(); - entityList.BarScroll = 0.0f; + allEntityList.UpdateScrollBarSize(); + allEntityList.BarScroll = 0.0f; } private void ClearFilter() { FilterEntities(""); - entityList.UpdateScrollBarSize(); - entityList.BarScroll = 0.0f; + categorizedEntityList.UpdateScrollBarSize(); + categorizedEntityList.BarScroll = 0.0f; entityFilterBox.Text = ""; } @@ -2804,18 +2919,12 @@ namespace Barotrauma private void CreateContextMenu() { + if (GUIContextMenu.CurrentContextMenu != null) { return; } + List targets = MapEntity.mapEntityList.Any(me => me.IsHighlighted && !MapEntity.SelectedList.Contains(me)) ? MapEntity.mapEntityList.Where(me => me.IsHighlighted).ToList() : new List(MapEntity.SelectedList); - contextMenu = new GUIListBox(new RectTransform(new Vector2(0.1f, 0.1f), GUI.Canvas) - { - ScreenSpaceOffset = PlayerInput.MousePosition.ToPoint() - }, style: "GUIToolTip") - { - Padding = new Vector4(5) - }; - Item target = null; var single = targets.Count == 1 ? targets.Single() : null; @@ -2825,155 +2934,118 @@ namespace Barotrauma var container = item.GetComponent(); if (container == null || container.DrawInventory) { target = item; } } - + // Holding shift brings up special context menu options if (PlayerInput.IsShiftDown()) { - new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), - TextManager.Get("SubEditor.EditBackgroundColor"), font: GUI.SmallFont) - { - UserData = "bgcolor" - }; - - new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), - TextManager.Get("SubEditor.ToggleTransparency"), font: GUI.SmallFont) - { - UserData = "transparency" - }; - new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), - TextManager.Get("SubEditor.ToggleGrid"), font: GUI.SmallFont) - { - UserData = "togglegrid" - }; - - new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), - TextManager.Get("editor.selectsame"), font: GUI.SmallFont) - { - UserData = "selectsame", - Enabled = targets.Count > 0 - }; - - new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), - TextManager.Get("SubEditor.AddImage"), font: GUI.SmallFont) - { - UserData = "addimage" - }; - - new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), - TextManager.Get("SubEditor.ToggleImageEditing"), font: GUI.SmallFont) - { - UserData = "editimages" - }; + GUIContextMenu.CreateContextMenu( + new ContextMenuOption("SubEditor.EditBackgroundColor", isEnabled: true, onSelected: CreateBackgroundColorPicker), + new ContextMenuOption("SubEditor.ToggleTransparency", isEnabled: true, onSelected: () => TransparentWiringMode = !TransparentWiringMode), + new ContextMenuOption("SubEditor.ToggleGrid", isEnabled: true, onSelected: () => ShouldDrawGrid = !ShouldDrawGrid), + new ContextMenuOption("SubEditor.PasteAssembly", isEnabled: true, PasteAssembly), + new ContextMenuOption("Editor.SelectSame", isEnabled: targets.Count > 0, onSelected: delegate + { + IEnumerable matching = MapEntity.mapEntityList.Where(e => e.prefab != null && targets.Any(t => t.prefab?.Identifier == e.prefab.Identifier) && !MapEntity.SelectedList.Contains(e)); + MapEntity.SelectedList.AddRange(matching); + }), + new ContextMenuOption("SubEditor.AddImage", isEnabled: true, onSelected: ImageManager.CreateImageWizard), + new ContextMenuOption("SubEditor.ToggleImageEditing", isEnabled: true, onSelected: delegate + { + ImageManager.EditorMode = !ImageManager.EditorMode; + if (!ImageManager.EditorMode) { GameMain.Config.SaveNewPlayerConfig(); } + })); } else { - new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), - TextManager.Get("label.openlabel"), font: GUI.SmallFont) - { - UserData = "open", - Enabled = target != null - }; - - new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), - TextManager.Get("editor.cut"), font: GUI.SmallFont) - { - UserData = "cut", - Enabled = targets.Count > 0 - }; - - new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), - TextManager.Get("editor.copytoclipboard"), font: GUI.SmallFont) - { - UserData = "copy", - Enabled = targets.Count > 0 - }; - - new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), - TextManager.Get("editor.paste"), font: GUI.SmallFont) - { - UserData = "paste", - Enabled = MapEntity.CopiedList.Any(), - }; - - new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), - TextManager.Get("delete"), font: GUI.SmallFont) - { - UserData = "delete", - Enabled = targets.Count > 0 - }; - } - - foreach (var guiComponent in contextMenu.Content.Children) - { - if (guiComponent is GUITextBlock child) - { - if (!child.Enabled) + GUIContextMenu.CreateContextMenu( + new ContextMenuOption("label.openlabel", isEnabled: target != null, onSelected: () => OpenItem(target)), + new ContextMenuOption("editor.cut", isEnabled: targets.Count > 0, onSelected: () => MapEntity.Cut(targets)), + new ContextMenuOption("editor.copytoclipboard", isEnabled: targets.Count > 0, onSelected: () => MapEntity.Copy(targets)), + new ContextMenuOption("editor.paste", isEnabled: MapEntity.CopiedList.Any(), onSelected: () => MapEntity.Paste(cam.ScreenToWorld(PlayerInput.MousePosition))), + new ContextMenuOption("delete", isEnabled: targets.Count > 0, onSelected: delegate { - child.TextColor *= 0.5f; - } - } + StoreCommand(new AddOrDeleteCommand(targets, true)); + foreach (var me in targets) + { + if (!me.Removed) { me.Remove(); } + } + })); + } + } + + private void PasteAssembly() + { + string clipboard = Clipboard.GetText(); + if (string.IsNullOrWhiteSpace(clipboard)) + { + DebugConsole.ThrowError("Unable to paste assembly: Clipboard content is empty."); + return; } - contextMenu.Content.Children.ForEach(c => + XElement element = null; + + try { - if (c is GUITextBlock block) - { - block.RectTransform.NonScaledSize = new Point((int) (block.TextSize.X + block.Padding.X * 2), (int)(18 * GUI.Scale)); - } - }); - int biggestSize = contextMenu.Content.Children.Max(c => c.Rect.Width + (int)contextMenu.Padding.X * 2); - contextMenu.Content.Children.ForEach(c => c.RectTransform.MinSize = new Point(biggestSize, c.Rect.Height)); - contextMenu.RectTransform.NonScaledSize = new Point(biggestSize, (int)(contextMenu.Content.Children.Sum(c => c.Rect.Height) + (contextMenu.Padding.X * 2))); - - contextMenu.OnSelected = (component, obj) => + element = XDocument.Parse(clipboard).Root; + } + catch (Exception) { /* ignored */ } + + if (element == null) { - if (!component.Enabled) { return false; } - switch (obj as string) - { - case "bgcolor": - CreateBackgroundColorPicker(); - break; - case "togglegrid": - ShouldDrawGrid = !ShouldDrawGrid; - break; - case "addimage": - ImageManager.CreateImageWizard(PlayerInput.MousePosition); - break; - case "editimages": - ImageManager.EditorMode = !ImageManager.EditorMode; - if (!ImageManager.EditorMode) { GameMain.Config.SaveNewPlayerConfig(); } - break; - case "transparency": - TransparentWiringMode = !TransparentWiringMode; - break; - case "selectsame": - IEnumerable matching = MapEntity.mapEntityList.Where(e => e.prefab != null && targets.Any(t => t.prefab?.Identifier == e.prefab.Identifier) && !MapEntity.SelectedList.Contains(e)); - MapEntity.SelectedList.AddRange(matching); - break; - case "copy": - MapEntity.Copy(targets); - break; - case "cut": - MapEntity.Cut(targets); - break; - case "paste": - MapEntity.Paste(cam.ScreenToWorld(contextMenu.Rect.Location.ToVector2())); - break; - case "delete": - StoreCommand(new AddOrDeleteCommand(targets, true)); - targets.ForEach(me => { if (!me.Removed) { me.Remove(); }}); - break; - case "open" when target != null: - OpenItem(target); - break; - } - contextMenu = null; - return true; - }; + DebugConsole.ThrowError("Unable to paste assembly: Clipboard content is not valid XML."); + return; + } + + Vector2 pos = cam.ScreenToWorld(PlayerInput.MousePosition); + Submarine sub = Submarine.MainSub; + List entities; + try + { + entities = ItemAssemblyPrefab.PasteEntities(pos, sub, element, selectInstance: true); + } + catch (Exception e) + { + DebugConsole.ThrowError("Unable to paste assembly: Failed to load items.", e); + return; + } + + if (!entities.Any()) { return; } + StoreCommand(new AddOrDeleteCommand(entities, false, handleInventoryBehavior: false)); } public static GUIMessageBox CreatePropertyColorPicker(Color originalColor, SerializableProperty property, ISerializableEntity entity) { + var entities = new List<(ISerializableEntity Entity, Color OriginalColor, SerializableProperty Property)> { (entity, originalColor, property) }; + + foreach (ISerializableEntity selectedEntity in MapEntity.SelectedList.Where(selectedEntity => selectedEntity is ISerializableEntity && entity != selectedEntity).Cast()) + { + switch (entity) + { + case ItemComponent _ when selectedEntity is Item item: + foreach (var component in item.Components) + { + if (component.GetType() == entity.GetType() && component != entity) + { + entities.Add((component, (Color) property.GetValue(component), property)); + } + } + break; + default: + if (selectedEntity.GetType() == entity.GetType()) + { + entities.Add((selectedEntity, (Color) property.GetValue(selectedEntity), property)); + } + else if (selectedEntity is { SerializableProperties: { } props} ) + { + if (props.TryGetValue(property.NameToLowerInvariant, out SerializableProperty foundProp)) + { + entities.Add((selectedEntity, (Color) foundProp.GetValue(selectedEntity), foundProp)); + } + } + break; + } + } + bool setValues = true; object sliderMutex = new object(), sliderTextMutex = new object(), @@ -3069,9 +3141,22 @@ namespace Barotrauma colorPicker.DisposeTextures(); msgBox.Close(); - if (entity is MapEntity { Removed: true } me) { return true; } Color newColor = SetColor(null); - StoreCommand(new PropertyCommand(entity, property.Name, newColor, originalColor)); + + Dictionary> oldProperties = new Dictionary>(); + + foreach (var (sEntity, color, _) in entities) + { + if (sEntity is MapEntity { Removed: true }) { continue; } + if (!oldProperties.ContainsKey(color)) + { + oldProperties.Add(color, new List()); + } + oldProperties[color].Add(sEntity); + } + + List affected = entities.Select(t => t.Entity).Where(se => se is MapEntity { Removed: false }).ToList(); + StoreCommand(new PropertyCommand(affected, property.Name, newColor, oldProperties)); if (MapEntity.EditingHUD != null && (MapEntity.EditingHUD.UserData == entity || (!(entity is ItemComponent ic) || MapEntity.EditingHUD.UserData == ic.Item))) { @@ -3097,8 +3182,12 @@ namespace Barotrauma { colorPicker.DisposeTextures(); msgBox.Close(); - if (entity is MapEntity { Removed: true } me) { return true; } - property.SetValue(entity, originalColor); + + foreach (var (e, color, prop) in entities) + { + if (e is MapEntity { Removed: true }) { continue; } + prop.TrySetValue(e, color); + } return true; }; @@ -3145,8 +3234,12 @@ namespace Barotrauma } Color color = ToolBox.HSVToRGB(colorPicker.SelectedHue, colorPicker.SelectedSaturation, colorPicker.SelectedValue); - color.A = originalColor.A; - property.TrySetValue(entity, color); + foreach (var (e, origColor, prop) in entities) + { + if (e is MapEntity { Removed: true }) { continue; } + color.A = origColor.A; + prop.TrySetValue(e, color); + } return color; void SetSliders(Vector3 hsv) @@ -3165,9 +3258,11 @@ namespace Barotrauma void SetColorPicker(Vector3 hsv) { + bool hueChanged = !MathUtils.NearlyEqual(colorPicker.SelectedHue, hsv.X); colorPicker.SelectedHue = hsv.X; colorPicker.SelectedSaturation = hsv.Y; colorPicker.SelectedValue = hsv.Z; + if (hueChanged) { colorPicker.RefreshHue(); } } void SetHex(Vector3 hsv) @@ -3423,6 +3518,8 @@ namespace Barotrauma private bool SelectPrefab(GUIComponent component, object obj) { + allEntityList.Deselect(); + categorizedEntityList.Deselect(); if (GUI.MouseOn is GUIButton || GUI.MouseOn?.Parent is GUIButton) { return false; } AddPreviouslyUsed(obj as MapEntityPrefab); @@ -3501,7 +3598,7 @@ namespace Barotrauma case ItemPrefab _: { // Place the item into our hands - DraggedItemPrefab = (MapEntityPrefab) obj; + DraggedItemPrefab = (MapEntityPrefab)obj; SoundPlayer.PlayUISound(GUISoundType.PickItem); break; } @@ -3513,7 +3610,7 @@ namespace Barotrauma SoundPlayer.PlayUISound(GUISoundType.PickItem); MapEntityPrefab.SelectPrefab(obj); } - + return false; } @@ -3865,11 +3962,7 @@ namespace Barotrauma wiringToolPanel.AddToGUIUpdateList(); } - if (contextMenu != null) - { - contextMenu.AddToGUIUpdateList(); - } - else if (MapEntity.HighlightedListBox != null) + if (MapEntity.HighlightedListBox != null) { MapEntity.HighlightedListBox.AddToGUIUpdateList(); } @@ -4018,6 +4111,7 @@ namespace Barotrauma /// public override void Update(double deltaTime) { + SkipInventorySlotUpdate = false; ImageManager.Update((float) deltaTime); if (GameMain.GraphicsWidth != screenResolution.X || GameMain.GraphicsHeight != screenResolution.Y) @@ -4119,27 +4213,31 @@ namespace Barotrauma } } - if (WiringMode && dummyCharacter != null) - { - if (wiringToolPanel.GetChild() is { } listBox) - { - if (!dummyCharacter.HeldItems.Any(it => it.HasTag("wire"))) - { - listBox.Deselect(); - } - - List numberKeys = PlayerInput.NumberKeys; - if (numberKeys.Find(PlayerInput.KeyHit) is { } key) - { - // treat 0 as the last key instead of first - int index = key == Keys.D0 ? numberKeys.Count : numberKeys.IndexOf(key) - 1; - listBox.Select(index, force: false, autoScroll: true, takeKeyBoardFocus: false); - } - } - } - if (GUI.KeyboardDispatcher.Subscriber == null) { + if (WiringMode && dummyCharacter != null) + { + if (wiringToolPanel.GetChild() is { } listBox) + { + if (!dummyCharacter.HeldItems.Any(it => it.HasTag("wire"))) + { + listBox.Deselect(); + } + + List numberKeys = PlayerInput.NumberKeys; + if (numberKeys.Find(PlayerInput.KeyHit) is { } key && key != Keys.None) + { + // treat 0 as the last key instead of first + int index = key == Keys.D0 ? numberKeys.Count : numberKeys.IndexOf(key) - 1; + if (index > -1 && index < listBox.Content.CountChildren) + { + listBox.Select(index, force: false, autoScroll: true, takeKeyBoardFocus: false); + SkipInventorySlotUpdate = true; + } + } + } + } + if (PlayerInput.KeyHit(Keys.E) && mode == Mode.Default) { if (dummyCharacter != null) @@ -4200,7 +4298,7 @@ namespace Barotrauma if (PlayerInput.IsCtrlDown() && MapEntity.StartMovingPos == Vector2.Zero) { - cam.MoveCamera((float) deltaTime, allowMove: false); + cam.MoveCamera((float) deltaTime, allowMove: false, allowZoom: GUI.MouseOn == null); // Save menu if (PlayerInput.KeyHit(Keys.S)) { @@ -4239,12 +4337,12 @@ namespace Barotrauma } else { - cam.MoveCamera((float) deltaTime, allowMove: true); + cam.MoveCamera((float) deltaTime, allowMove: true, allowZoom: GUI.MouseOn == null); } } else { - cam.MoveCamera((float) deltaTime, allowMove: false); + cam.MoveCamera((float) deltaTime, allowMove: false, allowZoom: GUI.MouseOn == null); } if (PlayerInput.MidButtonHeld()) @@ -4263,17 +4361,18 @@ namespace Barotrauma if (lightingEnabled) { - GameMain.LightManager?.Update((float)deltaTime); - } - - if (contextMenu != null) - { - Rectangle expandedRect = contextMenu.Rect; - expandedRect.Inflate(20, 20); - if (!expandedRect.Contains(PlayerInput.MousePosition)) + //turn off lights that are inside containers + foreach (Item item in Item.ItemList) { - contextMenu = null; + foreach (LightComponent lightComponent in item.GetComponents()) + { + lightComponent.Light.Color = item.Container != null || (item.body != null && !item.body.Enabled) ? + Color.Transparent : + lightComponent.LightColor; + lightComponent.Light.LightSpriteEffect = lightComponent.Item.SpriteEffects; + } } + GameMain.LightManager?.Update((float)deltaTime); } if (dummyCharacter != null && Entity.FindEntityByID(dummyCharacter.ID) == dummyCharacter) @@ -4655,6 +4754,7 @@ namespace Barotrauma } graphics.Clear(backgroundColor); + ImageManager.Draw(spriteBatch, cam); spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, transformMatrix: cam.Transform); @@ -4666,7 +4766,7 @@ namespace Barotrauma } Submarine.DrawBack(spriteBatch, true, e => e is Structure s && - (ShowThalamus || !s.prefab.Category.HasFlag(MapEntityCategory.Thalamus)) && + !IsSubcategoryHidden(e.prefab?.Subcategory) && (e.SpriteDepth >= 0.9f || s.Prefab.BackgroundSprite != null)); Submarine.DrawPaintedColors(spriteBatch, true); spriteBatch.End(); @@ -4687,15 +4787,15 @@ namespace Barotrauma Submarine.DrawBack(spriteBatch, true, e => (!(e is Structure) || e.SpriteDepth < 0.9f) && - (ShowThalamus || !e.prefab.Category.HasFlag(MapEntityCategory.Thalamus))); + !IsSubcategoryHidden(e.prefab?.Subcategory)); spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, transformMatrix: cam.Transform); - Submarine.DrawDamageable(spriteBatch, null, editing: true, e => ShowThalamus || !(e.prefab?.Category.HasFlag(MapEntityCategory.Thalamus) ?? false)); + Submarine.DrawDamageable(spriteBatch, null, editing: true, e => !IsSubcategoryHidden(e.prefab?.Subcategory)); spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, transformMatrix: cam.Transform); - Submarine.DrawFront(spriteBatch, editing: true, e => ShowThalamus || !(e.prefab?.Category.HasFlag(MapEntityCategory.Thalamus) ?? false)); + Submarine.DrawFront(spriteBatch, editing: true, e => !IsSubcategoryHidden(e.prefab?.Subcategory)); if (!WiringMode && !IsMouseOnEditorGUI()) { MapEntityPrefab.Selected?.DrawPlacing(spriteBatch, cam); @@ -4879,7 +4979,17 @@ namespace Barotrauma stream.Dispose(); } + public bool IsSubcategoryHidden(string subcategory) + { + if (string.IsNullOrEmpty(subcategory) || !hiddenSubCategories.ContainsKey(subcategory)) + { + return false; + } + return hiddenSubCategories[subcategory]; + } + public static bool IsSubEditor() => Screen.Selected is SubEditorScreen && !Submarine.Unloading; public static bool IsWiringMode() => Screen.Selected == GameMain.SubEditorScreen && GameMain.SubEditorScreen.WiringMode && !Submarine.Unloading; + } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index 3b9d3d2d1..dff1e31da 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -1208,7 +1208,7 @@ namespace Barotrauma SafeAdd((ISerializableEntity) entity, property); property.PropertyInfo.SetValue(entity, value); } - else if (entity is ISerializableEntity sEntity && sEntity.SerializableProperties != null) + else if (entity is ISerializableEntity { SerializableProperties: { } } sEntity) { var props = sEntity.SerializableProperties; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs index 01a06e4ec..7249c9972 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs @@ -30,6 +30,7 @@ namespace Barotrauma.Sounds public override int FillStreamBuffer(int samplePos, short[] buffer) { if (!Stream) throw new Exception("Called FillStreamBuffer on a non-streamed sound!"); + if (reader == null) throw new Exception("Called FillStreamBuffer when the reader is null!"); if (samplePos >= reader.TotalSamples * reader.Channels * 2) return 0; @@ -41,7 +42,7 @@ namespace Barotrauma.Sounds //MuffleBuffer(floatBuffer, reader.Channels); CastBuffer(floatBuffer, buffer, readSamples); - return readSamples * 2; + return readSamples; } static void MuffleBuffer(float[] buffer, int sampleRate, int channelCount) @@ -63,8 +64,20 @@ namespace Barotrauma.Sounds ALFormat = reader.Channels == 1 ? Al.FormatMono16 : Al.FormatStereo16; SampleRate = reader.SampleRate; + if (Buffers != null && SoundBuffers.BuffersGenerated < SoundBuffers.MaxBuffers) + { + Buffers.RequestAlBuffers(); FillBuffers(); + } + } + + public override void FillBuffers() + { if (!Stream) { + reader ??= new VorbisReader(Filename); + + reader.DecodedPosition = 0; + int bufferSize = (int)reader.TotalSamples * reader.Channels; float[] floatBuffer = new float[bufferSize]; @@ -86,26 +99,26 @@ namespace Barotrauma.Sounds CastBuffer(floatBuffer, shortBuffer, readSamples); - Al.BufferData(ALBuffer, ALFormat, shortBuffer, + Al.BufferData(Buffers.AlBuffer, ALFormat, shortBuffer, readSamples * sizeof(short), SampleRate); int alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to set buffer data for non-streamed audio! " + Al.GetErrorString(alError)); + throw new Exception("Failed to set regular buffer data for non-streamed audio! " + Al.GetErrorString(alError)); } MuffleBuffer(floatBuffer, SampleRate, reader.Channels); CastBuffer(floatBuffer, shortBuffer, readSamples); - Al.BufferData(ALMuffledBuffer, ALFormat, shortBuffer, + Al.BufferData(Buffers.AlMuffledBuffer, ALFormat, shortBuffer, readSamples * sizeof(short), SampleRate); alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to set buffer data for non-streamed audio! " + Al.GetErrorString(alError)); + throw new Exception("Failed to set muffled buffer data for non-streamed audio! " + Al.GetErrorString(alError)); } reader.Dispose(); reader = null; @@ -116,7 +129,7 @@ namespace Barotrauma.Sounds { if (Stream) { - reader.Dispose(); + reader?.Dispose(); } base.Dispose(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs index 8aadfa00b..fbcca4b32 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs @@ -14,35 +14,15 @@ namespace Barotrauma.Sounds get { return disposed; } } - public SoundManager Owner - { - get; - protected set; - } + public readonly SoundManager Owner; - public string Filename - { - get; - protected set; - } + public readonly string Filename; - public XElement XElement - { - get; - protected set; - } + public readonly XElement XElement; - public bool Stream - { - get; - protected set; - } + public readonly bool Stream; - public bool StreamsReliably - { - get; - protected set; - } + public readonly bool StreamsReliably; public virtual SoundManager.SourcePoolIndex SourcePoolIndex { @@ -52,16 +32,10 @@ namespace Barotrauma.Sounds } } - private uint alBuffer; - public uint ALBuffer + private SoundBuffers buffers; + public SoundBuffers Buffers { - get { return !Stream ? alBuffer : 0; } - } - - private uint alMuffledBuffer; - public uint ALMuffledBuffer - { - get { return !Stream ? alMuffledBuffer : 0; } + get { return !Stream ? buffers : null; } } public int ALFormat @@ -85,10 +59,10 @@ namespace Barotrauma.Sounds public float BaseNear; public float BaseFar; - public Sound(SoundManager owner, string filename, bool stream, bool streamsReliably, XElement xElement=null) + public Sound(SoundManager owner, string filename, bool stream, bool streamsReliably, XElement xElement=null, bool getFullPath=true) { Owner = owner; - Filename = Path.GetFullPath(filename.CleanUpPath()).CleanUpPath(); + Filename = getFullPath ? Path.GetFullPath(filename.CleanUpPath()).CleanUpPath() : filename; Stream = stream; StreamsReliably = streamsReliably; XElement = xElement; @@ -169,69 +143,20 @@ namespace Barotrauma.Sounds { if (!Stream) { - Al.GenBuffer(out alBuffer); - int alError = Al.GetError(); - if (alError != Al.NoError) - { - throw new Exception("Failed to create OpenAL buffer for non-streamed sound: " + Al.GetErrorString(alError)); - } - - if (!Al.IsBuffer(alBuffer)) - { - throw new Exception("Generated OpenAL buffer is invalid!"); - } - - Al.GenBuffer(out alMuffledBuffer); - alError = Al.GetError(); - if (alError != Al.NoError) - { - throw new Exception("Failed to create OpenAL buffer for non-streamed sound: " + Al.GetErrorString(alError)); - } - - if (!Al.IsBuffer(alMuffledBuffer)) - { - throw new Exception("Generated OpenAL buffer is invalid!"); - } + buffers = new SoundBuffers(this); } else { - alBuffer = 0; + buffers = null; } } + public virtual void FillBuffers() { } + public virtual void DeleteALBuffers() { Owner.KillChannels(this); - if (alBuffer != 0) - { - if (!Al.IsBuffer(alBuffer)) - { - throw new Exception("Buffer to delete is invalid!"); - } - - Al.DeleteBuffer(alBuffer); alBuffer = 0; - - int alError = Al.GetError(); - if (alError != Al.NoError) - { - throw new Exception("Failed to delete OpenAL buffer for non-streamed sound: " + Al.GetErrorString(alError)); - } - } - if (alMuffledBuffer != 0) - { - if (!Al.IsBuffer(alMuffledBuffer)) - { - throw new Exception("Buffer to delete is invalid!"); - } - - Al.DeleteBuffer(alMuffledBuffer); alMuffledBuffer = 0; - - int alError = Al.GetError(); - if (alError != Al.NoError) - { - throw new Exception("Failed to delete OpenAL buffer for non-streamed sound: " + Al.GetErrorString(alError)); - } - } + buffers?.Dispose(); } public virtual void Dispose() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundBuffer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundBuffer.cs new file mode 100644 index 000000000..8cbcc5b37 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundBuffer.cs @@ -0,0 +1,122 @@ +using Barotrauma.Extensions; +using OpenAL; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Barotrauma.Sounds +{ + public class SoundBuffers : IDisposable + { + private static HashSet bufferPool = new HashSet(); +#if OSX + public const int MaxBuffers = 400; //TODO: check that this value works for macOS +#else + public const int MaxBuffers = 32000; +#endif + public static int BuffersGenerated { get; private set; } = 0; + private Sound sound; + + public uint AlBuffer { get; private set; } = 0; + public uint AlMuffledBuffer { get; private set; } = 0; + + public SoundBuffers(Sound sound) { this.sound = sound; } + public void Dispose() + { + if (AlBuffer != 0) { bufferPool.Add(AlBuffer); } + if (AlMuffledBuffer != 0) { bufferPool.Add(AlMuffledBuffer); } + AlBuffer = 0; + AlMuffledBuffer = 0; + } + + public static void ClearPool() + { + bufferPool.ForEach(b => Al.DeleteBuffer(b)); + bufferPool.Clear(); + BuffersGenerated = 0; + } + + public bool RequestAlBuffers() + { + if (AlBuffer != 0) { return false; } + int alError = 0; + while (bufferPool.Count < 2 && BuffersGenerated < MaxBuffers) + { + Al.GenBuffer(out uint newBuffer); + alError = Al.GetError(); + if (alError != Al.NoError) + { + DebugConsole.AddWarning($"Error when generating sound buffer: {Al.GetErrorString(alError)}. {BuffersGenerated} buffer(s) were generated. No more sound buffers will be generated."); + BuffersGenerated = MaxBuffers; + } + else if (!Al.IsBuffer(newBuffer)) + { + DebugConsole.AddWarning($"Error when generating sound buffer: result is not a valid buffer. {BuffersGenerated} buffer(s) were generated. No more sound buffers will be generated."); + BuffersGenerated = MaxBuffers; + } + else + { + bufferPool.Add(newBuffer); + BuffersGenerated++; + if (BuffersGenerated >= MaxBuffers) + { + DebugConsole.AddWarning($"{BuffersGenerated} buffer(s) were generated. No more sound buffers will be generated."); + } + } + } + + if (bufferPool.Count >= 2) + { + AlBuffer = bufferPool.First(); + bufferPool.Remove(AlBuffer); + AlMuffledBuffer = bufferPool.First(); + bufferPool.Remove(AlMuffledBuffer); + return true; + } + + //can't generate any more OpenAL buffers! we'll have to steal a buffer from someone... + foreach (var otherSound in sound.Owner.LoadedSounds) + { + if (otherSound == sound) { continue; } + if (otherSound.IsPlaying()) { continue; } + if (otherSound.Buffers == null) { continue; } + if (otherSound.Buffers.AlBuffer == 0) { continue; } + + // Dispose all channels that are holding + // a reference to these buffers, otherwise + // an INVALID_OPERATION error will be thrown + // when attempting to set the buffer data later. + // Having the sources not play is not enough, + // as OpenAL assumes that you may want to call + // alSourcePlay without reassigning the buffer. + otherSound.Owner.KillChannels(otherSound); + + AlBuffer = otherSound.Buffers.AlBuffer; + AlMuffledBuffer = otherSound.Buffers.AlMuffledBuffer; + otherSound.Buffers.AlBuffer = 0; + otherSound.Buffers.AlMuffledBuffer = 0; + + // For performance reasons, sift the current sound to + // the end of the loadedSounds list, that way it'll + // be less likely to have its buffers stolen, which + // means less reuploads for frequently played sounds. + sound.Owner.MoveSoundToPosition(sound, sound.Owner.LoadedSoundCount-1); + + if (!Al.IsBuffer(AlBuffer)) + { + throw new Exception(sound.Filename + " has an invalid buffer!"); + } + if (!Al.IsBuffer(AlMuffledBuffer)) + { + throw new Exception(sound.Filename + " has an invalid muffled buffer!"); + } + + + return true; + } + + return false; + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs index 48a2cc7f5..45a3e50bf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs @@ -334,7 +334,11 @@ namespace Barotrauma.Sounds return; } - Al.Sourcei(alSource, Al.Buffer, muffled ? (int)Sound.ALMuffledBuffer : (int)Sound.ALBuffer); + if (Sound.Buffers.RequestAlBuffers()) + { + Sound.FillBuffers(); + } + Al.Sourcei(alSource, Al.Buffer, muffled ? (int)Sound.Buffers.AlMuffledBuffer : (int)Sound.Buffers.AlBuffer); alError = Al.GetError(); if (alError != Al.NoError) @@ -487,8 +491,10 @@ namespace Barotrauma.Sounds mutex = new object(); } +#if !DEBUG try { +#endif if (mutex != null) { Monitor.Enter(mutex); } if (sound.Owner.CountPlayingInstances(sound) < sound.MaxSimultaneousInstances) { @@ -506,17 +512,17 @@ namespace Barotrauma.Sounds throw new Exception("Failed to reset source buffer: " + debugName + ", " + Al.GetErrorString(alError)); } - if (!Al.IsBuffer(sound.ALBuffer)) + if (Sound.Buffers.RequestAlBuffers()) { - throw new Exception(sound.Filename + " has an invalid buffer!"); + Sound.FillBuffers(); } - uint alBuffer = sound.Owner.GetCategoryMuffle(category) || muffle ? sound.ALMuffledBuffer : sound.ALBuffer; + uint alBuffer = sound.Owner.GetCategoryMuffle(category) || muffled ? Sound.Buffers.AlMuffledBuffer : Sound.Buffers.AlBuffer; Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Buffer, (int)alBuffer); alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to bind buffer to source (" + ALSourceIndex.ToString() + ":" + sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex) + "," + sound.ALBuffer.ToString() + "): " + debugName + ", " + Al.GetErrorString(alError)); + throw new Exception("Failed to bind buffer to source (" + ALSourceIndex.ToString() + ":" + sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex) + "," + alBuffer.ToString() + "): " + debugName + ", " + Al.GetErrorString(alError)); } Al.SourcePlay(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex)); @@ -528,7 +534,7 @@ namespace Barotrauma.Sounds } else { - uint alBuffer = sound.Owner.GetCategoryMuffle(category) || muffle ? sound.ALMuffledBuffer : sound.ALBuffer; + uint alBuffer = 0; Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Buffer, (int)alBuffer); int alError = Al.GetError(); if (alError != Al.NoError) @@ -574,6 +580,7 @@ namespace Barotrauma.Sounds this.Near = near; this.Far = far; this.Category = category; +#if !DEBUG } catch { @@ -581,8 +588,11 @@ namespace Barotrauma.Sounds } finally { +#endif if (mutex != null) { Monitor.Exit(mutex); } +#if !DEBUG } +#endif Sound.Owner.Update(); } @@ -736,11 +746,6 @@ namespace Barotrauma.Sounds if (FilledByNetwork) { - if (Sound is VoipSound voipSound) - { - voipSound.ApplyFilters(buffer, readSamples); - } - if (readSamples <= 0) { streamAmplitude *= 0.5f; @@ -752,13 +757,18 @@ namespace Barotrauma.Sounds } else { + if (Sound is VoipSound voipSound) + { + voipSound.ApplyFilters(buffer, readSamples); + } + decayTimer = 0; } } else if (Sound.StreamsReliably) { - streamSeekPos += readSamples; - if (readSamples < STREAM_BUFFER_SIZE) + streamSeekPos += readSamples * 2; + if (readSamples * 2 < STREAM_BUFFER_SIZE) { if (looping) { @@ -775,7 +785,7 @@ namespace Barotrauma.Sounds { streamBufferAmplitudes[index] = readAmplitude; - Al.BufferData(streamBuffers[index], Sound.ALFormat, buffer, readSamples, Sound.SampleRate); + Al.BufferData(streamBuffers[index], Sound.ALFormat, buffer, readSamples * 2, Sound.SampleRate); alError = Al.GetError(); if (alError != Al.NoError) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs index 05d332d20..d5defd0dd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs @@ -30,6 +30,8 @@ namespace Barotrauma.Sounds private readonly SoundSourcePool[] sourcePools; private readonly List loadedSounds; + public IReadOnlyList LoadedSounds => loadedSounds; + private readonly SoundChannel[][] playingChannels = new SoundChannel[2][]; private readonly object threadDeathMutex = new object(); @@ -512,6 +514,18 @@ namespace Barotrauma.Sounds } } + public void MoveSoundToPosition(Sound sound, int pos) + { + lock (loadedSounds) + { + int index = loadedSounds.IndexOf(sound); + if (index >= 0) + { + loadedSounds.SiftElement(index, pos); + } + } + } + public void SetCategoryGainMultiplier(string category, float gain, int index=0) { if (Disabled) { return; } @@ -706,9 +720,11 @@ namespace Barotrauma.Sounds } bool areStreamsPlaying = false; + ManualResetEvent streamMre = null; void UpdateStreaming() { + streamMre = new ManualResetEvent(false); bool killThread = false; while (!killThread) { @@ -745,14 +761,20 @@ namespace Barotrauma.Sounds } } } + streamMre.WaitOne(10); + streamMre.Reset(); lock (threadDeathMutex) { areStreamsPlaying = !killThread; } - Thread.Sleep(10); //TODO: use a separate thread for network audio? } } + public void ForceStreamUpdate() + { + streamMre?.Set(); + } + private void ReloadSounds() { for (int i = loadedSounds.Count - 1; i >= 0; i--) @@ -788,6 +810,8 @@ namespace Barotrauma.Sounds } sourcePools[(int)SourcePoolIndex.Default]?.Dispose(); sourcePools[(int)SourcePoolIndex.Voice]?.Dispose(); + + SoundBuffers.ClearPool(); } public void Dispose() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index ffbee3767..158de9924 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -21,12 +21,14 @@ namespace Barotrauma public readonly string requiredTag; - public DamageSound(Sound sound, Vector2 damageRange, string damageType, string requiredTag = "") + public bool ignoreMuffling; + + public DamageSound(Sound sound, Vector2 damageRange, string damageType, bool ignoreMuffling, string requiredTag = "") { this.sound = sound; this.damageRange = damageRange; this.damageType = damageType; - + this.ignoreMuffling = ignoreMuffling; this.requiredTag = requiredTag; } } @@ -195,14 +197,21 @@ namespace Barotrauma { case "music": var newMusicClip = new BackgroundMusic(soundElement); - musicClips.AddIfNotNull(newMusicClip); - if (loadedSoundElements != null) + if (File.Exists(newMusicClip.File)) { - if (newMusicClip.Type.Equals("menu", StringComparison.OrdinalIgnoreCase)) + musicClips.AddIfNotNull(newMusicClip); + if (loadedSoundElements != null) { - targetMusic[0] = newMusicClip; + if (newMusicClip.Type.Equals("menu", StringComparison.OrdinalIgnoreCase)) + { + targetMusic[0] = newMusicClip; + } } } + else + { + DebugConsole.NewMessage($"Music file \"{newMusicClip.File}\" not found."); + } break; case "splash": SplashSounds.AddIfNotNull(GameMain.SoundManager.LoadSound(soundElement, false)); @@ -262,6 +271,7 @@ namespace Barotrauma damageSound, soundElement.GetAttributeVector2("damagerange", Vector2.Zero), damageSoundType, + soundElement.GetAttributeBool("ignoremuffling", false), soundElement.GetAttributeString("requiredtag", ""))); break; @@ -524,7 +534,7 @@ namespace Barotrauma Vector2 diff = gap.WorldPosition - listenerPos; if (Math.Abs(diff.X) < FlowSoundRange && Math.Abs(diff.Y) < FlowSoundRange) { - if (gap.Open < 0.01f) { continue; } + if (gap.Open < 0.01f || gap.LerpedFlowForce.LengthSquared() < 100.0f) { continue; } float gapFlow = Math.Abs(gap.LerpedFlowForce.X) + Math.Abs(gap.LerpedFlowForce.Y) * 2.5f; if (!gap.IsRoomToRoom) { gapFlow *= 2.0f; } if (gapFlow < 10.0f) { continue; } @@ -887,7 +897,17 @@ namespace Barotrauma if (currentMusic[i] == null || (musicChannel[i] == null || !musicChannel[i].IsPlaying)) { DisposeMusicChannel(i); - currentMusic[i] = GameMain.SoundManager.LoadSound(targetMusic[i].File, true); + try + { + currentMusic[i] = GameMain.SoundManager.LoadSound(targetMusic[i].File, true); + } + catch (System.IO.InvalidDataException e) + { + DebugConsole.ThrowError($"Failed to load the music clip \"{targetMusic[i].File}\".", e); + musicClips.Remove(targetMusic[i]); + targetMusic[i] = null; + break; + } musicChannel[i] = currentMusic[i].Play(0.0f, "music"); if (targetMusic[i].ContinueFromPreviousTime) { @@ -983,13 +1003,17 @@ namespace Barotrauma } Submarine targetSubmarine = Character.Controlled?.Submarine; - if ((targetSubmarine != null && targetSubmarine.AtDamageDepth) || - (GameMain.GameScreen != null && Screen.Selected == GameMain.GameScreen && Level.Loaded != null && Level.Loaded.GetRealWorldDepth(GameMain.GameScreen.Cam.Position.Y) > Level.Loaded.RealWorldCrushDepth)) + if (targetSubmarine != null && targetSubmarine.AtDamageDepth) + { + return "deep"; + } + if (GameMain.GameScreen != null && Screen.Selected == GameMain.GameScreen && Submarine.MainSub != null && + Level.Loaded != null && Level.Loaded.GetRealWorldDepth(GameMain.GameScreen.Cam.Position.Y) > Submarine.MainSub.RealWorldCrushDepth) { return "deep"; } - if (targetSubmarine != null) + if (targetSubmarine != null) { float floodedArea = 0.0f; float totalArea = 0.0f; @@ -1033,7 +1057,7 @@ namespace Barotrauma if (GameMain.GameSession != null) { - if (Submarine.Loaded != null && Level.Loaded != null && Submarine.MainSub != null && Submarine.MainSub.AtEndPosition) + if (Submarine.Loaded != null && Level.Loaded != null && Submarine.MainSub != null && Submarine.MainSub.AtEndExit) { return "levelend"; } @@ -1102,7 +1126,11 @@ namespace Barotrauma tempList.Add(s); } } - tempList.GetRandom().sound?.Play(1.0f, range, position, muffle: ShouldMuffleSound(Character.Controlled, position, range, null)); + var damageSound = tempList.GetRandom(); + if (damageSound.sound != null) + { + damageSound.sound.Play(1.0f, range, position, muffle: !damageSound.ignoreMuffling && ShouldMuffleSound(Character.Controlled, position, range, null)); + } } public static void PlayUISound(GUISoundType soundType) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/VideoSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/VideoSound.cs index 11bf78a85..d1a282e15 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/VideoSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/VideoSound.cs @@ -107,7 +107,7 @@ namespace Barotrauma.Sounds readAmount += buf.Length; } } - return readAmount*2; + return readAmount; } public override void Dispose() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs index 072a38cfe..89800a93f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs @@ -1,4 +1,5 @@ -using Barotrauma.Networking; +using Barotrauma.IO; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using OpenAL; using System; @@ -35,14 +36,15 @@ namespace Barotrauma.Sounds public float Near { get; private set; } public float Far { get; private set; } - private static BiQuad[] muffleFilters = new BiQuad[] + private BiQuad[] muffleFilters = new BiQuad[] { new LowpassFilter(VoipConfig.FREQUENCY, 800) }; - private static BiQuad[] radioFilters = new BiQuad[] + private BiQuad[] radioFilters = new BiQuad[] { new BandpassFilter(VoipConfig.FREQUENCY, 2000) }; + private const float PostRadioFilterBoost = 1.2f; private float gain; public float Gain @@ -61,10 +63,8 @@ namespace Barotrauma.Sounds get { return soundChannel?.CurrentAmplitude ?? 0.0f; } } - public VoipSound(string name, SoundManager owner, VoipQueue q) : base(owner, "voip", true, true) + public VoipSound(string name, SoundManager owner, VoipQueue q) : base(owner, $"VoIP ({name})", true, true, getFullPath: false) { - Filename = $"VoIP ({name})"; - VoipConfig.SetupEncoding(); ALFormat = Al.FormatMono16; @@ -104,7 +104,7 @@ namespace Barotrauma.Sounds if (gain * GameMain.Config.VoiceChatVolume > 1.0f) //TODO: take distance into account? { - fVal = Math.Clamp(fVal * gain * GameMain.Config.VoiceChatVolume, -1.0f, 1.0f); + fVal = Math.Clamp(fVal * gain * GameMain.Config.VoiceChatVolume, -1f, 1f); } if (UseMuffleFilter) @@ -118,7 +118,7 @@ namespace Barotrauma.Sounds { foreach (var filter in radioFilters) { - fVal = filter.Process(fVal); + fVal = Math.Clamp(filter.Process(fVal) * PostRadioFilterBoost, -1f, 1f); } } buffer[i] = FloatToShort(fVal); @@ -154,7 +154,7 @@ namespace Barotrauma.Sounds { VoipConfig.Decoder.Decode(compressedBuffer, 0, compressedSize, buffer, 0, VoipConfig.BUFFER_SIZE); bufferID++; - return VoipConfig.BUFFER_SIZE * 2; + return VoipConfig.BUFFER_SIZE; } if (bufferID < queue.LatestBufferID - (VoipQueue.BUFFER_COUNT - 1)) bufferID = queue.LatestBufferID - (VoipQueue.BUFFER_COUNT - 1); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs index 01524a0ec..2a86615d1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs @@ -29,7 +29,16 @@ namespace Barotrauma get { return texture != null && !cannotBeLoaded; } } - public Sprite(Texture2D texture, Rectangle? sourceRectangle, Vector2? newOffset, float newRotation = 0.0f) + public Sprite(Sprite other) : this(other.texture, other.sourceRect, other.offset, other.rotation) + { + FilePath = other.FilePath; + FullPath = other.FullPath; + Compress = other.Compress; + size = other.size; + effects = other.effects; + } + + public Sprite(Texture2D texture, Rectangle? sourceRectangle, Vector2? newOffset, float newRotation = 0.0f, string path = null) { this.texture = texture; @@ -45,6 +54,8 @@ namespace Barotrauma rotation = newRotation; + FilePath = path; + AddToList(this); } @@ -172,22 +183,22 @@ namespace Barotrauma return null; } - public void Draw(SpriteBatch spriteBatch, Vector2 pos, float rotate = 0.0f, float scale = 1.0f, SpriteEffects spriteEffect = SpriteEffects.None) + public void Draw(ISpriteBatch spriteBatch, Vector2 pos, float rotate = 0.0f, float scale = 1.0f, SpriteEffects spriteEffect = SpriteEffects.None) { this.Draw(spriteBatch, pos, Color.White, rotate, scale, spriteEffect); } - public void Draw(SpriteBatch spriteBatch, Vector2 pos, Color color, float rotate = 0.0f, float scale = 1.0f, SpriteEffects spriteEffect = SpriteEffects.None, float? depth = null) + public void Draw(ISpriteBatch spriteBatch, Vector2 pos, Color color, float rotate = 0.0f, float scale = 1.0f, SpriteEffects spriteEffect = SpriteEffects.None, float? depth = null) { this.Draw(spriteBatch, pos, color, this.origin, rotate, new Vector2(scale, scale), spriteEffect, depth); } - public void Draw(SpriteBatch spriteBatch, Vector2 pos, Color color, Vector2 origin, float rotate = 0.0f, float scale = 1.0f, SpriteEffects spriteEffect = SpriteEffects.None, float? depth = null) + public void Draw(ISpriteBatch spriteBatch, Vector2 pos, Color color, Vector2 origin, float rotate = 0.0f, float scale = 1.0f, SpriteEffects spriteEffect = SpriteEffects.None, float? depth = null) { this.Draw(spriteBatch, pos, color, origin, rotate, new Vector2(scale, scale), spriteEffect, depth); } - public virtual void Draw(SpriteBatch spriteBatch, Vector2 pos, Color color, Vector2 origin, float rotate, Vector2 scale, SpriteEffects spriteEffect = SpriteEffects.None, float? depth = null) + public virtual void Draw(ISpriteBatch spriteBatch, Vector2 pos, Color color, Vector2 origin, float rotate, Vector2 scale, SpriteEffects spriteEffect = SpriteEffects.None, float? depth = null) { if (Texture == null) { return; } //DrawSilhouette(spriteBatch, pos, origin, rotate, scale, spriteEffect, depth); @@ -209,12 +220,12 @@ namespace Barotrauma } } - public void DrawTiled(SpriteBatch spriteBatch, Vector2 position, Vector2 targetSize, + public void DrawTiled(ISpriteBatch spriteBatch, Vector2 position, Vector2 targetSize, Color? color = null, Vector2? startOffset = null, Vector2? textureScale = null, float? depth = null) { if (Texture == null) { return; } //Init optional values - Vector2 drawOffset = startOffset.HasValue ? startOffset.Value : Vector2.Zero; + Vector2 drawOffset = startOffset ?? Vector2.Zero; Vector2 scale = textureScale ?? Vector2.One; Color drawColor = color ?? Color.White; @@ -224,17 +235,20 @@ namespace Barotrauma //wrap the drawOffset inside the sourceRect drawOffset.X = (drawOffset.X / scale.X) % sourceRect.Width; drawOffset.Y = (drawOffset.Y / scale.Y) % sourceRect.Height; + + Vector2 flippedDrawOffset = Vector2.Zero; if (flipHorizontal) { - float diff = targetSize.X % (sourceRect.Width * scale.X); - drawOffset.X += (sourceRect.Width * scale.X - diff) / scale.X; + float diff = targetSize.X % (sourceRect.Width * scale.X); + flippedDrawOffset.X = (int)((sourceRect.Width * scale.X - diff) / scale.X); } if (flipVertical) { float diff = targetSize.Y % (sourceRect.Height * scale.Y); - drawOffset.Y += (sourceRect.Height * scale.Y - diff) / scale.Y; + flippedDrawOffset.Y = (int)((sourceRect.Height * scale.Y - diff) / scale.Y); } - + drawOffset += flippedDrawOffset; + //how many times the texture needs to be drawn on the x-axis int xTiles = (int)Math.Ceiling((targetSize.X + drawOffset.X * scale.X) / (sourceRect.Width * scale.X)); //how many times the texture needs to be drawn on the y-axis @@ -262,6 +276,10 @@ namespace Barotrauma { texPerspective.X += (int)diff; } + if (!flipVertical) + { + texPerspective.Y += (int)diff; + } } //drawing an offset flipped sprite, need to draw an extra slice to the left side if (currDrawPosition.X > position.X && x == 0) @@ -278,7 +296,7 @@ namespace Barotrauma if (flipVertical) { - slicePos.Y += size.Y; + slicePos.Y += flippedDrawOffset.Y; } spriteBatch.Draw(texture, slicePos, sliceRect, drawColor, rotation, Vector2.Zero, scale, effects, depth ?? this.depth); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/SpriteSheet.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/SpriteSheet.cs index 9538ca7f3..105bb81bf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/SpriteSheet.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/SpriteSheet.cs @@ -5,14 +5,14 @@ namespace Barotrauma { partial class SpriteSheet : Sprite { - public override void Draw(SpriteBatch spriteBatch, Vector2 pos, Color color, Vector2 origin, float rotate, Vector2 scale, SpriteEffects spriteEffect = SpriteEffects.None, float? depth = default(float?)) + public override void Draw(ISpriteBatch spriteBatch, Vector2 pos, Color color, Vector2 origin, float rotate, Vector2 scale, SpriteEffects spriteEffect = SpriteEffects.None, float? depth = default(float?)) { if (texture == null) return; spriteBatch.Draw(texture, pos + offset, sourceRects[0], color, rotation + rotate, origin, scale, spriteEffect, depth == null ? this.depth : (float)depth); } - public void Draw(SpriteBatch spriteBatch, int spriteIndex, Vector2 pos, Color color, Vector2 origin, float rotate, Vector2 scale, SpriteEffects spriteEffect = SpriteEffects.None, float? depth = default(float?)) + public void Draw(ISpriteBatch spriteBatch, int spriteIndex, Vector2 pos, Color color, Vector2 origin, float rotate, Vector2 scale, SpriteEffects spriteEffect = SpriteEffects.None, float? depth = default(float?)) { if (texture == null) return; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs new file mode 100644 index 000000000..403a70475 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs @@ -0,0 +1,262 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Xml.Linq; +using Barotrauma.IO; +using Barotrauma.Items.Components; + +namespace Barotrauma +{ + public class SpreadsheetExport + { + private const char separator = ','; + private const string debugIdentifier = "_spreadsheet"; + + public static void Export() + { + XDocument doc = new XDocument(); + if (doc.Root == null) + { + doc.Add(new XElement("Content")); + } + + XElement root = doc.Root!; + + foreach (ItemPrefab prefab in ItemPrefab.Prefabs) + { + XElement itemElement = new XElement("Item", + new XAttribute("identifier", prefab.Identifier), + new XAttribute("name", prefab.Name), + new XAttribute("tags", FormatArray(prefab.Tags)), + new XAttribute("value", prefab.DefaultPrice?.Price ?? 0) + ); + + itemElement.Add(ParseRecipe(prefab)); + itemElement.Add(ParseDecon(prefab)); + itemElement.Add(ParseMedical(prefab)); + itemElement.Add(ParseWeapon(prefab)); + + root.Add(itemElement); + } + + System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings + { + Indent = false, + NewLineOnAttributes = false + }; + + using XmlWriter? writer = XmlWriter.Create("spreadsheetdata.xml", settings); + doc.SaveSafe(writer); + } + + private static XElement ParseRecipe(ItemPrefab prefab) + { + FabricationRecipe? recipe = prefab.FabricationRecipes.FirstOrDefault(); + + List ingredients = recipe?.RequiredItems.SelectMany(ri => ri.ItemPrefabs).Distinct().ToList() ?? new List(); + Skill? skill = recipe?.RequiredSkills.FirstOrDefault(); + + return new XElement("Recipe", + new XAttribute("amount", recipe?.Amount ?? 0), + new XAttribute("time", recipe?.RequiredTime ?? 0), + new XAttribute("skillname", skill?.Identifier ?? ""), + new XAttribute("skillamount", (int?) skill?.Level ?? 0), + new XAttribute("ingredients", FormatArray(ingredients.Select(ip => ip.Name))), + new XAttribute("values", FormatArray(ingredients.Select(ip => ip.DefaultPrice?.Price ?? 0))) + ); + } + + private static XElement ParseDecon(ItemPrefab prefab) + { + List deconOutput = prefab.DeconstructItems.Select(item => ItemPrefab.Find(null, item.ItemIdentifier)).Where(outputPrefab => outputPrefab != null).ToList(); + return new XElement("Deconstruct", + new XAttribute("time", prefab.DeconstructTime), + new XAttribute("outputs", FormatArray(deconOutput.Select(ip => ip.Name))), + new XAttribute("values", FormatArray(deconOutput.Select(ip => ip.DefaultPrice?.Price ?? 0))) + ); + } + + private static XElement ParseMedical(ItemPrefab prefab) + { + XElement? itemMeleeWeapon = prefab.ConfigElement.GetChildElement(nameof(MeleeWeapon)); + // affliction, amount, duration + List> onSuccessAfflictions = new List>(); + List> onFailureAfflictions = new List>(); + int medicalRequiredSkill = 0; + if (itemMeleeWeapon != null) + { + List statusEffects = new List(); + foreach (XElement subElement in itemMeleeWeapon.Elements()) + { + string name = subElement.Name.ToString(); + if (name.Equals(nameof(StatusEffect), StringComparison.OrdinalIgnoreCase)) + { + StatusEffect statusEffect = StatusEffect.Load(subElement, debugIdentifier); + if (statusEffect == null || !statusEffect.HasTag("medical")) { continue; } + + statusEffects.Add(statusEffect); + } + else if (IsRequiredSkill(subElement, out Skill? skill) && skill != null) + { + medicalRequiredSkill = (int) skill.Level; + } + } + + List successEffects = statusEffects.Where(se => se.type == ActionType.OnUse).ToList(); + List failureEffects = statusEffects.Where(se => se.type == ActionType.OnFailure).ToList(); + + foreach (StatusEffect statusEffect in successEffects) + { + float duration = statusEffect.Duration; + onSuccessAfflictions.AddRange(statusEffect.ReduceAffliction.Select(pair => Tuple.Create(GetAfflictionName(pair.First), -pair.Second, duration))); + onSuccessAfflictions.AddRange(statusEffect.Afflictions.Select(affliction => Tuple.Create(affliction.Prefab.Name, affliction.NonClampedStrength, duration))); + } + + foreach (StatusEffect statusEffect in failureEffects) + { + float duration = statusEffect.Duration; + onFailureAfflictions.AddRange(statusEffect.ReduceAffliction.Select(pair => Tuple.Create(GetAfflictionName(pair.First), -pair.Second, duration))); + onFailureAfflictions.AddRange(statusEffect.Afflictions.Select(affliction => Tuple.Create(affliction.Prefab.Name, affliction.NonClampedStrength, duration))); + } + } + + return new XElement("Medical", + new XAttribute("skillamount", medicalRequiredSkill), + new XAttribute("successafflictions", FormatArray(onSuccessAfflictions.Select(tpl => tpl.Item1))), + new XAttribute("successamounts", FormatArray(onSuccessAfflictions.Select(tpl => FormatFloat(tpl.Item2)))), + new XAttribute("successdurations", FormatArray(onSuccessAfflictions.Select(tpl => FormatFloat(tpl.Item3)))), + new XAttribute("failureafflictions", FormatArray(onFailureAfflictions.Select(tpl => tpl.Item1))), + new XAttribute("failureamounts", FormatArray(onFailureAfflictions.Select(tpl => FormatFloat(tpl.Item2)))), + new XAttribute("failuredurations", FormatArray(onFailureAfflictions.Select(tpl => FormatFloat(tpl.Item3)))) + ); + } + + private static XElement ParseWeapon(ItemPrefab prefab) + { + float stun = 0; + bool isAoE = false; + float? structDamage = null; + int skillRequirement = 0; + + // affliction, amount + List> damages = new List>(); + + string[] validNames = { nameof(Projectile), nameof(MeleeWeapon), nameof(RepairTool), nameof(ItemComponent), nameof(RangedWeapon) }; + foreach (XElement icElement in prefab.ConfigElement.Elements()) + { + string icName = icElement.Name.ToString(); + if (!validNames.Any(name => icName.Equals(name, StringComparison.OrdinalIgnoreCase))) { continue; } + + foreach (XElement icChildElement in icElement.Elements()) + { + string name = icChildElement.Name.ToString(); + if (IsRequiredSkill(icChildElement, out Skill? skill) && skill != null) + { + skillRequirement = (int) skill.Level; + } + else if (name.Equals(nameof(Attack), StringComparison.OrdinalIgnoreCase)) + { + ParseAttack(new Attack(icChildElement, debugIdentifier)); + } + else if (name.Equals(nameof(Explosion), StringComparison.OrdinalIgnoreCase)) + { + ParseExplosion(new[] { new Explosion(icChildElement, debugIdentifier) }); + } + else if (name.Equals(nameof(StatusEffect), StringComparison.OrdinalIgnoreCase)) + { + ParseStatusEffect(new[] { StatusEffect.Load(icChildElement, debugIdentifier) }); + } + + void ParseStatusEffect(IEnumerable statusEffects) + { + foreach (StatusEffect effect in statusEffects) + { + if (effect.HasTargetType(StatusEffect.TargetType.Character)) { continue; } + + ParseAfflictions(effect.Afflictions); + ParseExplosion(effect.Explosions); + } + } + + void ParseExplosion(IEnumerable explosions) + { + foreach (Explosion explosion in explosions) + { + isAoE = true; + ParseAttack(explosion.Attack); + ParseStatusEffect(explosion.Attack.StatusEffects); + } + } + + void ParseAttack(Attack attack) + { + structDamage ??= attack.StructureDamage; + ParseAfflictions(attack.Afflictions.Keys); + ParseStatusEffect(attack.StatusEffects); + } + + void ParseAfflictions(IEnumerable afflictions) + { + foreach (Affliction affliction in afflictions) + { + // Exclude stuns + if (affliction.Prefab == AfflictionPrefab.Stun) + { + stun += affliction.NonClampedStrength; + continue; + } + + damages.Add(Tuple.Create(affliction.Prefab.Name, affliction.NonClampedStrength)); + } + } + } + } + + return new XElement("Weapon", + new XAttribute("damagenames", FormatArray(damages.Select(tpl => tpl.Item1))), + new XAttribute("damageamounts", FormatArray(damages.Select(tpl => FormatFloat(tpl.Item2)))), + new XAttribute("isaoe", isAoE), + new XAttribute("structuredamage", structDamage ?? 0), + new XAttribute("stun", FormatFloat(stun)), + new XAttribute("skillrequirement", skillRequirement) + ); + } + + private static string GetAfflictionName(string identifier) + { + return AfflictionPrefab.Prefabs.Find(prefab => prefab.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase))?.Name ?? CultureInfo.CurrentCulture.TextInfo.ToTitleCase(identifier.ToLower()); + } + + private static string FormatFloat(float value) + { + return value.ToString("0.00", CultureInfo.InvariantCulture); + } + + private static string FormatArray(IEnumerable array) + { + return string.Join(separator, array); + } + + private static bool IsRequiredSkill(XElement element, out Skill? skill) + { + string name = element.Name.ToString(); + bool isSkill = name.Equals("RequiredSkill", StringComparison.OrdinalIgnoreCase) || + name.Equals("RequiredSkills", StringComparison.OrdinalIgnoreCase); + + if (isSkill) + { + string identifier = element.GetAttributeString(nameof(Skill.Identifier).ToLowerInvariant(), string.Empty); + float level = element.GetAttributeFloat(nameof(Skill.Level).ToLowerInvariant(), 0f); + skill = new Skill(identifier, level); + } + else + { + skill = null; + } + + return isSkill; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs new file mode 100644 index 000000000..ed5a63cac --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs @@ -0,0 +1,324 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Barotrauma +{ + public class SpriteRecorder : ISpriteBatch, IDisposable + { + private struct Command + { + public readonly Texture2D Texture; + public readonly VertexPositionColorTexture VertexBL; + public readonly VertexPositionColorTexture VertexBR; + public readonly VertexPositionColorTexture VertexTL; + public readonly VertexPositionColorTexture VertexTR; + public readonly float Depth; + public readonly Vector2 Min; + public readonly Vector2 Max; + public readonly int Index; + + public bool Overlaps(Command other) + { + return + Min.X <= other.Max.X && Max.X >= other.Min.X && + Min.Y <= other.Max.Y && Max.Y >= other.Min.Y; + } + + public Command( + Texture2D texture, + Vector2 pos, + Rectangle srcRect, + Color color, + float rotation, + Vector2 origin, + Vector2 scale, + SpriteEffects effects, + float depth, + int index) + { + int srcRectLeft = srcRect.Left; + int srcRectRight = srcRect.Right; + int srcRectTop = srcRect.Top; + int srcRectBottom = srcRect.Bottom; + if (effects.HasFlag(SpriteEffects.FlipHorizontally)) + { + var temp = srcRectRight; + srcRectRight = srcRectLeft; + srcRectLeft = temp; + } + if (effects.HasFlag(SpriteEffects.FlipVertically)) + { + var temp = srcRectBottom; + srcRectBottom = srcRectTop; + srcRectTop = temp; + } + + rotation = MathHelper.ToRadians(rotation); + float sin = (float)Math.Sin(rotation); + float cos = (float)Math.Cos(rotation); + + var size = srcRect.Size.ToVector2() * scale; + + Vector2 wAdd = new Vector2(size.X * cos, size.X * sin); + Vector2 hAdd = new Vector2(-size.Y * sin, size.Y * cos); + pos.X -= origin.X * scale.X * cos - origin.Y * scale.Y * sin; + pos.Y -= origin.Y * scale.Y * cos + origin.X * scale.X * sin; + + Texture = texture; + + Depth = depth; + + VertexTL.Color = color; + VertexTR.Color = color; + VertexBL.Color = color; + VertexBR.Color = color; + + VertexTL.Position = new Vector3(pos.X, pos.Y, 0f); + VertexTR.Position = new Vector3(pos.X + wAdd.X, pos.Y + wAdd.Y, 0f); + VertexBL.Position = new Vector3(pos.X + hAdd.X, pos.Y + hAdd.Y, 0f); + VertexBR.Position = new Vector3(pos.X + wAdd.X + hAdd.X, pos.Y + wAdd.Y + hAdd.Y, 0f); + + Min = new Vector2( + MathUtils.Min + ( + VertexTL.Position.X, + VertexTR.Position.X, + VertexBL.Position.X, + VertexBR.Position.X + ), + MathUtils.Min + ( + VertexTL.Position.Y, + VertexTR.Position.Y, + VertexBL.Position.Y, + VertexBR.Position.Y + )); + + Max = new Vector2( + MathUtils.Max + ( + VertexTL.Position.X, + VertexTR.Position.X, + VertexBL.Position.X, + VertexBR.Position.X + ), + MathUtils.Max + ( + VertexTL.Position.Y, + VertexTR.Position.Y, + VertexBL.Position.Y, + VertexBR.Position.Y + )); + + VertexTL.TextureCoordinate = new Vector2((float)srcRectLeft / (float)texture.Width, (float)srcRectTop / (float)texture.Height); + VertexTR.TextureCoordinate = new Vector2((float)srcRectRight / (float)texture.Width, (float)srcRectTop / (float)texture.Height); + VertexBL.TextureCoordinate = new Vector2((float)srcRectLeft / (float)texture.Width, (float)srcRectBottom / (float)texture.Height); + VertexBR.TextureCoordinate = new Vector2((float)srcRectRight / (float)texture.Width, (float)srcRectBottom / (float)texture.Height); + + Index = index; + + } + } + + private struct RecordedBuffer + { + public readonly Texture2D Texture; + public readonly VertexBuffer VertexBuffer; + public readonly int PolyCount; + + public RecordedBuffer(List commandList, int startIndex, int count) + { + Texture = commandList[startIndex].Texture; + + VertexBuffer = new VertexBuffer(GameMain.Instance.GraphicsDevice, VertexPositionColorTexture.VertexDeclaration, count * 4, BufferUsage.WriteOnly); + VertexPositionColorTexture[] vertices = new VertexPositionColorTexture[count * 4]; + for (int i = 0; i < count; i++) + { + vertices[(i * 4) + 0] = commandList[startIndex + i].VertexBL; + vertices[(i * 4) + 1] = commandList[startIndex + i].VertexBR; + vertices[(i * 4) + 2] = commandList[startIndex + i].VertexTL; + vertices[(i * 4) + 3] = commandList[startIndex + i].VertexTR; + } + VertexBuffer.SetData(vertices); + + PolyCount = count * 2; + } + } + + public static BasicEffect BasicEffect = null; + + private List recordedBuffers = new List(); + private List commandList = new List(); + private SpriteSortMode currentSortMode; + + private IndexBuffer indexBuffer = null; + private int maxSpriteCount = 0; + + public volatile bool ReadyToRender = false; + private volatile bool isDisposed = false; + + public Vector2 Min { get; private set; } + public Vector2 Max { get; private set; } + + public void Begin(SpriteSortMode sortMode) + { + ReadyToRender = false; + currentSortMode = sortMode; + } + + public void Draw(Texture2D texture, Vector2 pos, Rectangle? srcRect, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float depth) + { + if (isDisposed) { return; } + + Command command = new Command(texture, pos, srcRect ?? texture.Bounds, color, rotation, origin, scale, effects, depth, commandList?.Count ?? 0); + if (commandList.Count == 0) { Min = command.Min; Max = command.Max; } + Min = new Vector2(Math.Min(command.Min.X, Min.X), Math.Min(command.Min.Y, Min.Y)); + Max = new Vector2(Math.Max(command.Max.X, Max.X), Math.Max(command.Max.Y, Max.Y)); + + commandList?.Add(command); + } + + public void End() + { + if (isDisposed) { return; } + //sort commands according to the sorting + //mode given in the last Begin call + switch (currentSortMode) + { + case SpriteSortMode.FrontToBack: + commandList.Sort((c1, c2) => + { + return c1.Depth < c2.Depth ? -1 + : c1.Depth > c2.Depth ? 1 + : c1.Index < c2.Index ? 1 + : c1.Index > c2.Index ? -1 + : 0; + }); + break; + case SpriteSortMode.BackToFront: + commandList.Sort((c1, c2) => + { + return c1.Depth < c2.Depth ? 1 + : c1.Depth > c2.Depth ? -1 + : c1.Index < c2.Index ? 1 + : c1.Index > c2.Index ? -1 + : 0; + }); + break; + } + + //try to place commands of the same texture + //contiguously for optimal buffer generation + //while maintaining the same visual result + for (int i=1;i= 0; j--) + { + if (commandList[j].Texture == commandList[i].Texture) + { + //no commands between i and j overlap with + //i, therefore we can safely sift i down to + //make a contiguous block + commandList.SiftElement(i, j + 1); + break; + } + else if (commandList[j].Overlaps(commandList[i])) + { + //an overlapping command was found, therefore + //attempting to sift this one down would change + //the visual result + break; + } + } + } + } + + if (isDisposed) { return; } + //each contiguous block of commands of the same texture + //requires a vertex buffer to be rendered + CrossThread.RequestExecutionOnMainThread(() => + { + if (commandList.Count == 0) { return; } + int startIndex = 0; + for (int i = 1; i < commandList.Count; i++) + { + if (commandList[i].Texture != commandList[startIndex].Texture) + { + maxSpriteCount = Math.Max(maxSpriteCount, i - startIndex); + recordedBuffers.Add(new RecordedBuffer(commandList, startIndex, i - startIndex)); + startIndex = i; + } + } + recordedBuffers.Add(new RecordedBuffer(commandList, startIndex, commandList.Count - startIndex)); + maxSpriteCount = Math.Max(maxSpriteCount, commandList.Count - startIndex); + }); + + commandList.Clear(); + + ReadyToRender = true; + } + + public void Render(Camera cam) + { + if (!ReadyToRender) { return; } + var gfxDevice = GameMain.Instance.GraphicsDevice; + + BasicEffect ??= new BasicEffect(gfxDevice); + BasicEffect.Projection = Matrix.CreateOrthographicOffCenter(new Rectangle(0, 0, cam.Resolution.X, cam.Resolution.Y), -1f, 1f); + BasicEffect.View = cam.Transform; + BasicEffect.World = Matrix.Identity; + BasicEffect.TextureEnabled = true; + BasicEffect.VertexColorEnabled = true; + BasicEffect.Alpha = 1f; + + int requiredIndexCount = maxSpriteCount * 6; + if (requiredIndexCount > 0 && (indexBuffer == null || indexBuffer.IndexCount < requiredIndexCount)) + { + indexBuffer?.Dispose(); + indexBuffer = new IndexBuffer(gfxDevice, IndexElementSize.SixteenBits, requiredIndexCount * 2, BufferUsage.WriteOnly); + ushort[] indices = new ushort[requiredIndexCount * 2]; + for (int i=0;i - /// Convert a HSV value into a RGB value. - /// - /// Value between 0 and 360 - /// Value between 0 and 1 - /// Value between 0 and 1 - /// Reference - /// - public static Color HSVToRGB(float hue, float saturation, float value) - { - float c = value * saturation; - - float h = Math.Clamp(hue, 0, 360) / 60f; - - float x = c * (1 - Math.Abs(h % 2 - 1)); - - float r = 0, - g = 0, - b = 0; - - if (0 <= h && h <= 1) { r = c; g = x; b = 0; } - else if (1 < h && h <= 2) { r = x; g = c; b = 0; } - else if (2 < h && h <= 3) { r = 0; g = c; b = x; } - else if (3 < h && h <= 4) { r = 0; g = x; b = c; } - else if (4 < h && h <= 5) { r = x; g = 0; b = c; } - else if (5 < h && h <= 6) { r = c; g = 0; b = x; } - - float m = value - c; - - return new Color(r + m, g + m, b + m); - } - /// /// Convert a RGB value into a HSV value. /// diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index bd6036610..705b948b6 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.12.0.2 + 0.13.0.11 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 5af83bd41..53342a335 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.12.0.2 + 0.13.0.11 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 0b6a5ab84..619c4ffc8 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.12.0.2 + 0.13.0.11 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/libfreetype6.so b/Barotrauma/BarotraumaClient/libfreetype6.so index 797dadf66..b6380ba49 100644 Binary files a/Barotrauma/BarotraumaClient/libfreetype6.so and b/Barotrauma/BarotraumaClient/libfreetype6.so differ diff --git a/Barotrauma/BarotraumaClient/webm_mem_playback_x64.so b/Barotrauma/BarotraumaClient/webm_mem_playback_x64.so index 5a5e0c642..37a47f811 100644 Binary files a/Barotrauma/BarotraumaClient/webm_mem_playback_x64.so and b/Barotrauma/BarotraumaClient/webm_mem_playback_x64.so differ diff --git a/Barotrauma/BarotraumaServer/DedicatedServer.exe b/Barotrauma/BarotraumaServer/DedicatedServer.exe index 8808709d4..13b1d7b32 100644 --- a/Barotrauma/BarotraumaServer/DedicatedServer.exe +++ b/Barotrauma/BarotraumaServer/DedicatedServer.exe @@ -1 +1,2 @@ +export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$PWD/linux64" ./DedicatedServer diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index f82daf131..50c12c344 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.12.0.2 + 0.13.0.11 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 3432ec711..b02374966 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.12.0.2 + 0.13.0.11 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs index 0aa71f569..f4d7baa52 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs @@ -26,6 +26,7 @@ namespace Barotrauma { msg.Write(ID); msg.Write(Name); + msg.Write(OriginalName); msg.Write((byte)Gender); msg.Write((byte)Race); msg.Write((byte)HeadSpriteId); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index 065e27556..1e90df4f0 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -105,6 +105,14 @@ namespace Barotrauma focusedItem = item; FocusedCharacter = null; } + else + { + //failed to interact with the item + // -> correct the position and the state of the Holdable component (in case the item was deattached client-side) + item.PositionUpdateInterval = 0.0f; + var holdable = item.GetComponent(); + holdable?.Item?.CreateServerEvent(holdable); + } } else if (closestEntity is Character character) { @@ -278,21 +286,21 @@ namespace Barotrauma switch ((NetEntityEvent.Type)extraData[0]) { case NetEntityEvent.Type.InventoryState: - msg.WriteRangedInteger(0, 0, 5); + msg.WriteRangedInteger(0, 0, 6); msg.Write(GameMain.Server.EntityEventManager.Events.Last()?.ID ?? (ushort)0); Inventory.ServerWrite(msg, c); break; case NetEntityEvent.Type.Control: - msg.WriteRangedInteger(1, 0, 5); + msg.WriteRangedInteger(1, 0, 6); Client owner = (Client)extraData[1]; msg.Write(owner != null && owner.Character == this && GameMain.Server.ConnectedClients.Contains(owner) ? owner.ID : (byte)0); break; case NetEntityEvent.Type.Status: - msg.WriteRangedInteger(2, 0, 5); + msg.WriteRangedInteger(2, 0, 6); WriteStatus(msg); break; case NetEntityEvent.Type.UpdateSkills: - msg.WriteRangedInteger(3, 0, 5); + msg.WriteRangedInteger(3, 0, 6); if (Info?.Job == null) { msg.Write((byte)0); @@ -311,15 +319,36 @@ namespace Barotrauma Limb attackLimb = extraData[1] as Limb; UInt16 targetEntityID = (UInt16)extraData[2]; int targetLimbIndex = extraData.Length > 3 ? (int)extraData[3] : 0; - msg.WriteRangedInteger(4, 0, 5); + msg.WriteRangedInteger(4, 0, 6); msg.Write((byte)(Removed ? 255 : Array.IndexOf(AnimController.Limbs, attackLimb))); msg.Write(targetEntityID); msg.Write((byte)targetLimbIndex); break; case NetEntityEvent.Type.AssignCampaignInteraction: - msg.WriteRangedInteger(5, 0, 5); + msg.WriteRangedInteger(5, 0, 6); msg.Write((byte)CampaignInteractionType); break; + case NetEntityEvent.Type.ObjectiveManagerOrderState: + msg.WriteRangedInteger(6, 0, 6); + if (!(AIController is HumanAIController controller)) + { + msg.Write(false); + break; + } + var currentOrderInfo = controller.ObjectiveManager.GetCurrentOrderInfo(); + if (!currentOrderInfo.HasValue) + { + msg.Write(false); + break; + } + msg.Write(true); + var orderPrefab = currentOrderInfo.Value.Order.Prefab; + int orderIndex = Order.PrefabList.IndexOf(orderPrefab); + msg.WriteRangedInteger(orderIndex, 0, Order.PrefabList.Count); + if (!orderPrefab.HasOptions) { break; } + int optionIndex = orderPrefab.Options.IndexOf(currentOrderInfo.Value.OrderOption); + msg.WriteRangedInteger(optionIndex, 0, orderPrefab.Options.Length); + break; default: DebugConsole.ThrowError("Invalid NetworkEvent type for entity " + ToString() + " (" + (NetEntityEvent.Type)extraData[0] + ")"); break; @@ -529,29 +558,28 @@ namespace Barotrauma msg.Write((byte)CampaignInteractionType); - // Current order - if (info.CurrentOrder != null) + + // Current orders + msg.Write((byte)info.CurrentOrders.Count(o => o.Order != null)); + foreach (var orderInfo in info.CurrentOrders) { - msg.Write(true); - msg.Write((byte)Order.PrefabList.IndexOf(info.CurrentOrder.Prefab)); - msg.Write(info.CurrentOrder.TargetEntity == null ? (UInt16)0 : info.CurrentOrder.TargetEntity.ID); - var hasOrderGiver = info.CurrentOrder.OrderGiver != null; + if (orderInfo.Order == null) { continue; } + msg.Write((byte)Order.PrefabList.IndexOf(orderInfo.Order.Prefab)); + msg.Write(orderInfo.Order.TargetEntity == null ? (UInt16)0 : orderInfo.Order.TargetEntity.ID); + var hasOrderGiver = orderInfo.Order.OrderGiver != null; msg.Write(hasOrderGiver); - if (hasOrderGiver) { msg.Write(info.CurrentOrder.OrderGiver.ID); } - msg.Write((byte)(string.IsNullOrWhiteSpace(info.CurrentOrderOption) ? 0 : Array.IndexOf(info.CurrentOrder.Prefab.Options, info.CurrentOrderOption))); - var hasTargetPosition = info.CurrentOrder.TargetPosition != null; + if (hasOrderGiver) { msg.Write(orderInfo.Order.OrderGiver.ID); } + msg.Write((byte)(string.IsNullOrWhiteSpace(orderInfo.OrderOption) ? 0 : Array.IndexOf(orderInfo.Order.Prefab.Options, orderInfo.OrderOption))); + msg.Write((byte)orderInfo.ManualPriority); + var hasTargetPosition = orderInfo.Order.TargetPosition != null; msg.Write(hasTargetPosition); if (hasTargetPosition) { - msg.Write(info.CurrentOrder.TargetPosition.Position.X); - msg.Write(info.CurrentOrder.TargetPosition.Position.Y); - msg.Write(info.CurrentOrder.TargetPosition.Hull == null ? (UInt16)0 : info.CurrentOrder.TargetPosition.Hull.ID); + msg.Write(orderInfo.Order.TargetPosition.Position.X); + msg.Write(orderInfo.Order.TargetPosition.Position.Y); + msg.Write(orderInfo.Order.TargetPosition.Hull == null ? (UInt16)0 : orderInfo.Order.TargetPosition.Hull.ID); } } - else - { - msg.Write(false); - } TryWriteStatus(msg); diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index 0944ad43f..563e0195d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -73,17 +73,27 @@ namespace Barotrauma Stopwatch sw = new Stopwatch(); sw.Start(); - int consoleWidth = Console.WindowWidth; - if (consoleWidth < 5) consoleWidth = 5; - int consoleHeight = Console.WindowHeight; - if (consoleHeight < 5) consoleHeight = 5; + int consoleWidth = 0; + int consoleHeight = 0; + + if(!Console.IsOutputRedirected) + { + consoleWidth = Console.WindowWidth; + if (consoleWidth < 5) consoleWidth = 5; + consoleHeight = Console.WindowHeight; + if (consoleHeight < 5) consoleHeight = 5; + } //dequeue messages lock (queuedMessages) { if (queuedMessages.Count > 0) { - Console.CursorLeft = 0; + + if (!Console.IsOutputRedirected) + { + Console.CursorLeft = 0; + } while (queuedMessages.Count > 0) { ColoredText msg = queuedMessages.Dequeue(); @@ -102,15 +112,21 @@ namespace Barotrauma if (msg.IsCommand) commandMemory.Add(msgTxt); - int paddingLen = consoleWidth - (msg.Text.Length % consoleWidth)-1; - msgTxt += new string(' ', paddingLen>0 ? paddingLen : 0); + if(!Console.IsOutputRedirected) + { + int paddingLen = consoleWidth - (msg.Text.Length % consoleWidth) - 1; + msgTxt += new string(' ', paddingLen > 0 ? paddingLen : 0); - Console.ForegroundColor = XnaToConsoleColor.Convert(msg.Color); + Console.ForegroundColor = XnaToConsoleColor.Convert(msg.Color); + } Console.WriteLine(msgTxt); if (sw.ElapsedMilliseconds >= maxTime) { break; } } - RewriteInputToCommandLine(input); + if(!Console.IsOutputRedirected) + { + RewriteInputToCommandLine(input); + } } if (Messages.Count > MaxMessages) { @@ -118,73 +134,78 @@ namespace Barotrauma } } - //read player input - bool rewriteInput = false; - while (Console.KeyAvailable) + // No good way to display input when console output is redirected, and can't read from redirected input using KeyAvailable. + if(!Console.IsOutputRedirected && !Console.IsInputRedirected) { - if (sw.ElapsedMilliseconds >= maxTime) + //read player input + bool rewriteInput = false; + while (Console.KeyAvailable) { - rewriteInput = false; - break; - } - rewriteInput = true; - ConsoleKeyInfo key = Console.ReadKey(true); - switch (key.Key) - { - case ConsoleKey.Enter: - lock (QueuedCommands) - { - QueuedCommands.Add(input); - } - input = ""; - memoryIndex = -1; + if (sw.ElapsedMilliseconds >= maxTime) + { + rewriteInput = false; break; - case ConsoleKey.Backspace: - if (input.Length > 0) input = input.Substring(0, input.Length - 1); - memoryIndex = -1; - break; - case ConsoleKey.LeftArrow: - input = AutoComplete(input, -1); - break; - case ConsoleKey.RightArrow: - input = AutoComplete(input, 1); - break; - case ConsoleKey.UpArrow: - memoryIndex--; - if (memoryIndex < 0) memoryIndex = commandMemory.Count - 1; - if (memoryIndex >= commandMemory.Count) memoryIndex = commandMemory.Count - 1; - if (memoryIndex >= 0) - { - input = commandMemory[memoryIndex]; - } - break; - case ConsoleKey.DownArrow: - memoryIndex++; - if (memoryIndex < 0) memoryIndex = 0; - if (memoryIndex >= commandMemory.Count) memoryIndex = 0; - if (commandMemory.Count>0) - { - input = commandMemory[memoryIndex]; - } - break; - case ConsoleKey.Tab: - if (input.Length > 0) - { - input = AutoComplete(input, 0); + } + rewriteInput = true; + ConsoleKeyInfo key = Console.ReadKey(true); + switch (key.Key) + { + case ConsoleKey.Enter: + lock (QueuedCommands) + { + QueuedCommands.Add(input); + } + input = ""; memoryIndex = -1; - } - break; - default: - if (key.KeyChar != 0) - { - input += key.KeyChar; + break; + case ConsoleKey.Backspace: + if (input.Length > 0) input = input.Substring(0, input.Length - 1); + ResetAutoComplete(); memoryIndex = -1; - } - ResetAutoComplete(); - break; + break; + case ConsoleKey.LeftArrow: + input = AutoComplete(input, -1); + break; + case ConsoleKey.RightArrow: + input = AutoComplete(input, 1); + break; + case ConsoleKey.UpArrow: + memoryIndex--; + if (memoryIndex < 0) memoryIndex = commandMemory.Count - 1; + if (memoryIndex >= commandMemory.Count) memoryIndex = commandMemory.Count - 1; + if (memoryIndex >= 0) + { + input = commandMemory[memoryIndex]; + } + break; + case ConsoleKey.DownArrow: + memoryIndex++; + if (memoryIndex < 0) memoryIndex = 0; + if (memoryIndex >= commandMemory.Count) memoryIndex = 0; + if (commandMemory.Count>0) + { + input = commandMemory[memoryIndex]; + } + break; + case ConsoleKey.Tab: + if (input.Length > 0) + { + input = AutoComplete(input, 0); + memoryIndex = -1; + } + break; + default: + if (key.KeyChar != 0) + { + input += key.KeyChar; + memoryIndex = -1; + } + ResetAutoComplete(); + break; + } } + if (rewriteInput) { RewriteInputToCommandLine(input); } } - if (rewriteInput) { RewriteInputToCommandLine(input); } sw.Stop(); } @@ -512,6 +533,13 @@ namespace Barotrauma NewMessage(perm + " is not a valid permission!", Color.Red); return; } + + if (permission == ClientPermissions.None) + { + NewMessage($"No permissions were given to {client.Name}. Did you mean \"revokeperm {client.Name} All\"?"); + return; + } + client.GivePermission(permission); GameMain.Server.UpdateClientPermissions(client); NewMessage("Granted " + perm + " permissions to " + client.Name + ".", Color.White); @@ -539,10 +567,13 @@ namespace Barotrauma return; } - NewMessage("Valid permissions are:", Color.White); - foreach (ClientPermissions permission in Enum.GetValues(typeof(ClientPermissions))) + if (args.Length < 2) { - NewMessage(" - " + permission.ToString(), Color.White); + NewMessage("Valid permissions are:", Color.White); + foreach (ClientPermissions permission in Enum.GetValues(typeof(ClientPermissions))) + { + NewMessage(" - " + permission.ToString(), Color.White); + } } ShowQuestionPrompt("Permission to revoke from \"" + client.Name + "\"?", (perm) => { @@ -715,7 +746,6 @@ namespace Barotrauma { NewMessage("Revoked \"" + client.Name + "\"'s permission to use the console commands " + string.Join(", ", revokedCommands.Select(c => c.names[0])) + ".", Color.White); } - }, args, 1); }); @@ -1801,6 +1831,14 @@ namespace Barotrauma GameMain.Server.SendConsoleMessage(perm + " is not a valid permission!", senderClient); return; } + + if (permission == ClientPermissions.None) + { + GameMain.Server.SendConsoleMessage($"No permissions were given to {client.Name}. Did you mean \"revokeperm {client.Name} All\"?", senderClient); + NewMessage($"No permissions were given to {client.Name}. Did you mean \"revokeperm {client.Name} All\"?"); + return; + } + client.GivePermission(permission); GameMain.Server.UpdateClientPermissions(client); GameMain.Server.SendConsoleMessage("Granted " + perm + " permissions to " + client.Name + ".", senderClient); @@ -1954,7 +1992,6 @@ namespace Barotrauma if (revokeAll) { revokedCommands.AddRange(commands); - client.RemovePermission(ClientPermissions.ConsoleCommands); } else { @@ -1971,10 +2008,13 @@ namespace Barotrauma revokedCommands.Add(matchingCommand); } } - client.GivePermission(ClientPermissions.ConsoleCommands); } client.SetPermissions(client.Permissions, client.PermittedConsoleCommands.Except(revokedCommands).ToList()); + if (client.PermittedConsoleCommands.Count == 0) + { + client.RemovePermission(ClientPermissions.ConsoleCommands); + } GameMain.Server.UpdateClientPermissions(client); GameMain.Server.SendConsoleMessage("Revoked \"" + client.Name + "\"'s permission to use the console commands " + string.Join(", ", revokedCommands.Select(c => c.names[0])) + ".", senderClient); if (revokeAll) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AbandonedOutpostMission.cs new file mode 100644 index 000000000..89446aa82 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AbandonedOutpostMission.cs @@ -0,0 +1,30 @@ +using Barotrauma.Networking; +using System; +using System.Linq; + +namespace Barotrauma +{ + partial class AbandonedOutpostMission : Mission + { + public override void ServerWriteInitial(IWriteMessage msg, Client c) + { + if (characters.Count == 0) + { + throw new InvalidOperationException("Server attempted to write AbandonedOutpostMission data when no characters had been spawned."); + } + + msg.Write((byte)characters.Count); + foreach (Character character in characters) + { + character.WriteSpawnData(msg, character.ID, restrictMessageSize: false); + msg.Write(requireKill.Contains(character)); + msg.Write(requireRescue.Contains(character)); + msg.Write((ushort)characterItems[character].Count()); + foreach (Item item in characterItems[character]) + { + item.WriteSpawnData(msg, item.ID, item.ParentInventory.Owner?.ID ?? Entity.NullEntityID, 0); + } + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/OutpostDestroyMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/OutpostDestroyMission.cs new file mode 100644 index 000000000..1c7c667af --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/OutpostDestroyMission.cs @@ -0,0 +1,19 @@ +using Barotrauma.Networking; +using System.Collections.Generic; + +namespace Barotrauma +{ + partial class OutpostDestroyMission : AbandonedOutpostMission + { + private readonly List spawnedItems = new List(); + public override void ServerWriteInitial(IWriteMessage msg, Client c) + { + base.ServerWriteInitial(msg, c); + msg.Write((ushort)spawnedItems.Count); + foreach (Item item in spawnedItems) + { + item.WriteSpawnData(msg, item.ID, Entity.NullEntityID, 0); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index 5cf61396e..6abe50855 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -361,7 +361,7 @@ namespace Barotrauma } #if !DEBUG - if (Server?.OwnerConnection == null && !Console.IsOutputRedirected) + if (Server?.OwnerConnection == null) { DebugConsole.UpdateCommandLine((int)(Timing.Accumulator * 800)); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs index fc8f7e167..9c3d14d77 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using System.Collections.Generic; namespace Barotrauma { @@ -13,10 +12,11 @@ namespace Barotrauma public override void ShowStartMessage() { - if (Mission == null) return; - - GameServer.Log(TextManager.Get("Mission") + ": " + Mission.Name, Networking.ServerLog.MessageType.ServerMessage); - GameServer.Log(Mission.Description, Networking.ServerLog.MessageType.ServerMessage); + foreach (Mission mission in Missions) + { + GameServer.Log(TextManager.Get("Mission") + ": " + mission.Name, ServerLog.MessageType.ServerMessage); + GameServer.Log(mission.Description, ServerLog.MessageType.ServerMessage); + } } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MissionMode.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MissionMode.cs index 4d36c08d4..51ad5e730 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MissionMode.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MissionMode.cs @@ -4,10 +4,11 @@ { public override void ShowStartMessage() { - if (mission == null) return; - - Networking.GameServer.Log(TextManager.Get("Mission") + ": " + mission.Name, Networking.ServerLog.MessageType.ServerMessage); - Networking.GameServer.Log(mission.Description, Networking.ServerLog.MessageType.ServerMessage); + foreach (Mission mission in missions) + { + Networking.GameServer.Log(TextManager.Get("Mission") + ": " + mission.Name, Networking.ServerLog.MessageType.ServerMessage); + Networking.GameServer.Log(mission.Description, Networking.ServerLog.MessageType.ServerMessage); + } } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 36e237a51..a4ee8f123 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -32,11 +32,11 @@ namespace Barotrauma get { return ForceMapUI || CoroutineManager.IsCoroutineRunning("LevelTransition"); } } - public static void StartNewCampaign(string savePath, string subPath, string seed) + public static void StartNewCampaign(string savePath, string subPath, string seed, CampaignSettings settings) { if (string.IsNullOrWhiteSpace(savePath)) { return; } - GameMain.GameSession = new GameSession(new SubmarineInfo(subPath), savePath, GameModePreset.MultiPlayerCampaign, seed); + GameMain.GameSession = new GameSession(new SubmarineInfo(subPath), savePath, GameModePreset.MultiPlayerCampaign, settings, seed); GameMain.NetLobbyScreen.ToggleCampaignMode(true); SaveUtil.SaveGame(GameMain.GameSession.SavePath); @@ -74,7 +74,7 @@ namespace Barotrauma DebugConsole.ShowQuestionPrompt("Enter a save name for the campaign:", (string saveName) => { string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer, saveName); - StartNewCampaign(savePath, GameMain.NetLobbyScreen.SelectedSub.FilePath, GameMain.NetLobbyScreen.LevelSeed); + StartNewCampaign(savePath, GameMain.NetLobbyScreen.SelectedSub.FilePath, GameMain.NetLobbyScreen.LevelSeed, CampaignSettings.Empty); }); } else @@ -367,6 +367,8 @@ namespace Barotrauma { if (CoroutineManager.IsCoroutineRunning("LevelTransition")) { return; } + Map?.Radiation?.UpdateRadiation(deltaTime); + base.Update(deltaTime); if (Level.Loaded != null) { @@ -441,9 +443,16 @@ namespace Barotrauma foreach (Mission mission in map.CurrentLocation.AvailableMissions) { msg.Write(mission.Prefab.Identifier); - Location missionDestination = mission.Locations[0] == map.CurrentLocation ? mission.Locations[1] : mission.Locations[0]; - LocationConnection connection = map.CurrentLocation.Connections.Find(c => c.OtherLocation(map.CurrentLocation) == missionDestination); - msg.Write((byte)map.CurrentLocation.Connections.IndexOf(connection)); + if (mission.Locations[0] == mission.Locations[1]) + { + msg.Write((byte)255); + } + else + { + Location missionDestination = mission.Locations[0] == map.CurrentLocation ? mission.Locations[1] : mission.Locations[0]; + LocationConnection connection = map.CurrentLocation.Connections.Find(c => c.OtherLocation(map.CurrentLocation) == missionDestination); + msg.Write((byte)map.CurrentLocation.Connections.IndexOf(connection)); + } } // Store balance @@ -658,13 +667,24 @@ namespace Barotrauma } bool validateHires = msg.ReadBoolean(); - bool fireCharacter = msg.ReadBoolean(); + bool renameCharacter = msg.ReadBoolean(); + int renamedIdentifier = -1; + string newName = null; + bool existingCrewMember = false; + if (renameCharacter) + { + renamedIdentifier = msg.ReadInt32(); + newName = msg.ReadString(); + existingCrewMember = msg.ReadBoolean(); + } + + bool fireCharacter = msg.ReadBoolean(); int firedIdentifier = -1; if (fireCharacter) { firedIdentifier = msg.ReadInt32(); } Location location = map?.CurrentLocation; - + List hiredCharacters = new List(); CharacterInfo firedCharacter = null; if (location != null && AllowedToManageCampaign(sender)) @@ -682,13 +702,45 @@ namespace Barotrauma } } + if (renameCharacter) + { + CharacterInfo characterInfo = null; + if (existingCrewMember && CrewManager != null) + { + characterInfo = CrewManager.CharacterInfos.FirstOrDefault(info => info.GetIdentifierUsingOriginalName() == renamedIdentifier); + } + else if(!existingCrewMember && location.HireManager != null) + { + characterInfo = location.HireManager.AvailableCharacters.FirstOrDefault(info => info.GetIdentifierUsingOriginalName() == renamedIdentifier); + } + + if (characterInfo != null && (characterInfo.Character?.IsBot ?? true)) + { + if (existingCrewMember) + { + CrewManager.RenameCharacter(characterInfo, newName); + } + else + { + location.HireManager.RenameCharacter(characterInfo, newName); + } + } + else + { + DebugConsole.ThrowError($"Tried to rename an invalid character ({renamedIdentifier})"); + } + } + if (location.HireManager != null) { if (validateHires) { foreach (CharacterInfo hireInfo in location.HireManager.PendingHires) { - TryHireCharacter(location, hireInfo); + if (TryHireCharacter(location, hireInfo)) + { + hiredCharacters.Add(hireInfo); + }; } } @@ -697,10 +749,10 @@ namespace Barotrauma List pendingHireInfos = new List(); foreach (int identifier in pendingHires) { - CharacterInfo match = location.GetHireableCharacters().FirstOrDefault(info => info.GetIdentifier() == identifier); + CharacterInfo match = location.GetHireableCharacters().FirstOrDefault(info => info.GetIdentifierUsingOriginalName() == identifier); if (match == null) { - DebugConsole.ThrowError($"Tried to hire a character that doesn't exist ({identifier})"); + DebugConsole.ThrowError($"Tried to add a character that doesn't exist ({identifier}) to pending hires"); continue; } @@ -712,25 +764,39 @@ namespace Barotrauma } location.HireManager.PendingHires = pendingHireInfos; } + + location.HireManager.AvailableCharacters.ForEachMod(info => + { + if(!location.HireManager.PendingHires.Contains(info)) + { + location.HireManager.RenameCharacter(info, info.OriginalName); + } + }); } } // bounce back - SendCrewState(validateHires, firedCharacter); + if (renameCharacter && existingCrewMember) + { + SendCrewState(hiredCharacters, (renamedIdentifier, newName), firedCharacter); + } + else + { + SendCrewState(hiredCharacters, default, firedCharacter); + } } /// /// Notifies the clients of the current bot situation like syncing pending and available hires - /// available hires are also synced /// - /// When set to true notifies the clients that the hires have been validated. - /// When not null will inform the clients that his character has been fired. + /// Inform the clients that these characters have been hired. + /// Inform the clients that this character has been fired. /// /// It might be obsolete to sync available hires. I found that the available hires are always the same between /// the client and the server when there's only one person on the server but when a second person joins both of /// their available hires are different from the server. /// - public void SendCrewState(bool validateHires, CharacterInfo firedCharacter) + public void SendCrewState(List hiredCharacters, (int id, string newName) renamedCrewMember, CharacterInfo firedCharacter) { List availableHires = new List(); List pendingHires = new List(); @@ -756,10 +822,26 @@ namespace Barotrauma msg.Write((ushort)pendingHires.Count); foreach (CharacterInfo pendingHire in pendingHires) { - msg.Write(pendingHire.GetIdentifier()); + msg.Write(pendingHire.GetIdentifierUsingOriginalName()); + } + + msg.Write((ushort)(hiredCharacters?.Count ?? 0)); + if(hiredCharacters != null) + { + foreach (CharacterInfo info in hiredCharacters) + { + info.ServerWrite(msg); + msg.Write(info.Salary); + } + } + + bool validRenaming = renamedCrewMember.id > -1 && !string.IsNullOrEmpty(renamedCrewMember.newName); + msg.Write(validRenaming); + if (validRenaming) + { + msg.Write(renamedCrewMember.id); + msg.Write(renamedCrewMember.newName); } - - msg.Write(validateHires); msg.Write(firedCharacter != null); if (firedCharacter != null) { msg.Write(firedCharacter.GetIdentifier()); } @@ -777,6 +859,7 @@ namespace Barotrauma new XAttribute("purchasedhullrepairs", PurchasedHullRepairs), new XAttribute("purchaseditemrepairs", PurchasedItemRepairs), new XAttribute("cheatsenabled", CheatsEnabled)); + modeElement.Add(Settings.Save()); CampaignMetadata?.Save(modeElement); Map.Save(modeElement); CargoManager?.SavePurchasedItems(modeElement); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs index 152fe95e5..eb8aa8e58 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs @@ -5,15 +5,13 @@ namespace Barotrauma.Items.Components { partial class DockingPort : ItemComponent, IDrawableComponent, IServerSerializable { - private UInt16 originalDockingTargetID; - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { msg.Write(docked); if (docked) { - msg.Write(originalDockingTargetID); + msg.Write(DockingTarget.item.ID); msg.Write(IsLocked); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs index b0a85019a..a30115996 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs @@ -8,7 +8,10 @@ namespace Barotrauma.Items.Components public override void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { base.ServerWrite(msg, c, extraData); - if (!attachable || body == null) { return; } + + bool writeAttachData = attachable && body != null; + msg.Write(writeAttachData); + if (!writeAttachData) { return; } msg.Write(Attached); msg.Write(body.SimPosition.X); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs index c57ba42cf..7b29551ae 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs @@ -51,7 +51,7 @@ namespace Barotrauma.Items.Components if (dockingButtonClicked) { - item.SendSignal(0, "1", "toggle_docking", sender: null); + item.SendSignal("1", "toggle_docking"); GameMain.Server.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ComponentState, item.GetComponentIndex(this), true }); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs index e36e3e67f..1382bdc27 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs @@ -20,17 +20,20 @@ namespace Barotrauma.Items.Components ServerLog.MessageType.ItemInteraction); OutputValue = newOutputValue; ShowOnDisplay(newOutputValue); - item.SendSignal(0, newOutputValue, "signal_out", null); + item.SendSignal(newOutputValue, "signal_out"); item.CreateServerEvent(this); } } - partial void ShowOnDisplay(string input) + partial void ShowOnDisplay(string input, bool addToHistory = true) { - messageHistory.Add(input); - while (messageHistory.Count > MaxMessages) + if (addToHistory) { - messageHistory.RemoveAt(0); + messageHistory.Add(input); + while (messageHistory.Count > MaxMessages) + { + messageHistory.RemoveAt(0); + } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index be554ba94..609cc2e0b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -269,6 +269,7 @@ namespace Barotrauma msg.Write(body == null ? (byte)0 : (byte)body.BodyType); msg.Write(SpawnedInOutpost); + msg.Write(AllowStealing); byte teamID = 0; foreach (WifiComponent wifiComponent in GetComponents()) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs index 83b790157..4a95adecd 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs @@ -121,13 +121,14 @@ namespace Barotrauma.Networking } } - public bool IsBanned(IPAddress IP, ulong steamID, out string reason) + public bool IsBanned(IPAddress IP, ulong steamID, ulong ownerSteamID, out string reason) { reason = string.Empty; if (IPAddress.IsLoopback(IP)) { return false; } var bannedPlayer = bannedPlayers.Find(bp => bp.CompareTo(IP) || - (steamID > 0 && (bp.SteamID == steamID || SteamManager.SteamIDStringToUInt64(bp.EndPoint) == steamID))); + (steamID > 0 && (bp.SteamID == steamID || SteamManager.SteamIDStringToUInt64(bp.EndPoint) == steamID)) || + (ownerSteamID > 0 && (bp.SteamID == ownerSteamID || SteamManager.SteamIDStringToUInt64(bp.EndPoint) == ownerSteamID))); reason = bannedPlayer?.Reason; return bannedPlayer != null; } @@ -166,6 +167,7 @@ namespace Barotrauma.Networking public void BanPlayer(string name, ulong steamID, string reason, TimeSpan? duration) { + if (steamID == 0) { return; } BanPlayer(name, "", steamID, reason, duration); } @@ -320,7 +322,17 @@ namespace Barotrauma.Networking outMsg.Write(bannedPlayer.Name); outMsg.Write(bannedPlayer.UniqueIdentifier); - outMsg.Write(bannedPlayer.IsRangeBan); outMsg.WritePadBits(); + outMsg.Write(bannedPlayer.IsRangeBan); + outMsg.Write(bannedPlayer.ExpirationTime != null); + outMsg.WritePadBits(); + if (bannedPlayer.ExpirationTime != null) + { + double hoursFromNow = (bannedPlayer.ExpirationTime.Value - DateTime.Now).TotalHours; + outMsg.Write(hoursFromNow); + } + + outMsg.Write(bannedPlayer.Reason ?? ""); + if (c.Connection == GameMain.Server.OwnerConnection) { outMsg.Write(bannedPlayer.EndPoint); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs index 55358eae5..dfcda4b3c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs @@ -25,7 +25,54 @@ namespace Barotrauma.Networking int orderIndex = msg.ReadByte(); orderTargetCharacter = Entity.FindEntityByID(msg.ReadUInt16()) as Character; orderTargetEntity = Entity.FindEntityByID(msg.ReadUInt16()) as Entity; - int orderOptionIndex = msg.ReadByte(); + + Order orderPrefab = null; + int? orderOptionIndex = null; + string orderOption = null; + + // The option of a Dismiss order is written differently so we know what order we target + // now that the game supports multiple current orders simultaneously + if (orderIndex >= 0 && orderIndex < Order.PrefabList.Count) + { + orderPrefab = Order.PrefabList[orderIndex]; + if (orderPrefab.Identifier != "dismissed") + { + orderOptionIndex = msg.ReadByte(); + } + // Does the dismiss order have a specified target? + else if(msg.ReadBoolean()) + { + int identifierCount = msg.ReadByte(); + if (identifierCount > 0) + { + int dismissedOrderIndex = msg.ReadByte(); + Order dismissedOrderPrefab = null; + if (dismissedOrderIndex >= 0 && dismissedOrderIndex < Order.PrefabList.Count) + { + dismissedOrderPrefab = Order.PrefabList[dismissedOrderIndex]; + orderOption = dismissedOrderPrefab.Identifier; + } + if (identifierCount > 1) + { + int dismissedOrderOptionIndex = msg.ReadByte(); + if (dismissedOrderPrefab != null) + { + var options = dismissedOrderPrefab.Options; + if (options != null && dismissedOrderOptionIndex >= 0 && dismissedOrderOptionIndex < options.Length) + { + orderOption += $".{options[dismissedOrderOptionIndex]}"; + } + } + } + } + } + } + else + { + orderOptionIndex = msg.ReadByte(); + } + + int orderPriority = msg.ReadByte(); orderTargetType = (Order.OrderTargetType)msg.ReadByte(); if (msg.ReadBoolean()) { @@ -41,14 +88,14 @@ namespace Barotrauma.Networking if (orderIndex < 0 || orderIndex >= Order.PrefabList.Count) { - DebugConsole.ThrowError($"Invalid order message from client \"{c.Name}\" - order index out of bounds ({orderIndex}, {orderOptionIndex})."); + DebugConsole.ThrowError($"Invalid order message from client \"{c.Name}\" - order index out of bounds ({orderIndex})."); if (NetIdUtils.IdMoreRecent(ID, c.LastSentChatMsgID)) { c.LastSentChatMsgID = ID; } return; } - Order orderPrefab = Order.PrefabList[orderIndex]; - string orderOption = orderOptionIndex < 0 || orderOptionIndex >= orderPrefab.Options.Length ? "" : orderPrefab.Options[orderOptionIndex]; - orderMsg = new OrderChatMessage(orderPrefab, orderOption, orderTargetPosition ?? orderTargetEntity as ISpatialEntity, orderTargetCharacter, c.Character) + orderPrefab ??= Order.PrefabList[orderIndex]; + orderOption ??= orderOptionIndex == null || orderOptionIndex < 0 || orderOptionIndex >= orderPrefab.Options.Length ? "" : orderPrefab.Options[orderOptionIndex.Value]; + orderMsg = new OrderChatMessage(orderPrefab, orderOption, orderPriority, orderTargetPosition ?? orderTargetEntity as ISpatialEntity, orderTargetCharacter, c.Character) { WallSectionIndex = wallSectionIndex }; @@ -147,7 +194,7 @@ namespace Barotrauma.Networking } if (order != null) { - orderTargetCharacter.SetOrder(order, orderMsg.OrderOption, orderMsg.Sender); + orderTargetCharacter.SetOrder(order, orderMsg.OrderOption, orderMsg.OrderPriority, orderMsg.Sender); } } else if (orderMsg.Order.IsIgnoreOrder) @@ -183,12 +230,16 @@ namespace Barotrauma.Networking 2 + //(UInt16)NetStateID 1 + //(byte)Type Encoding.UTF8.GetBytes(Text).Length + 2; - + + if (SenderClient != null) + { + length += 8; //SteamID or local ID (ulong) + } if (Sender != null && c.InGame) { length += 2; //sender ID (UInt16) } - else if (SenderName != null) + if (SenderName != null) { length += Encoding.UTF8.GetBytes(SenderName).Length + 2; } @@ -205,11 +256,17 @@ namespace Barotrauma.Networking msg.Write(Text); msg.Write(SenderName); + msg.Write(SenderClient != null); + if (SenderClient != null) + { + msg.Write((SenderClient.SteamID != 0) ? SenderClient.SteamID : SenderClient.ID); + } msg.Write(Sender != null && c.InGame); if (Sender != null && c.InGame) { msg.Write(Sender.ID); } + msg.WritePadBits(); if (Type == ChatMessageType.ServerMessageBoxInGame) { msg.Write(IconStyle); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs index 9beb811a0..8c435e979 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs @@ -73,6 +73,7 @@ namespace Barotrauma.Networking public NetworkConnection Connection { get; set; } public bool SpectateOnly; + public bool? WaitForNextRoundRespawn; public int KarmaKickCount; @@ -163,7 +164,7 @@ namespace Barotrauma.Networking public void RemovePermission(ClientPermissions permission) { - if (this.Permissions.HasFlag(permission)) this.Permissions &= ~permission; + this.Permissions &= ~permission; } public bool HasPermission(ClientPermissions permission) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 63dfb2859..2f4426c74 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -284,6 +284,7 @@ namespace Barotrauma.Networking newClient.Connection = connection; newClient.Connection.Status = NetworkConnectionStatus.Connected; newClient.SteamID = connection.SteamID; + newClient.OwnerSteamID = connection.OwnerSteamID; newClient.Language = connection.Language; ConnectedClients.Add(newClient); @@ -301,19 +302,20 @@ namespace Barotrauma.Networking LastClientListUpdateID++; - if (newClient.Connection == OwnerConnection) + if (newClient.Connection == OwnerConnection && OwnerConnection != null) { newClient.GivePermission(ClientPermissions.All); newClient.PermittedConsoleCommands.AddRange(DebugConsole.Commands); SendConsoleMessage("Granted all permissions to " + newClient.Name + ".", newClient); } - SendChatMessage($"ServerMessage.JoinedServer~[client]={clName}", ChatMessageType.Server, null, changeType: PlayerConnectionChangeType.Joined); + SendChatMessage($"ServerMessage.JoinedServer~[client]={ClientLogName(newClient)}", ChatMessageType.Server, null, changeType: PlayerConnectionChangeType.Joined); serverSettings.ServerDetailsChanged = true; if (previousPlayer != null && previousPlayer.Name != newClient.Name) { - SendChatMessage($"ServerMessage.PreviousClientName~[client]={clName}~[previousname]={previousPlayer.Name}", ChatMessageType.Server, null); + string prevNameSanitized = previousPlayer.Name.Replace("‖", ""); + SendChatMessage($"ServerMessage.PreviousClientName~[client]={ClientLogName(newClient)}~[previousname]={prevNameSanitized}", ChatMessageType.Server, null); previousPlayer.Name = newClient.Name; } @@ -448,12 +450,12 @@ namespace Barotrauma.Networking //or very close and someone from the crew made it inside the outpost subAtLevelEnd = Submarine.MainSub.DockedTo.Contains(Level.Loaded.EndOutpost) || - (Submarine.MainSub.AtEndPosition && charactersInsideOutpost > 0) || + (Submarine.MainSub.AtEndExit && charactersInsideOutpost > 0) || (charactersInsideOutpost > charactersOutsideOutpost); } else { - subAtLevelEnd = Submarine.MainSub.AtEndPosition; + subAtLevelEnd = Submarine.MainSub.AtEndExit; } } @@ -475,12 +477,19 @@ namespace Barotrauma.Networking } else if (isCrewDead && respawnManager == null) { +#if !DEBUG if (endRoundTimer <= 0.0f) { SendChatMessage(TextManager.GetWithVariable("CrewDeadNoRespawns", "[time]", "60"), ChatMessageType.Server); } endRoundDelay = 60.0f; endRoundTimer += deltaTime; +#endif + } + else if (isCrewDead && (GameMain.GameSession?.GameMode is CampaignMode)) + { + endRoundDelay = 1.0f; + endRoundTimer += deltaTime; } else { @@ -501,10 +510,14 @@ namespace Barotrauma.Networking { Log("Ending round (submarine reached the end of the level)", ServerLog.MessageType.ServerMessage); } - else + else if (respawnManager == null) { Log("Ending round (no living players left and respawning is not enabled during this round)", ServerLog.MessageType.ServerMessage); } + else + { + Log("Ending round (no living players left)", ServerLog.MessageType.ServerMessage); + } EndGame(); return; } @@ -752,6 +765,7 @@ namespace Barotrauma.Networking string seed = inc.ReadString(); string subName = inc.ReadString(); string subHash = inc.ReadString(); + CampaignSettings settings = new CampaignSettings(inc); var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.Hash == subHash); @@ -772,10 +786,10 @@ namespace Barotrauma.Networking string localSavePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer, saveName); if (connectedClient.HasPermission(ClientPermissions.SelectMode) || connectedClient.HasPermission(ClientPermissions.ManageCampaign)) { - MultiPlayerCampaign.StartNewCampaign(localSavePath, matchingSub.FilePath, seed); + MultiPlayerCampaign.StartNewCampaign(localSavePath, matchingSub.FilePath, seed, settings); } } - } + } else { string saveName = inc.ReadString(); @@ -814,6 +828,9 @@ namespace Barotrauma.Networking case ClientPacketHeader.READY_CHECK: ReadyCheck.ServerRead(inc, connectedClient); break; + case ClientPacketHeader.READY_TO_SPAWN: + ReadReadyToSpawnMessage(inc, connectedClient); + break; case ClientPacketHeader.FILE_REQUEST: if (serverSettings.AllowFileTransfers) { @@ -912,12 +929,19 @@ namespace Barotrauma.Networking errorLines.Add("Campaign ID: " + campaign.CampaignID); errorLines.Add("Campaign save ID: " + campaign.LastSaveID); } - errorLines.Add("Mission: " + (GameMain.GameSession?.Mission?.Prefab.Identifier ?? "none")); + foreach (Mission mission in GameMain.GameSession.Missions) + { + errorLines.Add("Mission: " + mission.Prefab.Identifier); + } } if (GameMain.GameSession?.Submarine != null) { errorLines.Add("Submarine: " + GameMain.GameSession.Submarine.Info.Name); } + if (GameMain.NetworkMember?.RespawnManager?.RespawnShuttle != null) + { + errorLines.Add("Respawn shuttle: " + GameMain.NetworkMember.RespawnManager.RespawnShuttle.Info.Name); + } if (Level.Loaded != null) { errorLines.Add("Level: " + Level.Loaded.Seed + ", " + string.Join(", ", Level.Loaded.EqualityCheckValues.Select(cv => cv.ToString("X")))); @@ -1183,6 +1207,15 @@ namespace Barotrauma.Networking mpCampaign.ServerReadCrew(inc, sender); } } + private void ReadReadyToSpawnMessage(IReadMessage inc, Client sender) + { + sender.SpectateOnly = inc.ReadBoolean() && (serverSettings.AllowSpectating || sender.Connection == OwnerConnection); + sender.WaitForNextRoundRespawn = inc.ReadBoolean(); + if (!(GameMain.GameSession?.GameMode is CampaignMode)) + { + sender.WaitForNextRoundRespawn = null; + } + } private void ClientReadServerCommand(IReadMessage inc) { @@ -1300,15 +1333,23 @@ namespace Barotrauma.Networking else if (mpCampaign != null) { var availableTransition = mpCampaign.GetAvailableTransition(out _, out _); + //don't force location if we've teleported + bool forceLocation = !mpCampaign.Map.AllowDebugTeleport || mpCampaign.Map.CurrentLocation == Level.Loaded.StartLocation; switch (availableTransition) { case CampaignMode.TransitionType.ReturnToPreviousEmptyLocation: - mpCampaign.Map.SelectLocation( - mpCampaign.Map.CurrentLocation.Connections.Find(c => c.LevelData == Level.Loaded?.LevelData).OtherLocation(mpCampaign.Map.CurrentLocation)); + if (forceLocation) + { + mpCampaign.Map.SelectLocation( + mpCampaign.Map.CurrentLocation.Connections.Find(c => c.LevelData == Level.Loaded?.LevelData).OtherLocation(mpCampaign.Map.CurrentLocation)); + } mpCampaign.LoadNewLevel(); break; case CampaignMode.TransitionType.ProgressToNextEmptyLocation: - mpCampaign.Map.SetLocation(mpCampaign.Map.Locations.IndexOf(Level.Loaded.EndLocation)); + if (forceLocation) + { + mpCampaign.Map.SetLocation(mpCampaign.Map.Locations.IndexOf(Level.Loaded.EndLocation)); + } mpCampaign.LoadNewLevel(); break; case CampaignMode.TransitionType.None: @@ -1556,11 +1597,11 @@ namespace Barotrauma.Networking { distSqr = Math.Min(distSqr, Vector2.DistanceSquared(character.WorldPosition, c.Character.ViewTarget.WorldPosition)); } - if (distSqr >= NetConfig.DisableCharacterDistSqr) { continue; } + if (distSqr >= MathUtils.Pow2(character.Params.DisableDistance)) { continue; } } else { - if (character != c.Character && Vector2.DistanceSquared(character.WorldPosition, c.SpectatePos.Value) >= NetConfig.DisableCharacterDistSqr) + if (character != c.Character && Vector2.DistanceSquared(character.WorldPosition, c.SpectatePos.Value) >= MathUtils.Pow2(character.Params.DisableDistance)) { continue; } @@ -1850,6 +1891,8 @@ namespace Barotrauma.Networking { outmsg.Write(autoRestartTimerRunning ? serverSettings.AutoRestartTimer : 0.0f); } + + outmsg.Write(serverSettings.RadiationEnabled); } else { @@ -2031,12 +2074,12 @@ namespace Barotrauma.Networking } } - startGameCoroutine = GameMain.Instance.ShowLoading(StartGame(selectedSub, selectedShuttle, selectedMode), false); + startGameCoroutine = GameMain.Instance.ShowLoading(StartGame(selectedSub, selectedShuttle, selectedMode, CampaignSettings.Unsure), false); yield return CoroutineStatus.Success; } - private IEnumerable StartGame(SubmarineInfo selectedSub, SubmarineInfo selectedShuttle, GameModePreset selectedMode) + private IEnumerable StartGame(SubmarineInfo selectedSub, SubmarineInfo selectedShuttle, GameModePreset selectedMode, CampaignSettings settings) { entityEventManager.Clear(); @@ -2064,7 +2107,7 @@ namespace Barotrauma.Networking //don't instantiate a new gamesession if we're playing a campaign if (campaign == null || GameMain.GameSession == null) { - GameMain.GameSession = new GameSession(selectedSub, "", selectedMode, GameMain.NetLobbyScreen.LevelSeed, missionType: GameMain.NetLobbyScreen.MissionType); + GameMain.GameSession = new GameSession(selectedSub, "", selectedMode, settings, GameMain.NetLobbyScreen.LevelSeed, missionType: GameMain.NetLobbyScreen.MissionType); } List playingClients = new List(connectedClients); @@ -2110,7 +2153,6 @@ namespace Barotrauma.Networking Log("Game mode: " + selectedMode.Name, ServerLog.MessageType.ServerMessage); Log("Submarine: " + GameMain.GameSession.SubmarineInfo.Name, ServerLog.MessageType.ServerMessage); Log("Level seed: " + campaign.NextLevel.Seed, ServerLog.MessageType.ServerMessage); - if (GameMain.GameSession.Mission != null) { Log("Mission: " + GameMain.GameSession.Mission.Prefab.Name, ServerLog.MessageType.ServerMessage); } } else { @@ -2120,7 +2162,11 @@ namespace Barotrauma.Networking Log("Game mode: " + selectedMode.Name, ServerLog.MessageType.ServerMessage); Log("Submarine: " + selectedSub.Name, ServerLog.MessageType.ServerMessage); Log("Level seed: " + GameMain.NetLobbyScreen.LevelSeed, ServerLog.MessageType.ServerMessage); - if (GameMain.GameSession.Mission != null) { Log("Mission: " + GameMain.GameSession.Mission.Prefab.Name, ServerLog.MessageType.ServerMessage); } + } + + foreach (Mission mission in GameMain.GameSession.Missions) + { + Log("Mission: " + mission.Prefab.Name, ServerLog.MessageType.ServerMessage); } if (GameMain.GameSession.SubmarineInfo.IsFileCorrupted) @@ -2132,12 +2178,17 @@ namespace Barotrauma.Networking } MissionMode missionMode = GameMain.GameSession.GameMode as MissionMode; - bool missionAllowRespawn = GameMain.GameSession.Campaign == null && (missionMode?.Mission == null || missionMode.Mission.AllowRespawn); - bool outpostAllowRespawn = GameMain.GameSession.Campaign != null && Level.Loaded?.Type == LevelData.LevelType.Outpost; + bool missionAllowRespawn = missionMode == null || !missionMode.Missions.Any(m => !m.AllowRespawn); + bool isOutpost = campaign != null && campaign.NextLevel?.Type == LevelData.LevelType.Outpost; - if (serverSettings.AllowRespawn && (missionAllowRespawn || outpostAllowRespawn)) + if (serverSettings.AllowRespawn && missionAllowRespawn) { - respawnManager = new RespawnManager(this, serverSettings.UseRespawnShuttle && !outpostAllowRespawn ? selectedShuttle : null); + respawnManager = new RespawnManager(this, serverSettings.UseRespawnShuttle && !isOutpost ? selectedShuttle : null); + } + if (campaign != null) + { + campaign.CargoManager.CreatePurchasedItems(); + campaign.SendCrewState(null, default, null); } Level.Loaded?.SpawnNPCs(); @@ -2202,7 +2253,7 @@ namespace Barotrauma.Networking } else { - client.CharacterInfo.ResetCurrentOrder(); + client.CharacterInfo.ClearCurrentOrders(); } characterInfos.Add(client.CharacterInfo); if (client.CharacterInfo.Job == null || client.CharacterInfo.Job.Prefab != client.AssignedJob.First) @@ -2245,7 +2296,9 @@ namespace Barotrauma.Networking List spawnWaypoints = null; List mainSubWaypoints = WayPoint.SelectCrewSpawnPoints(characterInfos, Submarine.MainSubs[n]).ToList(); - if (Level.Loaded?.StartOutpost != null && Level.Loaded.Type == LevelData.LevelType.Outpost && + if (Level.Loaded?.StartOutpost != null && + Level.Loaded.Type == LevelData.LevelType.Outpost && + (Level.Loaded.StartOutpost.Info.OutpostGenerationParams?.SpawnCrewInsideOutpost ?? false) && Level.Loaded.StartOutpost.GetConnectedSubs().Any(s => s.Info.Type == SubmarineType.Player)) { spawnWaypoints = WayPoint.WayPointList.FindAll(wp => @@ -2322,7 +2375,7 @@ namespace Barotrauma.Networking foreach (Submarine sub in Submarine.MainSubs) { - if (sub == null) continue; + if (sub == null) { continue; } List spawnList = new List(); foreach (KeyValuePair kvp in serverSettings.ExtraCargo) @@ -2330,7 +2383,7 @@ namespace Barotrauma.Networking spawnList.Add(new PurchasedItem(kvp.Key, kvp.Value)); } - CargoManager.CreateItems(spawnList); + CargoManager.CreateItems(spawnList, sub); } TraitorManager = null; @@ -2385,12 +2438,11 @@ namespace Barotrauma.Networking msg.Write((byte)ServerPacketHeader.STARTGAME); msg.Write(seed); msg.Write(gameSession.GameMode.Preset.Identifier); - - bool missionAllowRespawn = campaign == null && (missionMode?.Mission == null || missionMode.Mission.AllowRespawn); - bool outpostAllowRespawn = campaign != null && campaign.NextLevel?.Type == LevelData.LevelType.Outpost; - msg.Write(serverSettings.AllowRespawn && (missionAllowRespawn || outpostAllowRespawn)); + bool missionAllowRespawn = missionMode == null || !missionMode.Missions.Any(m => !m.AllowRespawn); + msg.Write(serverSettings.AllowRespawn && missionAllowRespawn); msg.Write(serverSettings.AllowDisguises); msg.Write(serverSettings.AllowRewiring); + msg.Write(serverSettings.LockAllDefaultWires); msg.Write(serverSettings.AllowRagdollButton); msg.Write(serverSettings.UseRespawnShuttle); msg.Write((byte)GameMain.Config.LosMode); @@ -2406,7 +2458,11 @@ namespace Barotrauma.Networking msg.Write(gameSession.SubmarineInfo.MD5Hash.Hash); msg.Write(GameMain.NetLobbyScreen.SelectedShuttle.Name); msg.Write(GameMain.NetLobbyScreen.SelectedShuttle.MD5Hash.Hash); - msg.Write((short)(GameMain.GameSession.GameMode?.Mission == null ? -1 : MissionPrefab.List.IndexOf(GameMain.GameSession.GameMode.Mission.Prefab))); + msg.Write((byte)GameMain.GameSession.GameMode.Missions.Count()); + foreach (Mission mission in GameMain.GameSession.GameMode.Missions) + { + msg.Write((short)MissionPrefab.List.IndexOf(mission.Prefab)); + } } else { @@ -2446,13 +2502,20 @@ namespace Barotrauma.Networking msg.Write(contentFile.Path); } msg.Write(Submarine.MainSub?.Info.EqualityCheckVal ?? 0); - msg.Write(GameMain.GameSession.Mission?.Prefab.Identifier ?? ""); + msg.Write((byte)GameMain.GameSession.Missions.Count()); + foreach (Mission mission in GameMain.GameSession.Missions) + { + msg.Write(mission.Prefab.Identifier); + } msg.Write((byte)GameMain.GameSession.Level.EqualityCheckValues.Count); foreach (int equalityCheckValue in GameMain.GameSession.Level.EqualityCheckValues) { msg.Write(equalityCheckValue); } - GameMain.GameSession.Mission?.ServerWriteInitial(msg, client); + foreach (Mission mission in GameMain.GameSession.Missions) + { + mission.ServerWriteInitial(msg, client); + } } public void EndGame(CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None) @@ -2475,7 +2538,7 @@ namespace Barotrauma.Networking string endMessage = TextManager.FormatServerMessage("RoundSummaryRoundHasEnded"); var traitorResults = TraitorManager?.GetEndResults() ?? new List(); - Mission mission = GameMain.GameSession.Mission; + List missions = GameMain.GameSession.Missions.ToList(); if (GameMain.GameSession.IsRunning) { GameMain.GameSession.EndRound(endMessage, traitorResults); @@ -2517,7 +2580,11 @@ namespace Barotrauma.Networking msg.Write((byte)ServerPacketHeader.ENDGAME); msg.Write((byte)transitionType); msg.Write(endMessage); - msg.Write(mission != null && mission.Completed); + msg.Write((byte)missions.Count); + foreach (Mission mission in missions) + { + msg.Write(mission.Completed); + } msg.Write(GameMain.GameSession?.WinningTeam == null ? (byte)0 : (byte)GameMain.GameSession.WinningTeam); msg.Write((byte)traitorResults.Count); @@ -2529,10 +2596,11 @@ namespace Barotrauma.Networking foreach (Client client in connectedClients) { serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); - client.Character?.ResetCurrentOrder(); + client.Character?.Info?.ClearCurrentOrders(); client.Character = null; client.HasSpawned = false; client.InGame = false; + client.WaitForNextRoundRespawn = null; } } @@ -2694,6 +2762,10 @@ namespace Barotrauma.Networking { serverSettings.BanList.BanPlayer(client.Name, client.SteamID, reason, duration); } + if (client.OwnerSteamID > 0) + { + serverSettings.BanList.BanPlayer(client.Name, client.OwnerSteamID, reason, duration); + } } public void BanPreviousPlayer(PreviousPlayer previousPlayer, string reason, bool range = false, TimeSpan? duration = null) @@ -2713,6 +2785,10 @@ namespace Barotrauma.Networking { serverSettings.BanList.BanPlayer(previousPlayer.Name, previousPlayer.SteamID, reason, duration); } + if (previousPlayer.OwnerSteamID > 0) + { + serverSettings.BanList.BanPlayer(previousPlayer.Name, previousPlayer.OwnerSteamID, reason, duration); + } string msg = $"ServerMessage.BannedFromServer~[client]={previousPlayer.Name}"; if (!string.IsNullOrWhiteSpace(reason)) @@ -2760,9 +2836,10 @@ namespace Barotrauma.Networking client.Character = null; client.HasSpawned = false; + client.WaitForNextRoundRespawn = null; client.InGame = false; - if (string.IsNullOrWhiteSpace(msg)) { msg = $"ServerMessage.ClientLeftServer~[client]={client.Name}"; } + if (string.IsNullOrWhiteSpace(msg)) { msg = $"ServerMessage.ClientLeftServer~[client]={ClientLogName(client)}"; } if (string.IsNullOrWhiteSpace(targetmsg)) { targetmsg = "ServerMessage.YouLeftServer"; } if (!string.IsNullOrWhiteSpace(reason)) { @@ -2999,7 +3076,8 @@ namespace Barotrauma.Networking else if (type == ChatMessageType.Radio) { //send to chat-linked wifi components - senderRadio.TransmitSignal(0, message, senderRadio.Item, senderCharacter, sentFromChat: true); + Signal s = new Signal(message, sender: senderCharacter, source: senderRadio.Item); + senderRadio.TransmitSignal(s, sentFromChat: true); } //check which clients can receive the message and apply distance effects @@ -3050,7 +3128,7 @@ namespace Barotrauma.Networking string myReceivedMessage = type == ChatMessageType.Server || type == ChatMessageType.Error ? TextManager.GetServerMessage(message) : message; if (!string.IsNullOrWhiteSpace(myReceivedMessage)) { - AddChatMessage(myReceivedMessage, (ChatMessageType)type, senderName, senderCharacter); + AddChatMessage(myReceivedMessage, (ChatMessageType)type, senderName, senderClient, senderCharacter); } } } @@ -3072,14 +3150,14 @@ namespace Barotrauma.Networking if (!client.Character.CanHearCharacter(message.Sender)) { continue; } } - SendDirectChatMessage(new OrderChatMessage(message.Order, message.OrderOption, message.TargetEntity, message.TargetCharacter, message.Sender), client); + SendDirectChatMessage(new OrderChatMessage(message.Order, message.OrderOption, message.OrderPriority, message.TargetEntity, message.TargetCharacter, message.Sender), client); } string myReceivedMessage = message.Text; if (!string.IsNullOrWhiteSpace(myReceivedMessage)) { - AddChatMessage(new OrderChatMessage(message.Order, message.OrderOption, myReceivedMessage, message.TargetEntity, message.TargetCharacter, message.Sender)); + AddChatMessage(new OrderChatMessage(message.Order, message.OrderOption, message.OrderPriority, myReceivedMessage, message.TargetEntity, message.TargetCharacter, message.Sender)); } } @@ -3461,9 +3539,9 @@ namespace Barotrauma.Networking unassigned.RemoveAt(i); } - //go throught the jobs whose MinNumber>0 (i.e. at least one crew member has to have the job) + // Assign the necessary jobs that are always required at least one, in vanilla this means in practice the captain bool unassignedJobsFound = true; - while (unassignedJobsFound && unassigned.Count > 0) + while (unassignedJobsFound && unassigned.Any()) { unassignedJobsFound = false; @@ -3471,16 +3549,33 @@ namespace Barotrauma.Networking { if (unassigned.Count == 0) { break; } if (jobPrefab.MinNumber < 1 || assignedClientCount[jobPrefab] >= jobPrefab.MinNumber) { continue; } + // Find the client that wants the job the most, don't force any jobs yet, because it might be that we can meet the preference for other jobs. + Client client = FindClientWithJobPreference(unassigned, jobPrefab, forceAssign: false); + if (client != null) + { + AssignJob(client, jobPrefab); + } + } - //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); + if (unassigned.Any()) + { + // Another pass, force required jobs that are not yet filled. + foreach (JobPrefab jobPrefab in jobList) + { + if (unassigned.Count == 0) { break; } + if (jobPrefab.MinNumber < 1 || assignedClientCount[jobPrefab] >= jobPrefab.MinNumber) { continue; } + AssignJob(FindClientWithJobPreference(unassigned, jobPrefab, forceAssign: true), jobPrefab); + } + } - assignedClient.AssignedJob = - assignedClient.JobPreferences.FirstOrDefault(jp => jp.First == jobPrefab) ?? - new Pair(jobPrefab, 0); + void AssignJob(Client client, JobPrefab jobPrefab) + { + client.AssignedJob = + client.JobPreferences.FirstOrDefault(jp => jp.First == jobPrefab) ?? + new Pair(jobPrefab, Rand.Int(jobPrefab.Variants)); assignedClientCount[jobPrefab]++; - unassigned.Remove(assignedClient); + unassigned.Remove(client); //the job still needs more crew members, set unassignedJobsFound to true to keep the while loop running if (assignedClientCount[jobPrefab] < jobPrefab.MinNumber) { unassignedJobsFound = true; } @@ -3514,32 +3609,37 @@ namespace Barotrauma.Networking } } while (unassigned.Count > 0 && canAssign);*/ - //attempt to give the clients a job they have in their job preferences - for (int i = unassigned.Count - 1; i >= 0; i--) + // Attempt to give the clients a job they have in their job preferences. + // First evaluate all the primary preferences, then all the secondary etc. + for (int preferenceIndex = 0; preferenceIndex < 3; preferenceIndex++) { - if (unassignedSpawnPoints.Count == 0) { break; } - foreach (Pair preferredJob in unassigned[i].JobPreferences) + if (unassignedSpawnPoints.None()) { break; } + for (int i = unassigned.Count - 1; i >= 0; i--) { - //can't assign this job if maximum number has reached or the clien't karma is too low - if (assignedClientCount[preferredJob.First] >= preferredJob.First.MaxNumber || unassigned[i].Karma < preferredJob.First.MinKarma) + if (unassignedSpawnPoints.None()) { break; } + Client client = unassigned[i]; + if (preferenceIndex >= client.JobPreferences.Count) { continue; } + var preferredJob = client.JobPreferences[preferenceIndex]; + JobPrefab jobPrefab = preferredJob.First; + if (assignedClientCount[jobPrefab] >= jobPrefab.MaxNumber || client.Karma < jobPrefab.MinKarma) { + //can't assign this job if maximum number has reached or the clien't karma is too low continue; } //give the client their preferred job if there's a spawnpoint available for that job - var matchingSpawnPoint = unassignedSpawnPoints.Find(s => s.AssignedJob == preferredJob.First); - //if the job is not available in any spawnpoint (custom job?), treat empty spawnpoints - //as a matching ones - if (matchingSpawnPoint == null && !availableSpawnPoints.Any(s => s.AssignedJob == preferredJob.First)) + var matchingSpawnPoint = unassignedSpawnPoints.Find(s => s.AssignedJob == jobPrefab); + if (matchingSpawnPoint == null && !availableSpawnPoints.Any(s => s.AssignedJob == jobPrefab)) { + //if the job is not available in any spawnpoint (custom job?), treat empty spawnpoints + //as a matching ones matchingSpawnPoint = unassignedSpawnPoints.Find(s => s.AssignedJob == null); } if (matchingSpawnPoint != null) { unassignedSpawnPoints.Remove(matchingSpawnPoint); - unassigned[i].AssignedJob = preferredJob; - assignedClientCount[preferredJob.First]++; + client.AssignedJob = preferredJob; + assignedClientCount[jobPrefab]++; unassigned.RemoveAt(i); - break; } } } @@ -3660,15 +3760,13 @@ namespace Barotrauma.Networking private Client FindClientWithJobPreference(List clients, JobPrefab job, bool forceAssign = false) { - int bestPreference = 0; + int bestPreference = int.MaxValue; Client preferredClient = null; foreach (Client c in clients) { - if (c.Karma < job.MinKarma) continue; + if (ServerSettings.KarmaEnabled && c.Karma < job.MinKarma) { continue; } int index = c.JobPreferences.IndexOf(c.JobPreferences.Find(j => j.First == job)); - if (index == -1) index = 1000; - - if (preferredClient == null || index < bestPreference) + if (index > -1 && index < bestPreference) { bestPreference = index; preferredClient = c; @@ -3684,29 +3782,19 @@ namespace Barotrauma.Networking return preferredClient; } - public void UpdateMissionState(int state) + public void UpdateMissionState(Mission mission, int state) { foreach (var client in connectedClients) { IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ServerPacketHeader.MISSION); + int missionIndex = GameMain.GameSession.GetMissionIndex(mission); + msg.Write((byte)(missionIndex == -1 ? 255: missionIndex)); msg.Write((ushort)state); serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } } - public static string ClientLogName(Client client, string name = null) - { - if (client == null) { return name; } - string retVal = "‖"; - if (client.Karma < 40.0f) - { - retVal += "color:#ff9900;"; - } - retVal += "metadata:" + (client.SteamID != 0 ? client.SteamID.ToString() : client.ID.ToString()) + "‖" + (name ?? client.Name) + "‖end‖"; - return retVal; - } - public static string CharacterLogName(Character character) { if (character == null) { return "[NULL]"; } @@ -3782,6 +3870,7 @@ namespace Barotrauma.Networking public string Name; public string EndPoint; public UInt64 SteamID; + public UInt64 OwnerSteamID; public float Karma; public int KarmaKickCount; public readonly List KickVoters = new List(); @@ -3791,6 +3880,7 @@ namespace Barotrauma.Networking Name = c.Name; EndPoint = c.Connection?.EndPointString ?? ""; SteamID = c.SteamID; + OwnerSteamID = c.OwnerSteamID; } public bool MatchesClient(Client c) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs index dd80754d3..f2cef0e0e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs @@ -1,6 +1,4 @@ -using System; - -namespace Barotrauma.Networking +namespace Barotrauma.Networking { partial class OrderChatMessage : ChatMessage { @@ -9,34 +7,19 @@ namespace Barotrauma.Networking msg.Write((byte)ServerNetObject.CHAT_MESSAGE); msg.Write(NetStateID); msg.Write((byte)ChatMessageType.Order); - msg.Write(SenderName); + msg.Write(SenderClient != null); + if (SenderClient != null) + { + msg.Write((SenderClient.SteamID != 0) ? SenderClient.SteamID : SenderClient.ID); + } msg.Write(Sender != null && c.InGame); if (Sender != null && c.InGame) { msg.Write(Sender.ID); } - - msg.Write((byte)Order.PrefabList.IndexOf(Order.Prefab)); - msg.Write(TargetCharacter == null ? (UInt16)0 : TargetCharacter.ID); - msg.Write(TargetEntity is Entity ? (TargetEntity as Entity).ID : (UInt16)0); - msg.Write((byte)Array.IndexOf(Order.Prefab.Options, OrderOption)); - msg.Write((byte)Order.TargetType); - if (Order.TargetType == Order.OrderTargetType.Position && TargetEntity is OrderTarget orderTarget) - { - msg.Write(true); - msg.Write(orderTarget.Position.X); - msg.Write(orderTarget.Position.Y); - msg.Write(orderTarget.Hull == null ? (UInt16)0 : orderTarget.Hull.ID); - } - else - { - msg.Write(false); - if (Order.TargetType == Order.OrderTargetType.WallSection) - { - msg.Write((byte)(WallSectionIndex ?? Order.WallSectionIndex ?? 0)); - } - } + msg.WritePadBits(); + WriteOrder(msg); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs index 8d75fa8f4..8daa8ad3d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs @@ -184,7 +184,7 @@ namespace Barotrauma.Networking return; } - if (serverSettings.BanList.IsBanned(inc.SenderConnection.RemoteEndPoint.Address, 0, out string banReason)) + if (serverSettings.BanList.IsBanned(inc.SenderConnection.RemoteEndPoint.Address, 0, 0, out string banReason)) { //IP banned: deny immediately inc.SenderConnection.Deny(DisconnectReason.Banned.ToString() + "/ " + banReason); @@ -233,7 +233,7 @@ namespace Barotrauma.Networking return; } if (pendingClient != null) { pendingClients.Remove(pendingClient); } - if (serverSettings.BanList.IsBanned(conn.IPEndPoint.Address, conn.SteamID, out string banReason)) + if (serverSettings.BanList.IsBanned(conn.IPEndPoint.Address, conn.SteamID, conn.OwnerSteamID, out string banReason)) { Disconnect(conn, DisconnectReason.Banned.ToString() + "/ " + banReason); return; @@ -308,7 +308,8 @@ namespace Barotrauma.Networking } LidgrenConnection pendingConnection = pendingClient.Connection as LidgrenConnection; - if (serverSettings.BanList.IsBanned(pendingConnection.NetConnection.RemoteEndPoint.Address, steamID, out string banReason)) + string banReason; + if (serverSettings.BanList.IsBanned(pendingConnection.NetConnection.RemoteEndPoint.Address, steamID, ownerID, out banReason)) { RemovePendingClient(pendingClient, DisconnectReason.Banned, banReason); return; @@ -316,6 +317,7 @@ namespace Barotrauma.Networking if (status == Steamworks.AuthResponse.OK) { + pendingClient.OwnerSteamID = ownerID; pendingClient.InitializationStep = serverSettings.HasPassword ? ConnectionInitialization.Password : ConnectionInitialization.ContentPackageOrder; pendingClient.UpdateTime = Timing.TotalTime; } @@ -442,8 +444,16 @@ namespace Barotrauma.Networking Steamworks.BeginAuthResult authSessionStartState = Steam.SteamManager.StartAuthSession(ticket, steamId); if (authSessionStartState != Steamworks.BeginAuthResult.OK) { - RemovePendingClient(pendingClient, DisconnectReason.SteamAuthenticationFailed, "Steam auth session failed to start: " + authSessionStartState.ToString()); - return; + if (requireSteamAuth) + { + RemovePendingClient(pendingClient, DisconnectReason.SteamAuthenticationFailed, "Steam auth session failed to start: " + authSessionStartState.ToString()); + return; + } + else + { + steamId = 0; + pendingClient.InitializationStep = serverSettings.HasPassword ? ConnectionInitialization.Password : ConnectionInitialization.ContentPackageOrder; + } } pendingClient.SteamID = steamId; pendingClient.Connection.Name = name; @@ -452,7 +462,7 @@ namespace Barotrauma.Networking pendingClient.AuthSessionStarted = true; } } - else //TODO: could remove since this seems impossible + else { if (pendingClient.SteamID != steamId) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs index bfabe7aa8..c72467622 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs @@ -49,6 +49,16 @@ namespace Barotrauma.Networking Connection.SetSteamIDIfUnknown(value ?? 0); } } + private UInt64? ownerSteamId; + public UInt64? OwnerSteamID + { + get { return ownerSteamId; } + set + { + ownerSteamId = value; + Connection.SetOwnerSteamIDIfUnknown(value ?? 0); + } + } public Int32? PasswordSalt; public bool AuthSessionStarted; @@ -59,6 +69,7 @@ namespace Barotrauma.Networking InitializationStep = ConnectionInitialization.SteamTicketAndVersion; Retries = 0; SteamID = null; + OwnerSteamID = null; PasswordSalt = null; UpdateTime = Timing.TotalTime + Timing.Step * 3.0; TimeOut = NetworkConnection.TimeoutThreshold; @@ -107,8 +118,8 @@ namespace Barotrauma.Networking RemovePendingClient(pendingClient, DisconnectReason.InvalidVersion, $"DisconnectMessage.InvalidVersion~[version]={GameMain.Version}~[clientversion]={version}"); - GameServer.Log(name + " (" + pendingClient.SteamID.ToString() + ") couldn't join the server (incompatible game version)", ServerLog.MessageType.Error); - DebugConsole.NewMessage(name + " (" + pendingClient.SteamID.ToString() + ") couldn't join the server (incompatible game version)", Microsoft.Xna.Framework.Color.Red); + GameServer.Log($"{name} ({steamId}) couldn't join the server (incompatible game version)", ServerLog.MessageType.Error); + DebugConsole.NewMessage($"{name} ({steamId}) couldn't join the server (incompatible game version)", Microsoft.Xna.Framework.Color.Red); return; } @@ -119,7 +130,7 @@ namespace Barotrauma.Networking if (nameTaken != null) { RemovePendingClient(pendingClient, DisconnectReason.NameTaken, ""); - GameServer.Log(name + " (" + pendingClient.SteamID.ToString() + ") couldn't join the server (name too similar to the name of the client \"" + nameTaken.Name + "\").", ServerLog.MessageType.Error); + GameServer.Log($"{name} ({steamId}) couldn't join the server (name too similar to the name of the client \"" + nameTaken.Name + "\").", ServerLog.MessageType.Error); return; } @@ -172,6 +183,7 @@ namespace Barotrauma.Networking else if (pendingClient.Connection is SteamP2PConnection s) { serverSettings.BanList.BanPlayer(pendingClient.Name, s.SteamID, banReason, duration); + serverSettings.BanList.BanPlayer(pendingClient.Name, s.OwnerSteamID, banReason, duration); } } @@ -183,7 +195,8 @@ namespace Barotrauma.Networking } else if (pendingClient.Connection is SteamP2PConnection s) { - return serverSettings.BanList.IsBanned(s.SteamID, out banReason); + return serverSettings.BanList.IsBanned(s.SteamID, out banReason) || + serverSettings.BanList.IsBanned(s.OwnerSteamID, out banReason); } banReason = null; return false; @@ -199,7 +212,7 @@ namespace Barotrauma.Networking return; } - if (connectedClients.Count >= serverSettings.MaxPlayers - 1) + if (connectedClients.Count >= serverSettings.MaxPlayers) { RemovePendingClient(pendingClient, DisconnectReason.ServerFull, ""); } @@ -273,6 +286,7 @@ namespace Barotrauma.Networking { Steam.SteamManager.StopAuthSession(pendingClient.SteamID.Value); pendingClient.SteamID = null; + pendingClient.OwnerSteamID = null; pendingClient.AuthSessionStarted = false; } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs index c53cdff42..c7b85305c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs @@ -123,6 +123,7 @@ namespace Barotrauma.Networking if (!started) { return; } UInt64 senderSteamId = inc.ReadUInt64(); + UInt64 ownerSteamId = inc.ReadUInt64(); byte incByte = inc.ReadByte(); bool isCompressed = (incByte & (byte)PacketHeader.IsCompressed) != 0; @@ -145,7 +146,9 @@ namespace Barotrauma.Networking pendingClient?.Heartbeat(); connectedClient?.Heartbeat(); - if (serverSettings.BanList.IsBanned(senderSteamId, out string banReason)) + string banReason; + if (serverSettings.BanList.IsBanned(senderSteamId, out banReason) || + serverSettings.BanList.IsBanned(ownerSteamId, out banReason)) { if (pendingClient != null) { @@ -181,6 +184,10 @@ namespace Barotrauma.Networking if (pendingClient != null) { + if (ownerSteamId != 0) + { + pendingClient.Connection.SetOwnerSteamIDIfUnknown(ownerSteamId); + } ReadConnectionInitializationStep(pendingClient, new ReadOnlyMessage(inc.Buffer, false, inc.BytePosition, inc.LengthBytes - inc.BytePosition, null)); } else @@ -223,6 +230,7 @@ namespace Barotrauma.Networking { Language = GameMain.Config.Language }; + OwnerConnection.SetOwnerSteamIDIfUnknown(OwnerSteamID); OnInitializationComplete?.Invoke(OwnerConnection); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index 4cf089309..3c7e188a7 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -19,14 +19,22 @@ namespace Barotrauma.Networking if (c.SpectateOnly && (GameMain.Server.ServerSettings.AllowSpectating || GameMain.Server.OwnerConnection == c.Connection)) { continue; } if (c.Character != null && !c.Character.IsDead) { continue; } - //don't allow respawning if the client has previously disconnected and their corpse is still present on the server + //don't allow respawn if the client already has a character (they'll regain control once they're in sync) var matchingData = campaign?.GetClientCharacterData(c); - if (matchingData != null && matchingData.HasSpawned && - Character.CharacterList.Any(c => c.Info == matchingData.CharacterInfo && c.CauseOfDeath?.Type == CauseOfDeathType.Disconnected)) + if (matchingData != null && matchingData.HasSpawned && + Character.CharacterList.Any(c => c.Info == matchingData.CharacterInfo && !c.IsDead)) { continue; } + if (UseRespawnPrompt) + { + if (matchingData != null && matchingData.HasSpawned) + { + if (!c.WaitForNextRoundRespawn.HasValue || c.WaitForNextRoundRespawn.Value) { continue; } + } + } + yield return c; } } @@ -68,34 +76,53 @@ namespace Barotrauma.Networking return botsToRespawn; } - private bool RespawnPending() + private bool ShouldStartRespawnCountdown() { int characterToRespawnCount = GetClientsToRespawn().Count(); + return ShouldStartRespawnCountdown(characterToRespawnCount); + } + + private bool ShouldStartRespawnCountdown(int characterToRespawnCount) + { int totalCharacterCount = GameMain.Server.ConnectedClients.Count; return (float)characterToRespawnCount >= Math.Max((float)totalCharacterCount * GameMain.Server.ServerSettings.MinRespawnRatio, 1.0f); } partial void UpdateWaiting(float deltaTime) { - bool respawnPending = RespawnPending(); - if (respawnPending != RespawnCountdownStarted) + if (RespawnShuttle != null) { - RespawnCountdownStarted = respawnPending; - RespawnTime = DateTime.Now + new TimeSpan(0,0,0,0, (int)(GameMain.Server.ServerSettings.RespawnInterval * 1000.0f)); - GameMain.Server.CreateEntityEvent(this); + RespawnShuttle.Velocity = Vector2.Zero; } - if (!RespawnCountdownStarted) { return; } + int clientsToRespawn = GetClientsToRespawn().Count(); + if (RespawnCountdownStarted) + { + if (clientsToRespawn == 0) + { + RespawnCountdownStarted = false; + GameMain.Server.CreateEntityEvent(this); + } + } + else + { + bool shouldStartCountdown = ShouldStartRespawnCountdown(clientsToRespawn); + if (shouldStartCountdown) + { + RespawnCountdownStarted = true; + if (RespawnTime < DateTime.Now) + { + RespawnTime = DateTime.Now + new TimeSpan(0, 0, 0, 0, (int)(GameMain.Server.ServerSettings.RespawnInterval * 1000.0f)); + } + GameMain.Server.CreateEntityEvent(this); + } + } - if (DateTime.Now > RespawnTime) + if (RespawnCountdownStarted && DateTime.Now > RespawnTime) { DispatchShuttle(); RespawnCountdownStarted = false; } - - if (RespawnShuttle == null) { return; } - - RespawnShuttle.Velocity = Vector2.Zero; } private void DispatchShuttle() @@ -114,10 +141,10 @@ namespace Barotrauma.Networking GameServer.Log("Dispatching the respawn shuttle.", ServerLog.MessageType.Spawning); - RespawnCharacters(); + Vector2 spawnPos = FindSpawnPos(); + RespawnCharacters(spawnPos); CoroutineManager.StopCoroutines("forcepos"); - Vector2 spawnPos = FindSpawnPos(); if (spawnPos.Y > Level.Loaded.Size.Y) { CoroutineManager.StartCoroutine(ForceShuttleToPos(Level.Loaded.StartPosition - Vector2.UnitY * Level.ShaftHeight, 100.0f), "forcepos"); @@ -136,7 +163,7 @@ namespace Barotrauma.Networking GameServer.Log("Respawning everyone in main sub.", ServerLog.MessageType.Spawning); GameMain.Server.CreateEntityEvent(this); - RespawnCharacters(); + RespawnCharacters(null); } } @@ -183,7 +210,6 @@ namespace Barotrauma.Networking partial void UpdateTransportingProjSpecific(float deltaTime) { - if (!ReturnCountdownStarted) { //if there are no living chracters inside, transporting can be stopped immediately @@ -192,7 +218,7 @@ namespace Barotrauma.Networking ReturnTime = DateTime.Now; ReturnCountdownStarted = true; } - else if (!RespawnPending()) + else if (!ShouldStartRespawnCountdown()) { //don't start counting down until someone else needs to respawn ReturnTime = DateTime.Now + new TimeSpan(0, 0, 0, 0, milliseconds: (int)(maxTransportTime * 1000)); @@ -218,7 +244,7 @@ namespace Barotrauma.Networking } } - partial void RespawnCharactersProjSpecific() + partial void RespawnCharactersProjSpecific(Vector2? shuttlePos) { var respawnSub = RespawnShuttle ?? Submarine.MainSub; @@ -230,6 +256,8 @@ namespace Barotrauma.Networking //get rid of the existing character c.Character?.DespawnNow(); + c.WaitForNextRoundRespawn = null; + var matchingData = campaign?.GetClientCharacterData(c); if (matchingData != null && !matchingData.HasSpawned) { @@ -265,10 +293,21 @@ namespace Barotrauma.Networking //(in order to give them appropriate ID card tags) var mainSubSpawnPoints = WayPoint.SelectCrewSpawnPoints(characterInfos, Submarine.MainSub); - ItemPrefab divingSuitPrefab = MapEntityPrefab.Find(null, "divingsuit") as ItemPrefab; - ItemPrefab oxyPrefab = MapEntityPrefab.Find(null, "oxygentank") as ItemPrefab; - ItemPrefab scooterPrefab = MapEntityPrefab.Find(null, "underwaterscooter") as ItemPrefab; - ItemPrefab batteryPrefab = MapEntityPrefab.Find(null, "batterycell") as ItemPrefab; + ItemPrefab divingSuitPrefab = null; + if ((shuttlePos != null && Level.Loaded.GetRealWorldDepth(shuttlePos.Value.Y) > Level.DefaultRealWorldCrushDepth) || + Level.Loaded.GetRealWorldDepth(Submarine.MainSub.WorldPosition.Y) > Level.DefaultRealWorldCrushDepth) + { + divingSuitPrefab = ItemPrefab.Prefabs.FirstOrDefault(it => it.Tags.Any(t => t.Equals("respawnsuitdeep", StringComparison.OrdinalIgnoreCase))); + } + if (divingSuitPrefab == null) + { + divingSuitPrefab = + ItemPrefab.Prefabs.FirstOrDefault(it => it.Tags.Any(t => t.Equals("respawnsuit", StringComparison.OrdinalIgnoreCase))) ?? + ItemPrefab.Find(null, "divingsuit"); + } + ItemPrefab oxyPrefab = ItemPrefab.Find(null, "oxygentank"); + ItemPrefab scooterPrefab = ItemPrefab.Find(null, "underwaterscooter"); + ItemPrefab batteryPrefab = ItemPrefab.Find(null, "batterycell"); var cargoSp = WayPoint.WayPointList.Find(wp => wp.Submarine == respawnSub && wp.SpawnType == SpawnType.Cargo); @@ -276,8 +315,7 @@ namespace Barotrauma.Networking { bool bot = i >= clients.Count; - characterInfos[i].CurrentOrder = null; - characterInfos[i].CurrentOrderOption = null; + characterInfos[i].ClearCurrentOrders(); var character = Character.Create(characterInfos[i], shuttleSpawnPoints[i].WorldPosition, characterInfos[i].Name, isRemotePlayer: !bot, hasAi: bot); character.TeamID = CharacterTeamType.Team1; @@ -333,6 +371,15 @@ namespace Barotrauma.Networking } var characterData = campaign?.GetClientCharacterData(clients[i]); + if (characterData != null && Level.Loaded?.Type != LevelData.LevelType.Outpost && characterData.HasSpawned) + { + var respawnPenaltyAffliction = AfflictionPrefab.List.FirstOrDefault(a => a.AfflictionType.Equals("respawnpenalty", StringComparison.OrdinalIgnoreCase)); + if (respawnPenaltyAffliction != null) + { + character.CharacterHealth.ApplyAffliction(targetLimb: null, respawnPenaltyAffliction.Instantiate(10.0f)); + } + } + if (characterData == null || characterData.HasSpawned) { //give the character the items they would've gotten if they had spawned in the main sub diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index 79baf5f76..1f2107de1 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -49,6 +49,7 @@ namespace Barotrauma.Networking outMsg.Write((byte)MaxPlayers); outMsg.Write(HasPassword); outMsg.Write(IsPublic); + outMsg.Write(AllowFileTransfers); outMsg.WritePadBits(); outMsg.WriteRangedInteger(TickRate, 1, 60); @@ -159,6 +160,8 @@ namespace Barotrauma.Networking AutoRestart = autoRestart; } + RadiationEnabled = incMsg.ReadBoolean(); + changed |= true; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs index f00dec7dd..acb320756 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs @@ -65,7 +65,7 @@ namespace Barotrauma public void ServerRead(IReadMessage inc, Client sender) { - if (GameMain.Server == null || sender == null) return; + if (GameMain.Server == null || sender == null) { return; } byte voteTypeByte = inc.ReadByte(); VoteType voteType = VoteType.Unknown; @@ -83,7 +83,10 @@ namespace Barotrauma { case VoteType.Sub: int equalityCheckVal = inc.ReadInt32(); - SubmarineInfo sub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.EqualityCheckVal == equalityCheckVal); + string hash = equalityCheckVal > 0 ? string.Empty : inc.ReadString(); + SubmarineInfo sub = equalityCheckVal > 0 ? + SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Type == SubmarineType.Player && s.EqualityCheckVal == equalityCheckVal) : + SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Type == SubmarineType.Player && s.MD5Hash.Hash == hash); sender.SetVote(voteType, sub); break; case VoteType.Mode: diff --git a/Barotrauma/BarotraumaServer/ServerSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaServer/ServerSource/Physics/PhysicsBody.cs index fc5d6e837..561192ab6 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Physics/PhysicsBody.cs @@ -18,7 +18,7 @@ namespace Barotrauma if (Math.Abs(FarseerBody.LinearVelocity.X) > MaxVel || Math.Abs(FarseerBody.LinearVelocity.Y) > MaxVel) { - DebugConsole.ThrowError("Item velocity out of range (" + FarseerBody.LinearVelocity + ")"); + DebugConsole.ThrowError($"Entity velocity out of range ({(UserData?.ToString() ?? "null")}, {FarseerBody.LinearVelocity})"); } #endif diff --git a/Barotrauma/BarotraumaServer/ServerSource/Program.cs b/Barotrauma/BarotraumaServer/ServerSource/Program.cs index 5be940ee3..ba8bb55fe 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Program.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Program.cs @@ -42,6 +42,14 @@ namespace Barotrauma #endif Console.WriteLine("Barotrauma Dedicated Server " + GameMain.Version + " (" + AssemblyInfo.BuildString + ", branch " + AssemblyInfo.GitBranch + ", revision " + AssemblyInfo.GitRevision + ")"); + if(Console.IsOutputRedirected) + { + Console.WriteLine("Output redirection detected; colored text and command input will be disabled."); + } + if(Console.IsInputRedirected) + { + Console.WriteLine("Redirected input is detected but is not supported by this application. Input will be ignored."); + } string executableDir = Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location); Directory.SetCurrentDirectory(executableDir); @@ -152,7 +160,11 @@ namespace Barotrauma } string crashReport = sb.ToString(); - Console.ForegroundColor = ConsoleColor.Red; + + if (!Console.IsOutputRedirected) + { + Console.ForegroundColor = ConsoleColor.Red; + } Console.Write(crashReport); File.WriteAllText(filePath,sb.ToString()); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs index 6be934070..c24ba4ba1 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs @@ -11,6 +11,8 @@ namespace Barotrauma private SubmarineInfo selectedSub; private SubmarineInfo selectedShuttle; + public bool RadiationEnabled = true; + public SubmarineInfo SelectedSub { get { return selectedSub; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs index 49e44f615..d35216f45 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs @@ -141,7 +141,7 @@ namespace Barotrauma { return; } - if (GameMain.GameSession.Mission is CombatMission) + if (GameMain.GameSession.Missions.Any(m => m is CombatMission)) { var teamIds = new[] { CharacterTeamType.Team1, CharacterTeamType.Team2 }; foreach (var teamId in teamIds) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs index f340ab805..1e16d660f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs @@ -249,7 +249,7 @@ namespace Barotrauma { return; } - if (Traitors.Values.Any(traitor => traitor.Character?.IsDead ?? true || traitor.Character.Removed)) + if (Traitors.Values.Any(traitor => traitor.Character == null || traitor.Character.IsDead || traitor.Character.Removed)) { Traitors.Values.ForEach(traitor => traitor.UpdateCurrentObjective("", Identifier)); pendingObjectives.Clear(); diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index d81ec69ab..b3f2ff36d 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.12.0.2 + 0.13.0.11 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml index b90e7b764..47ab60408 100644 --- a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml +++ b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml @@ -62,6 +62,7 @@ + @@ -81,6 +82,7 @@ + @@ -98,6 +100,7 @@ + @@ -156,10 +159,9 @@ - + - - + @@ -167,23 +169,16 @@ - - - - - - - @@ -217,6 +212,7 @@ + @@ -228,6 +224,8 @@ + + @@ -241,5 +239,17 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/CameraTransition.cs b/Barotrauma/BarotraumaShared/SharedSource/CameraTransition.cs index 1a578ebf4..234ae434e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/CameraTransition.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/CameraTransition.cs @@ -1,5 +1,6 @@ using Microsoft.Xna.Framework; using NLog.Targets; +using System; using System.Collections.Generic; using System.Linq; @@ -18,8 +19,11 @@ namespace Barotrauma private readonly Alignment? cameraEndPos; private readonly float? startZoom; private readonly float? endZoom; - public readonly float Duration; + + public readonly float WaitDuration; + public readonly float PanDuration; public readonly bool FadeOut; + public readonly bool LosFadeIn; private readonly CoroutineHandle updateCoroutine; @@ -28,10 +32,12 @@ namespace Barotrauma public bool AllowInterrupt = false; public bool RemoveControlFromCharacter = true; - public CameraTransition(ISpatialEntity targetEntity, Camera cam, Alignment? cameraStartPos, Alignment? cameraEndPos, bool fadeOut = true, float duration = 10.0f, float? startZoom = null, float? endZoom = null) + public CameraTransition(ISpatialEntity targetEntity, Camera cam, Alignment? cameraStartPos, Alignment? cameraEndPos, bool fadeOut = true, bool losFadeIn = false, float waitDuration = 0f, float panDuration = 10.0f, float? startZoom = null, float? endZoom = null) { - Duration = duration; + WaitDuration = waitDuration; + PanDuration = panDuration; FadeOut = fadeOut; + LosFadeIn = losFadeIn; this.cameraStartPos = cameraStartPos; this.cameraEndPos = cameraEndPos; this.startZoom = startZoom; @@ -77,9 +83,12 @@ namespace Barotrauma Vector2 initialCameraPos = cam.Position; Vector2? initialTargetPos = targetEntity?.WorldPosition; - float timer = 0.0f; - while (timer < Duration) + float timer = -WaitDuration; + + while (timer < PanDuration) { + float clampedTimer = Math.Max(timer, 0f); + if (Screen.Selected != GameMain.GameScreen) { yield return new WaitForSeconds(0.1f); @@ -136,14 +145,20 @@ namespace Barotrauma MathHelper.Lerp(maxPos.Y, minPos.Y, (cameraEndPos.Value.ToVector2().Y + 1.0f) / 2.0f)) : prevControlled?.WorldPosition ?? targetEntity.WorldPosition; - Vector2 cameraPos = Vector2.SmoothStep(startPos, endPos, timer / Duration); + Vector2 cameraPos = Vector2.SmoothStep(startPos, endPos, clampedTimer / PanDuration); cam.Translate(cameraPos - cam.Position); #if CLIENT - cam.Zoom = MathHelper.SmoothStep(startZoom, endZoom, timer / Duration); - if (timer / Duration > 0.9f) + cam.Zoom = MathHelper.SmoothStep(startZoom, endZoom, clampedTimer / PanDuration); + if (clampedTimer / PanDuration > 0.9f) { - if (FadeOut) { GUI.ScreenOverlayColor = Color.Lerp(Color.TransparentBlack, Color.Black, ((timer / Duration) - 0.9f) * 10.0f); } + if (FadeOut) { GUI.ScreenOverlayColor = Color.Lerp(Color.TransparentBlack, Color.Black, ((clampedTimer / PanDuration) - 0.9f) * 10.0f); } + } + if (LosFadeIn && clampedTimer / PanDuration > 0.8f) + { + GameMain.LightManager.LosAlpha = ((clampedTimer / PanDuration) - 0.8f) * 5.0f; + Lights.LightManager.ViewTarget = prevControlled ?? (targetEntity as Entity); + GameMain.LightManager.LosEnabled = true; } #endif timer += CoroutineManager.UnscaledDeltaTime; @@ -158,6 +173,7 @@ namespace Barotrauma #if CLIENT GUI.ScreenOverlayColor = Color.TransparentBlack; GameMain.LightManager.LosEnabled = true; + GameMain.LightManager.LosAlpha = 1f; #endif if (prevControlled != null && !prevControlled.Removed) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs index bf34bd69a..b4624d8ad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs @@ -265,6 +265,37 @@ namespace Barotrauma } } + public void UnequipEmptyItems(Item parentItem, bool avoidDroppingInSea = true) => UnequipEmptyItems(Character, parentItem, avoidDroppingInSea); + + public void UnequipContainedItems(Item parentItem, Func predicate = null, bool avoidDroppingInSea = true) => UnequipContainedItems(Character, parentItem, predicate, avoidDroppingInSea); + + public static void UnequipEmptyItems(Character character, Item parentItem, bool avoidDroppingInSea = true) => UnequipContainedItems(character, parentItem, it => it.Condition <= 0, avoidDroppingInSea); + + public static void UnequipContainedItems(Character character, Item parentItem, Func predicate, bool avoidDroppingInSea = true) + { + var inventory = parentItem.OwnInventory; + if (inventory == null) { return; } + if (predicate == null || inventory.AllItems.Any(predicate)) + { + foreach (Item containedItem in inventory.AllItemsMod) + { + if (containedItem == null) { continue; } + if (predicate == null || predicate(containedItem)) + { + if (character.Submarine != Submarine.MainSub && avoidDroppingInSea) + { + // If we are outside of main sub, try to put the item in the inventory instead dropping it in the sea. + if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.anySlot)) + { + continue; + } + } + containedItem.Drop(character); + } + } + } + } + public void ReequipUnequipped() { foreach (var item in unequippedItems) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs index 0bcee48f4..5de1618ac 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs @@ -232,8 +232,7 @@ namespace Barotrauma public bool IsWithinSector(Vector2 worldPosition) { - if (sectorRad >= MathHelper.TwoPi) return true; - + if (sectorRad >= MathHelper.TwoPi) { return true; } Vector2 diff = worldPosition - WorldPosition; return MathUtils.GetShortestAngle(MathUtils.VectorToAngle(diff), MathUtils.VectorToAngle(sectorDir)) <= sectorRad * 0.5f; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 250c2f770..dc73b621c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -12,6 +12,10 @@ namespace Barotrauma { public enum AIState { Idle, Attack, Escape, Eat, Flee, Avoid, Aggressive, PassiveAggressive, Protect, Observe, Freeze, Follow } + public enum AttackPattern { Straight, Sweep, Circle } + + public enum CirclePhase { Start, CloseIn, FallBack, Advance, Strike } + partial class EnemyAIController : AIController { public static bool DisableEnemyAI; @@ -49,6 +53,7 @@ namespace Barotrauma private float updateMemoriesTimer; private float attackLimbResetTimer; + private bool IsAttackRunning => AttackingLimb != null && AttackingLimb.attack.IsRunning; private bool IsCoolDownRunning => AttackingLimb != null && AttackingLimb.attack.CoolDownTimer > 0; public float CombatStrength => AIParams.CombatStrength; private float Sight => AIParams.Sight; @@ -71,6 +76,23 @@ namespace Barotrauma Reverse = _attackingLimb != null && _attackingLimb.attack.Reverse; } } + + private double lastAttackUpdateTime; + + private Attack _activeAttack; + public Attack ActiveAttack + { + get + { + if (_activeAttack == null) { return null; } + return lastAttackUpdateTime > Timing.TotalTime - _activeAttack.Duration ? _activeAttack : null; + } + private set + { + _activeAttack = value; + lastAttackUpdateTime = Timing.TotalTime; + } + } private AITargetMemory selectedTargetMemory; private float targetValue; @@ -91,8 +113,17 @@ namespace Barotrauma private float avoidTimer; private float observeTimer; private float sweepTimer; - - public bool StayInsideLevel = true; + private float circleRotation; + private float circleDir; + private bool inverseDir; + private bool breakCircling; + private float circleRotationSpeed; + private Vector2 circleOffset; + private float circleFallbackDistance; + private float strikeTimer; + private float aggressionIntensity; + private CirclePhase CirclePhase; + private float currentAttackIntensity; private readonly IEnumerable myBodies; @@ -141,9 +172,21 @@ namespace Barotrauma } } + /// + /// The monster won't try to damage these submarines + /// + public HashSet UnattackableSubmarines + { + get; + private set; + } = new HashSet(); + + public bool IsTargetingPlayerTeam => IsTargetInPlayerTeam(SelectedAiTarget); public bool IsBeingChasedBy(Character c) => c.AIController is EnemyAIController enemyAI && enemyAI.SelectedAiTarget?.Entity is Character && (enemyAI.State == AIState.Aggressive || enemyAI.State == AIState.Attack); private bool IsBeingChased => SelectedAiTarget?.Entity is Character targetCharacter && IsBeingChasedBy(targetCharacter); + private bool IsTargetInPlayerTeam(AITarget target) => target?.Entity?.Submarine != null && target.Entity.Submarine.Info.IsPlayer || target?.Entity is Character targetCharacter && targetCharacter.IsOnPlayerTeam; + private bool reverse; public bool Reverse { @@ -241,7 +284,7 @@ namespace Barotrauma colliderLength = size.Y; requiredHoleCount = (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderWidth) / Structure.WallSectionSize); - avoidLookAheadDistance = Math.Max(colliderWidth * 3, 1.5f); + avoidLookAheadDistance = Math.Max(Math.Max(colliderWidth, colliderLength) * 3, 1.5f); myBodies = Character.AnimController.Limbs.Select(l => l.body.FarseerBody); } @@ -267,7 +310,7 @@ namespace Barotrauma private CharacterParams.TargetParams GetTargetParams(AITarget aiTarget) => GetTargetParams(GetTargetingTag(aiTarget)); private string GetTargetingTag(AITarget aiTarget) { - if (aiTarget.Entity == null) { return null; } + if (aiTarget?.Entity == null) { return null; } string targetingTag = null; if (aiTarget.Entity is Character targetCharacter) { @@ -346,6 +389,7 @@ namespace Barotrauma { if (DisableEnemyAI) { return; } base.Update(deltaTime); + UpdateTriggers(deltaTime); bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X)); if (steeringManager == insideSteering) @@ -431,7 +475,7 @@ namespace Barotrauma { updateTargetsTimer -= deltaTime; } - else if (avoidTimer <= 0) + else if (avoidTimer <= 0 || activeTriggers.Any() && returnTimer <= 0) { CharacterParams.TargetParams targetingParams = null; UpdateTargets(Character, out targetingParams); @@ -583,7 +627,7 @@ namespace Barotrauma if (c.IsDead || c.Removed) { return false; } if (!IsFriendly(Character, c)) { return true; } // Only apply the threshold to friendly characters - return a.Damage >= selectedTargetingParams.Threshold; + return a.Damage >= selectedTargetingParams.DamageThreshold; } Character attacker = targetCharacter.LastAttackers.LastOrDefault(IsValid)?.Character; if (attacker != null) @@ -721,7 +765,7 @@ namespace Barotrauma { var location = memory.Location; float dist = Vector2.DistanceSquared(WorldPosition, location); - if (dist < 50 * 50) + if (dist < 50 * 50 || !IsPositionInsideAllowedZone(WorldPosition, out _)) { // Target is gone ResetAITarget(); @@ -1305,7 +1349,7 @@ namespace Barotrauma } } } - else if (!IsCoolDownRunning) + else if (!IsAttackRunning && !IsCoolDownRunning) { // If not, reset the attacking limb, if the cooldown is not running // Don't use the property, because we don't want cancel reversing, if we are reversing. @@ -1393,27 +1437,221 @@ namespace Barotrauma } else { - if (selectedTargetingParams.SweepDistance > 0) + switch (selectedTargetingParams.AttackPattern) { - Vector2 toTarget = attackWorldPos - WorldPosition; - if (distance <= 0) - { - distance = toTarget.Length(); - } - float amplitude = MathHelper.Lerp(0, selectedTargetingParams.SweepStrength, MathUtils.InverseLerp(selectedTargetingParams.SweepDistance, 0, distance)); - if (amplitude > 0) - { - sweepTimer += deltaTime * selectedTargetingParams.SweepSpeed; - float sin = (float)Math.Sin(sweepTimer) * amplitude; - steerPos = MathUtils.RotatePointAroundTarget(attackSimPos, SimPosition, MathHelper.ToDegrees(sin)); - } - else - { - sweepTimer = Rand.Range(-1000, 1000) * selectedTargetingParams.SweepSpeed; - } + case AttackPattern.Sweep: + if (selectedTargetingParams.SweepDistance > 0) + { + if (distance <= 0) + { + distance = (attackWorldPos - WorldPosition).Length(); + } + float amplitude = MathHelper.Lerp(0, selectedTargetingParams.SweepStrength, MathUtils.InverseLerp(selectedTargetingParams.SweepDistance, 0, distance)); + if (amplitude > 0) + { + sweepTimer += deltaTime * selectedTargetingParams.SweepSpeed; + float sin = (float)Math.Sin(sweepTimer) * amplitude; + steerPos = MathUtils.RotatePointAroundTarget(attackSimPos, SimPosition, sin); + } + else + { + sweepTimer = Rand.Range(-1000, 1000) * selectedTargetingParams.SweepSpeed; + } + } + break; + case AttackPattern.Circle: + if (IsCoolDownRunning) { break; } + if (IsAttackRunning && CirclePhase != CirclePhase.Strike) { break; } + if (selectedTargetingParams == null) { break; } + var targetSub = SelectedAiTarget.Entity?.Submarine; + if (targetSub == null) { break; } + float subSize = Math.Max(targetSub.Borders.Width, targetSub.Borders.Height) / 2; + float sqrDistToSub = Vector2.DistanceSquared(WorldPosition, targetSub.WorldPosition); + switch (CirclePhase) + { + case CirclePhase.Start: + currentAttackIntensity = MathUtils.InverseLerp(AIParams.StartAggression, AIParams.MaxAggression, aggressionIntensity * Rand.Range(0.9f, 1.1f)); + inverseDir = false; + circleDir = GetDirFromHeadingInRadius(); + circleRotation = 0; + strikeTimer = 0; + blockCheckTimer = 0; + breakCircling = false; + float minRotationSpeed = 0.01f * selectedTargetingParams.CircleRotationSpeed; + float maxRotationSpeed = 0.5f * selectedTargetingParams.CircleRotationSpeed; + float minFallBackDistance = selectedTargetingParams.CircleStartDistance * 0.5f; + float maxFallBackDistance = selectedTargetingParams.CircleStartDistance; + // The lower the rotation speed, the slower the progression. Also the distance to the target stays longer. + // So basically if the value is higher, the creature will strike the sub more quickly and with more precision. + circleRotationSpeed = MathHelper.Lerp(minRotationSpeed, maxRotationSpeed, currentAttackIntensity * Rand.Range(0.9f, 1.1f)); + circleFallbackDistance = MathHelper.Lerp(maxFallBackDistance, minFallBackDistance, currentAttackIntensity * Rand.Range(0.9f, 1.1f)); + circleOffset = Rand.Vector(MathHelper.Lerp(selectedTargetingParams.CircleMaxRandomOffset, 0, currentAttackIntensity * Rand.Range(0.9f, 1.1f))); + canAttack = false; + aggressionIntensity = Math.Clamp(aggressionIntensity, AIParams.StartAggression, AIParams.MaxAggression); + if (targetSub.Borders.Width < 1000) + { + breakCircling = true; + CirclePhase = CirclePhase.CloseIn; + } + else if (sqrDistToSub > MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance)) + { + CirclePhase = CirclePhase.CloseIn; + } + else if (sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance)) + { + CirclePhase = CirclePhase.FallBack; + } + else + { + CirclePhase = CirclePhase.Advance; + } + break; + case CirclePhase.CloseIn: + if (AttackingLimb != null && distance > 0 && distance < AttackingLimb.attack.Range * GetStrikeDistanceMultiplier(targetSub.Velocity)) + { + strikeTimer = AttackingLimb.attack.CoolDown; + CirclePhase = CirclePhase.Strike; + } + else if (!breakCircling && sqrDistToSub <= MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance / 2) && targetSub.Velocity.LengthSquared() <= MathUtils.Pow2(GetTargetMaxSpeed())) + { + CirclePhase = CirclePhase.Advance; + } + canAttack = false; + break; + case CirclePhase.FallBack: + bool isBlocked = !UpdateFallBack(attackWorldPos, deltaTime, followThrough: false, checkBlocking: true); + if (isBlocked || sqrDistToSub > MathUtils.Pow2(subSize + circleFallbackDistance)) + { + CirclePhase = CirclePhase.Advance; + break; + } + return; + case CirclePhase.Advance: + Vector2 subSpeed = targetSub.Velocity; + float requiredDistMultiplier = 1; + // If the target sub is moving fast, just steer towards the target until close enough to strike + if (breakCircling || subSpeed.LengthSquared() > MathUtils.Pow2(GetTargetMaxSpeed()) || sqrDistToSub > MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance * 1.2f)) + { + CirclePhase = CirclePhase.CloseIn; + } + else + { + circleRotation += deltaTime * circleRotationSpeed * circleDir; + if (circleRotation < -360) + { + circleRotation += 360; + } + else if (circleRotation > 360) + { + circleRotation -= 360; + } + Vector2 targetPos = attackSimPos + circleOffset; + if (Vector2.DistanceSquared(SimPosition, targetPos) < 100) + { + // Too close to the target point + // When the offset position is outside of the sub it happens that the creature sometimes reaches the target point, + // which makes it continue circling around the point (as supposed) + // But when there is some offset and the offset is too near, this is not what we want. + if (AttackingLimb != null && sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance)) + { + CirclePhase = CirclePhase.Strike; + strikeTimer = AttackingLimb.attack.CoolDown; + } + else + { + CirclePhase = CirclePhase.Start; + } + break; + } + steerPos = MathUtils.RotatePointAroundTarget(SimPosition, targetPos, circleRotation); + requiredDistMultiplier = GetStrikeDistanceMultiplier(subSpeed); + if (IsBlocked(deltaTime, steerPos)) + { + if (!inverseDir) + { + // First try changing the direction + circleDir = -circleDir; + inverseDir = true; + } + else if (circleRotationSpeed < 1) + { + // Then try increasing the rotation speed to change the movement curve + circleRotationSpeed *= 1.1f; + } + else if (circleOffset.LengthSquared() > 0.1f) + { + // Then try removing the offset + circleOffset = Vector2.Zero; + } + else + { + // If we still fail, just steer towards the target + breakCircling = true; + } + } + } + if (AttackingLimb != null && distance > 0 && distance < AttackingLimb.attack.Range * requiredDistMultiplier && IsFacing(margin: MathHelper.Lerp(0.5f, 0.9f, currentAttackIntensity))) + { + strikeTimer = AttackingLimb.attack.CoolDown; + CirclePhase = CirclePhase.Strike; + } + canAttack = false; + break; + case CirclePhase.Strike: + strikeTimer -= deltaTime; + // just continue the movement forward to make it possible to evade the attack + steerPos = SimPosition + Steering; + if (strikeTimer <= 0) + { + CirclePhase = CirclePhase.Start; + aggressionIntensity += AIParams.AggressionCumulation; + } + break; + } + break; + + bool IsFacing(float margin) + { + float offset = steeringLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2; + Vector2 forward = VectorExtensions.Forward(steeringLimb.body.TransformedRotation - offset * Character.AnimController.Dir); + return Vector2.Dot(Vector2.Normalize(attackWorldPos - WorldPosition), forward) > margin; + } + + float GetStrikeDistanceMultiplier(Vector2 subSpeed) + { + float requiredDistMultiplier = 2; + bool isHeading = Steering != null && Vector2.Dot(Vector2.Normalize(attackWorldPos - WorldPosition), Vector2.Normalize(Steering)) > 0.9f; + if (isHeading) + { + requiredDistMultiplier = selectedTargetingParams.CircleStrikeDistanceMultiplier; + float subSpeedHorizontal = Math.Abs(subSpeed.X); + if (subSpeedHorizontal > 1) + { + // Reduce the required distance if the target is moving. + requiredDistMultiplier -= MathHelper.Lerp(0, Math.Max(selectedTargetingParams.CircleStrikeDistanceMultiplier - 1, 1), Math.Clamp(subSpeedHorizontal / 10, 0, 1)); + if (requiredDistMultiplier < 2) + { + requiredDistMultiplier = 2; + } + } + } + return requiredDistMultiplier; + } + + float GetDirFromHeadingInRadius() + { + Vector2 heading = VectorExtensions.Forward(Character.AnimController.Collider.Rotation); + float angle = MathUtils.VectorToAngle(heading); + return angle > MathHelper.Pi || angle < -MathHelper.Pi ? -1 : 1; + } + + float GetTargetMaxSpeed() => Character.ApplyTemporarySpeedLimits(Character.AnimController.CurrentSwimParams.MovementSpeed * 0.3f); } SteeringManager.SteeringSeek(steerPos, 10); - SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15); + if (SelectedAiTarget?.Entity is Character || distance == 0 || distance > ConvertUnits.ToDisplayUnits(avoidLookAheadDistance * 2)) + { + SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 30); + } } } if (canAttack) @@ -1433,6 +1671,10 @@ namespace Barotrauma IgnoreTarget(SelectedAiTarget); } } + else if (IsAttackRunning) + { + AttackingLimb.attack.ResetAttackTimer(); + } } private readonly List attackLimbs = new List(); @@ -1596,9 +1838,9 @@ namespace Barotrauma bool retaliate = !isFriendly && SelectedAiTarget != attacker.AiTarget && attacker.Submarine == Character.Submarine; bool avoidGunFire = AIParams.AvoidGunfire && attacker.Submarine != Character.Submarine; - if (State == AIState.Attack && !IsCoolDownRunning) + if (State == AIState.Attack && !IsAttackRunning && !IsCoolDownRunning) { - // Don't retaliate or escape while performing an attack + // Don't retaliate or escape while performing an attack/under cooldown retaliate = false; avoidGunFire = false; } @@ -1633,6 +1875,9 @@ namespace Barotrauma private bool UpdateLimbAttack(float deltaTime, Limb attackingLimb, Vector2 attackSimPos, float distance = -1, Limb targetLimb = null) { if (SelectedAiTarget?.Entity == null) { return false; } + + ActiveAttack = attackingLimb?.attack; + if (wallTarget != null) { // If the selected target is not the wall target, make the wall target the selected target. @@ -1665,8 +1910,22 @@ namespace Barotrauma return false; } + private readonly float blockCheckInterval = 0.1f; + private float blockCheckTimer; + private bool isBlocked; + private bool IsBlocked(float deltaTime, Vector2 steerPos, Category collisionCategory = Physics.CollisionLevel) + { + blockCheckTimer -= deltaTime; + if (blockCheckTimer <= 0) + { + blockCheckTimer = blockCheckInterval; + isBlocked = Submarine.PickBodies(SimPosition, steerPos, collisionCategory: collisionCategory).Any(); + } + return isBlocked; + } + private Vector2? attackVector = null; - private void UpdateFallBack(Vector2 attackWorldPos, float deltaTime, bool followThrough) + private bool UpdateFallBack(Vector2 attackWorldPos, float deltaTime, bool followThrough, bool checkBlocking = false) { if (attackVector == null) { @@ -1683,6 +1942,11 @@ namespace Barotrauma { SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15); } + if (checkBlocking) + { + return !IsBlocked(deltaTime, SimPosition + attackDir * (avoidLookAheadDistance / 2)); + } + return true; } #endregion @@ -1816,6 +2080,7 @@ namespace Barotrauma targetValue = 0; selectedTargetMemory = null; targetingParams = null; + bool isAnyTargetClose = false; foreach (AITarget aiTarget in AITarget.List) { @@ -1896,10 +2161,21 @@ namespace Barotrauma } else { - // Ignore all structures and items inside wrecks - if (aiTarget.Entity.Submarine != null && aiTarget.Entity.Submarine.Info.IsWreck) { continue; } - // Ignore the target if it's a room and the character is already inside a sub - if (character.CurrentHull != null && aiTarget.Entity is Hull) { continue; } + // Ignore all structures, items, and hulls inside wrecks and beacons + if (aiTarget.Entity.Submarine != null) + { + if (aiTarget.Entity.Submarine.Info.IsWreck || aiTarget.Entity.Submarine.Info.IsBeacon || UnattackableSubmarines.Contains(aiTarget.Entity.Submarine)) + { + continue; + } + } + if (aiTarget.Entity is Hull hull) + { + // Ignore the target if it's a room and the character is already inside a sub + if (character.CurrentHull != null) { continue; } + // Ignore ruins + if (hull.Submarine == null) { continue; } + } Door door = null; if (aiTarget.Entity is Item item) @@ -1914,6 +2190,14 @@ namespace Barotrauma continue; } } + if (door == null) + { + // Ignore items inside ruins, unless we are in the same hull. We can't target the ruin walls. + if (item.Submarine == null && item.CurrentHull != Character.CurrentHull) + { + continue; + } + } foreach (var prio in AIParams.Targets) { if (item.HasTag(prio.Tag)) @@ -2092,11 +2376,17 @@ namespace Barotrauma if (targetParams.IgnoreInside && character.CurrentHull != null) { continue; } if (targetParams.IgnoreOutside && character.CurrentHull == null) { continue; } if (targetParams.IgnoreIncapacitated && targetCharacter != null && targetCharacter.IsIncapacitated) { continue; } + if (targetParams.IgnoreIfNotInSameSub) + { + if (aiTarget.Entity.Submarine != Character.Submarine) { continue; } + var targetHull = targetCharacter != null ? targetCharacter.CurrentHull : aiTarget.Entity is Item it ? it.CurrentHull : null; + if ((targetHull == null) != (character.CurrentHull == null)) { continue; } + } if (targetParams.State == AIState.Observe || targetParams.State == AIState.Eat) { if (targetCharacter != null && targetCharacter.Submarine != Character.Submarine) { - // Don't allow to target characters that are inside a different submarine / outside when we are inside. + // Never allow observing or eating characters that are inside a different submarine / outside when we are inside. continue; } } @@ -2129,18 +2419,16 @@ namespace Barotrauma } } } - + if (!aiTarget.IsWithinSector(WorldPosition)) { continue; } Vector2 toTarget = aiTarget.WorldPosition - character.WorldPosition; float dist = toTarget.Length(); - + float nonModifiedDist = dist; //if the target has been within range earlier, the character will notice it more easily if (targetMemories.ContainsKey(aiTarget)) { dist *= 0.9f; } - if (!CanPerceive(aiTarget, dist)) { continue; } - if (!aiTarget.IsWithinSector(WorldPosition)) { continue; } //if the target is very close, the distance doesn't make much difference // -> just ignore the distance and attack whatever has the highest priority @@ -2152,6 +2440,48 @@ namespace Barotrauma // Inside the sub, treat objects that are up or down, as they were farther away. dist *= 3; } + + if (targetParams.AttackPattern == AttackPattern.Circle) + { + if (Character.Submarine == null && aiTarget.Entity?.Submarine != null && !isAnyTargetClose) + { + if (Submarine.MainSubs.Contains(aiTarget.Entity.Submarine)) + { + // Prioritize targets that are near the horizontal center of the sub, but only when none of the targets is reachable. + float horizontalDistanceToSubCenter = Math.Abs(aiTarget.WorldPosition.X - aiTarget.Entity.Submarine.WorldPosition.X); + dist *= MathHelper.Lerp(1f, 5f, MathUtils.InverseLerp(0, 10000, horizontalDistanceToSubCenter)); + } + else + { + dist *= 5; + } + } + } + + // Don't target characters that are outside of the allowed zone, unless chasing or escaping. + switch (targetParams.State) + { + case AIState.Escape: + case AIState.Avoid: + break; + default: + if (targetParams.State == AIState.Attack) + { + // In the attack state allow going into non-allowed zone only when chasing a target. + if (State == targetParams.State && SelectedAiTarget == aiTarget) { break; } + } + if (!IsPositionInsideAllowedZone(aiTarget.WorldPosition, out _)) + { + // If we have recently been damaged by the target (or another player/bot in the same team) allow targeting it even when we are in the idle state. + bool isTargetInPlayerTeam = IsTargetInPlayerTeam(aiTarget); + if (Character.LastAttackers.None(a => a.Damage > 0 && a.Character != null && (a.Character == aiTarget.Entity || a.Character.IsOnPlayerTeam && isTargetInPlayerTeam))) + { + continue; + } + } + break; + } + valueModifier *= targetMemory.Priority / (float)Math.Sqrt(dist); if (valueModifier > targetValue) @@ -2181,7 +2511,7 @@ namespace Barotrauma } } } - if (targetCharacter.Submarine != Character.Submarine) + if (targetCharacter.Submarine != Character.Submarine || (targetCharacter.CurrentHull == null) != (Character.CurrentHull == null)) { if (targetCharacter.Submarine != null) { @@ -2195,30 +2525,21 @@ namespace Barotrauma } else if (Character.CurrentHull != null) { - // Target outside, but we are inside -> Check if we can get to the target. - // Only check if we are not already targeting the character. - // If we are, keep the target (unless we choose another). + // Target outside, but we are inside -> Ignore the target but allow to keep target that is currently selected. if (SelectedAiTarget?.Entity != targetCharacter) { - foreach (var gap in Character.CurrentHull.ConnectedGaps) - { - var door = gap.ConnectedDoor; - if (door == null) - { - var wall = gap.ConnectedWall; - if (wall != null) - { - for (int j = 0; j < wall.Sections.Length; j++) - { - WallSection section = wall.Sections[j]; - if (!CanPassThroughHole(wall, j) && section?.gap != null) - { - continue; - } - } - } - } - } + continue; + } + } + } + else if (targetCharacter.Submarine == null && Character.Submarine == null) + { + // Ignore the target when it's far enough and blocked by the level geometry, because the steering avoidance probably can't get us to the target. + if (dist > Math.Clamp(ConvertUnits.ToDisplayUnits(colliderLength) * 10, 1000, 5000)) + { + if (Submarine.PickBodies(SimPosition, targetCharacter.SimPosition, collisionCategory: Physics.CollisionLevel).Any()) + { + continue; } } } @@ -2227,6 +2548,10 @@ namespace Barotrauma selectedTargetMemory = targetMemory; targetValue = valueModifier; targetingParams = targetParams; + if (!isAnyTargetClose) + { + isAnyTargetClose = ConvertUnits.ToDisplayUnits(colliderLength) > nonModifiedDist; + } } } @@ -2355,12 +2680,12 @@ namespace Barotrauma } } } - if (!Character.AnimController.CanEnterSubmarine && wallTarget == null) + if (!Character.AnimController.CanEnterSubmarine && wallTarget == null && selectedTargetingParams?.AttackPattern == AttackPattern.Straight) { - if (closestBody.UserData is Structure w && w.Submarine != null || closestBody.UserData is Item i && i.Submarine != null) + if (closestBody.UserData is Structure w && w.Submarine != null && w.Submarine == SelectedAiTarget.Entity?.Submarine || + closestBody.UserData is Item i && i.Submarine != null && i.Submarine == SelectedAiTarget.Entity?.Submarine) { // Cannot reach the target, because it's blocked by a disabled wall or a door - State = AIState.Idle; IgnoreTarget(SelectedAiTarget); ResetAITarget(); } @@ -2489,6 +2814,44 @@ namespace Barotrauma private readonly float stateResetCooldown = 10; private float stateResetTimer; private bool isStateChanged; + private readonly Dictionary activeTriggers = new Dictionary(); + private readonly HashSet inactiveTriggers = new HashSet(); + + public void LaunchTrigger(AITrigger trigger) + { + if (trigger.IsTriggered) { return; } + if (activeTriggers.ContainsKey(trigger)) { return; } + if (activeTriggers.ContainsValue(selectedTargetingParams)) + { + if (!trigger.AllowToOverride) { return; } + var existingTrigger = activeTriggers.FirstOrDefault(kvp => kvp.Value == selectedTargetingParams && kvp.Key.AllowToBeOverridden); + if (existingTrigger.Key == null) { return; } + activeTriggers.Remove(existingTrigger.Key); + } + trigger.Launch(); + activeTriggers.Add(trigger, selectedTargetingParams); + ChangeParams(selectedTargetingParams, trigger.State); + } + + private void UpdateTriggers(float deltaTime) + { + foreach (var triggerObject in activeTriggers) + { + AITrigger trigger = triggerObject.Key; + trigger.UpdateTimer(deltaTime); + if (!trigger.IsActive) + { + trigger.Reset(); + ResetParams(triggerObject.Value); + inactiveTriggers.Add(trigger); + } + } + foreach (AITrigger trigger in inactiveTriggers) + { + activeTriggers.Remove(trigger); + } + inactiveTriggers.Clear(); + } /// /// Resets the target's state to the original value defined in the xml. @@ -2504,11 +2867,7 @@ namespace Barotrauma tempParams.Values.ForEach(t => AIParams.RemoveTarget(t)); tempParams.Remove(tag); } - targetParams.Reset(); - ResetAITarget(); - // Enforce the idle state so that we don't keep following the target if there's one - State = AIState.Idle; - PreviousState = AIState.Idle; + ResetParams(targetParams); return true; } else @@ -2520,6 +2879,27 @@ namespace Barotrauma private readonly Dictionary modifiedParams = new Dictionary(); private readonly Dictionary tempParams = new Dictionary(); + private void ChangeParams(CharacterParams.TargetParams targetParams, AIState state, float? priority = null) + { + if (targetParams == null) { return; } + if (priority.HasValue) + { + targetParams.Priority = priority.Value; + } + targetParams.State = state; + } + + private void ResetParams(CharacterParams.TargetParams targetParams) + { + targetParams?.Reset(); + if (selectedTargetingParams == targetParams || State == AIState.Idle) + { + ResetAITarget(); + State = AIState.Idle; + PreviousState = AIState.Idle; + } + } + private void ChangeParams(string tag, AIState state, float? priority = null, bool onlyExisting = false) { if (!AIParams.TryGetTarget(tag, out CharacterParams.TargetParams targetParams)) @@ -2622,6 +3002,7 @@ namespace Barotrauma { SetStateResetTimer(); } + blockCheckTimer = 0; } private void SetStateResetTimer() => stateResetTimer = stateResetCooldown * Rand.Range(0.75f, 1.25f); @@ -2673,37 +3054,64 @@ namespace Barotrauma } } + private bool IsPositionInsideAllowedZone(Vector2 pos, out Vector2 targetDir) + { + targetDir = Vector2.Zero; + if (Level.Loaded == null) { return true; } + if (AIParams.AvoidAbyss) + { + if (pos.Y < Level.Loaded.AbyssStart) + { + // Too far down + targetDir = Vector2.UnitY; + } + } + else if (AIParams.StayInAbyss) + { + if (pos.Y > Level.Loaded.AbyssStart) + { + // Too far up + targetDir = -Vector2.UnitY; + } + else if (pos.Y < Level.Loaded.AbyssEnd) + { + // Too far down + targetDir = Vector2.UnitY; + } + } + float margin = 30000; + if (pos.X < -margin) + { + // Too far left + targetDir = Vector2.UnitX; + } + else if (pos.X > Level.Loaded.Size.X + margin) + { + // Too far right + targetDir = -Vector2.UnitX; + } + return targetDir == Vector2.Zero; + } + private Vector2 returnDir; private float returnTimer; private void SteerInsideLevel(float deltaTime) { - if (SteeringManager is IndoorsSteeringManager || !StayInsideLevel) { return; } + if (SteeringManager is IndoorsSteeringManager) { return; } if (Level.Loaded == null) { return; } - Point levelSize = Level.Loaded.Size; - float returnTime = 10; - if (WorldPosition.Y < 0) + if (State == AIState.Attack && returnTimer <= 0) { return; } + float returnTime = 5; + if (!IsPositionInsideAllowedZone(WorldPosition, out Vector2 targetDir)) { - // Too far down + returnDir = targetDir; returnTimer = returnTime * Rand.Range(0.75f, 1.25f); - returnDir = Vector2.UnitY; - } - if (WorldPosition.X < 0) - { - // Too far left - returnTimer = returnTime * Rand.Range(0.75f, 1.25f); - returnDir = Vector2.UnitX; - } - if (WorldPosition.X > levelSize.X) - { - // Too far right - returnTimer = returnTime * Rand.Range(0.75f, 1.25f); - returnDir = -Vector2.UnitX; } if (returnTimer > 0) { returnTimer -= deltaTime; SteeringManager.Reset(); - SteeringManager.SteeringManual(deltaTime, returnDir * 2); + SteeringManager.SteeringManual(deltaTime, returnDir * 10); + SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, 15); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index bea255d4a..6ba9ae846 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -15,7 +15,7 @@ namespace Barotrauma private readonly AIObjectiveManager objectiveManager; - private float sortTimer; + public float SortTimer { get; set; } private float crouchRaycastTimer; private float reactTimer; private float unreachableClearTimer; @@ -52,6 +52,30 @@ namespace Barotrauma private readonly float obstacleRaycastInterval = 1; private float obstacleRaycastTimer; + private readonly float enemyCheckInterval = 0.2f; + private readonly float enemySpotDistanceOutside = 1500; + private readonly float enemySpotDistanceInside = 1000; + private float enemycheckTimer; + + /// + /// How far other characters can hear reports done by this character (e.g. reports for fires, intruders). Defaults to infinity. + /// + public float ReportRange { get; set; } = float.PositiveInfinity; + + private float _aimSpeed = 1; + public float AimSpeed + { + get { return _aimSpeed; } + set { _aimSpeed = Math.Max(value, 0.01f); } + } + + private float _aimAccuracy = 1; + public float AimAccuracy + { + get { return _aimAccuracy; } + set { _aimAccuracy = Math.Clamp(value, 0f, 1f); } + } + /// /// List of previous attacks done to this character /// @@ -64,18 +88,6 @@ namespace Barotrauma public AIObjectiveManager ObjectiveManager => objectiveManager; - public Order CurrentOrder - { - get; - private set; - } - - public string CurrentOrderOption - { - get; - private set; - } - public float CurrentHullSafety { get; private set; } = 100; private readonly Dictionary structureDamageAccumulator = new Dictionary(); @@ -119,12 +131,9 @@ namespace Barotrauma outsideSteering = new SteeringManager(this); objectiveManager = new AIObjectiveManager(c); reactTimer = GetReactionTime(); - sortTimer = Rand.Range(0f, sortObjectiveInterval); - InitProjSpecific(); + SortTimer = Rand.Range(0f, sortObjectiveInterval); } - partial void InitProjSpecific(); - public override void Update(float deltaTime) { if (DisableCrewAI || Character.Removed) { return; } @@ -171,23 +180,63 @@ namespace Barotrauma bool IsCloseEnoughToTargetSub(float threshold) => SelectedAiTarget?.Entity?.Submarine is Submarine sub && sub != null && Vector2.DistanceSquared(Character.WorldPosition, sub.WorldPosition) < MathUtils.Pow(Math.Max(sub.Borders.Size.X, sub.Borders.Size.Y) / 2 + threshold, 2); bool hasValidPath = HasValidPath(); - if (Character.Submarine == null && hasValidPath) + if (Character.Submarine == null) { - obstacleRaycastTimer -= deltaTime; - if (obstacleRaycastTimer <= 0) + if (hasValidPath) { - obstacleRaycastTimer = obstacleRaycastInterval; - // Swimming outside and using the path finder -> check that the path is not blocked with anything (the path finder doesn't know about other subs). - foreach (var connectedSub in Submarine.MainSub.GetConnectedSubs()) + obstacleRaycastTimer -= deltaTime; + if (obstacleRaycastTimer <= 0) { - if (connectedSub == Submarine.MainSub) { continue; } - Vector2 rayStart = SimPosition - connectedSub.SimPosition; - Vector2 dir = PathSteering.CurrentPath.CurrentNode.WorldPosition - WorldPosition; - Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 5); - if (Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true) != null) + obstacleRaycastTimer = obstacleRaycastInterval; + // Swimming outside and using the path finder -> check that the path is not blocked with anything (the path finder doesn't know about other subs). + foreach (var connectedSub in Submarine.MainSub.GetConnectedSubs()) { - PathSteering.CurrentPath.Unreachable = true; - break; + if (connectedSub == Submarine.MainSub) { continue; } + Vector2 rayStart = SimPosition - connectedSub.SimPosition; + Vector2 dir = PathSteering.CurrentPath.CurrentNode.WorldPosition - WorldPosition; + Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 5); + if (Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true) != null) + { + PathSteering.CurrentPath.Unreachable = true; + break; + } + } + } + } + } + if (Character.Submarine == null || !IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID)) + { + // Spot enemies while staying outside or inside an enemy ship. + enemycheckTimer -= deltaTime; + if (enemycheckTimer < 0) + { + enemycheckTimer = enemyCheckInterval * Rand.Range(0.75f, 1.25f); + if (!objectiveManager.IsCurrentObjective()) + { + float closestDistance = 0; + Character closestEnemy = null; + foreach (Character c in Character.CharacterList) + { + if (c.Submarine != Character.Submarine) { continue; } + if (c.Removed || c.IsDead || c.IsIncapacitated) { continue; } + if (IsFriendly(c)) { continue; } + Vector2 toTarget = c.WorldPosition - WorldPosition; + float dist = toTarget.LengthSquared(); + float maxDistance = Character.Submarine == null ? enemySpotDistanceOutside : enemySpotDistanceInside; + if (dist > maxDistance * maxDistance) { continue; } + Vector2 forward = VectorExtensions.Forward(Character.AnimController.Collider.Rotation); + forward.X *= Character.AnimController.Dir; + if (Vector2.Dot(toTarget, forward) < 0.2f) { continue; } + if (!Character.CanSeeCharacter(c)) { continue; } + if (dist < closestDistance || closestEnemy == null) + { + closestEnemy = c; + closestDistance = dist; + } + } + if (closestEnemy != null) + { + AddCombatObjective(AIObjectiveCombat.CombatMode.Defensive, closestEnemy); } } } @@ -216,14 +265,14 @@ namespace Barotrauma CheckCrouching(deltaTime); Character.ClearInputs(); - if (sortTimer > 0.0f) + if (SortTimer > 0.0f) { - sortTimer -= deltaTime; + SortTimer -= deltaTime; } else { objectiveManager.SortObjectives(); - sortTimer = sortObjectiveInterval; + SortTimer = sortObjectiveInterval; } objectiveManager.UpdateObjectives(deltaTime); @@ -240,14 +289,14 @@ namespace Barotrauma { if (Character.CurrentHull != null) { - if (Character.TeamID == CharacterTeamType.FriendlyNPC) + if (Character.IsOnPlayerTeam) { - // Outpost npcs don't inform each other about threads, like crew members do. - VisibleHulls.ForEach(h => RefreshHullSafety(h)); + VisibleHulls.ForEach(h => PropagateHullSafety(Character, h)); } else { - VisibleHulls.ForEach(h => PropagateHullSafety(Character, h)); + // Outpost npcs don't inform each other about threats, like crew members do. + VisibleHulls.ForEach(h => RefreshHullSafety(h)); } } if (Character.SpeechImpediment < 100.0f) @@ -367,9 +416,11 @@ namespace Barotrauma if (isCarrying) { - if (findItemState == FindItemState.DivingSuit && ObjectiveManager.IsCurrentObjective()) + if (findItemState != FindItemState.OtherItem) { - if (ObjectiveManager.GetActiveObjective() is AIObjectiveGoTo gotoObjective && NeedsDivingGearOnPath(gotoObjective)) + var decontain = ObjectiveManager.GetActiveObjectives().LastOrDefault(); + if (decontain != null && decontain.TargetItem != null && decontain.TargetItem.HasTag(AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR) && + ObjectiveManager.GetActiveObjective() is AIObjectiveGoTo gotoObjective && NeedsDivingGearOnPath(gotoObjective)) { // Don't try to put the diving suit in a locker if the suit would be needed in any hull in the path to the locker. gotoObjective.Abandon = true; @@ -384,14 +435,17 @@ namespace Barotrauma // Diving gear if (oxygenLow || findItemState != FindItemState.OtherItem) { - if (!NeedsDivingGear(Character.CurrentHull, out bool needsSuit) || !needsSuit || oxygenLow) + bool needsGear = NeedsDivingGear(Character.CurrentHull, out _); + if (!needsGear || oxygenLow) { - bool shouldKeepTheGearOn = Character.AnimController.HeadInWater - || Character.Submarine == null - || Character.Submarine.TeamID != Character.TeamID - || ObjectiveManager.IsCurrentObjective() - || ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character // wait order - || ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn); + bool shouldKeepTheGearOn = + Character.AnimController.InWater || + Character.AnimController.HeadInWater || + Character.CurrentHull == null || + Character.Submarine.TeamID != Character.TeamID || + ObjectiveManager.IsCurrentObjective() || + ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character || // wait order + ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn); if (oxygenLow && Character.CurrentHull.Oxygen > 0) { shouldKeepTheGearOn = false; @@ -717,17 +771,11 @@ namespace Barotrauma targetHull = hull; } } - foreach (var ballastFlora in MapCreatures.Behavior.BallastFloraBehavior.EntityList) + if (IsBallastFloraNoticeable(Character, hull)) { - if (ballastFlora.Parent?.Submarine != Character.Submarine) { continue; } - if (!ballastFlora.HasBrokenThrough) { continue; } - // Don't react to the first two branches, because they are usually in the very edges of the room. - if (ballastFlora.Branches.Count(b => !b.Removed && b.Health > 0 && b.CurrentHull == hull) > 2) - { - var orderPrefab = Order.GetPrefab("reportballastflora"); - newOrder = new Order(orderPrefab, hull, null, orderGiver: Character); - targetHull = hull; - } + var orderPrefab = Order.GetPrefab("reportballastflora"); + newOrder = new Order(orderPrefab, hull, null, orderGiver: Character); + targetHull = hull; } if (!isFighting) { @@ -784,16 +832,31 @@ namespace Barotrauma identifier: newOrder.Prefab.Identifier + (targetHull?.DisplayName ?? "null"), minDurationBetweenSimilar: 60.0f); } - else if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime)) + else if (Character.IsOnPlayerTeam && GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime)) { Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Order); #if SERVER - GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder, "", targetHull, null, Character)); + GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder, "", CharacterInfo.HighestManualOrderPriority, targetHull, null, Character)); #endif } } } + public static bool IsBallastFloraNoticeable(Character character, Hull hull) + { + foreach (var ballastFlora in MapCreatures.Behavior.BallastFloraBehavior.EntityList) + { + if (ballastFlora.Parent?.Submarine != character.Submarine) { continue; } + if (!ballastFlora.HasBrokenThrough) { continue; } + // Don't react to the first two branches, because they are usually in the very edges of the room. + if (ballastFlora.Branches.Count(b => !b.Removed && b.Health > 0 && b.CurrentHull == hull) > 2) + { + return true; + } + } + return false; + } + public static void ReportProblem(Character reporter, Order order) { if (reporter == null || order == null) { return; } @@ -807,6 +870,8 @@ namespace Barotrauma private void UpdateSpeaking() { + if (!Character.IsOnPlayerTeam) { return; } + if (Character.Oxygen < 20.0f) { Character.Speak(TextManager.Get("DialogLowOxygen"), null, Rand.Range(0.5f, 5.0f), "lowoxygen", 30.0f); @@ -885,7 +950,7 @@ namespace Barotrauma } if (attacker == null || attacker.IsDead || attacker.Removed) { - // Don't react on the damage if there's no attacker. + // Don't react to the damage if there's no attacker. // We might consider launching the retreat combat objective in some cases, so that the bot does not just stand somewhere getting damaged and dying. // But fires and enemies should already be handled by the FindSafetyObjective. return; @@ -893,12 +958,17 @@ namespace Barotrauma //if (Character.LastDamageSource == null) { return; } //AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, Rand.Range(0.5f, 1f, Rand.RandSync.Unsynced)); } - else if (realDamage <= 0 && (attacker.IsBot || attacker.TeamID == Character.TeamID)) + if (realDamage <= 0 && (attacker.IsBot || attacker.TeamID == Character.TeamID)) { - // Don't react on damage that is entirely based on karma penalties (medics, poisons etc), unless applier is player + // Don't react to damage that is entirely based on karma penalties (medics, poisons etc), unless applier is player return; } - else if (IsFriendly(attacker)) + if (attacker.Submarine == null && Character.Submarine != null) + { + // Don't react to attackers that are outside of the sub (e.g. AoE attacks) + return; + } + if (IsFriendly(attacker)) { if (attacker.AnimController.Anim == Barotrauma.AnimController.Animation.CPR && attacker.SelectedCharacter == Character) { @@ -911,7 +981,7 @@ namespace Barotrauma { if (cumulativeDamage > 1) { - // Don't retaliate on damage done by human ai, because we know it's accidental + // Don't retaliate on damage done by friendly NPC, because we know it's accidental AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker); } } @@ -921,49 +991,29 @@ namespace Barotrauma // Inform other NPCs if (cumulativeDamage > 1) { - foreach (Character otherCharacter in Character.CharacterList) - { - if (otherCharacter == Character || otherCharacter.IsDead || otherCharacter.IsUnconscious || otherCharacter.Removed || - otherCharacter.Info?.Job == null || otherCharacter.TeamID != CharacterTeamType.FriendlyNPC || - !(otherCharacter.AIController is HumanAIController otherHumanAI) || - otherCharacter.IsInstigator) - { - continue; - } - if (!otherHumanAI.IsFriendly(Character)) { continue; } - bool isWitnessing = otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) || otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull); - if (otherCharacter.IsSecurity) - { - // Alert all the security officers magically - float delay = isWitnessing ? GetReactionTime() * 2 : Rand.Range(2.0f, 5.0f, Rand.RandSync.Unsynced); - otherHumanAI.AddCombatObjective(DetermineCombatMode(otherCharacter, cumulativeDamage), attacker, delay); - } - else if (isWitnessing) - { - var mode = Character.CombatAction != null ? Character.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat; - // Other witnesses retreat to safety - otherHumanAI.AddCombatObjective(mode, attacker, GetReactionTime()); - } - } + InformOtherNPCs(cumulativeDamage); } if (Character.IsBot) { if (ObjectiveManager.CurrentObjective is AIObjectiveFightIntruders) { return; } - if (Character.IsSecurity) + if (attacker.IsPlayer) { - if (attacker.TeamID != Character.TeamID && cumulativeDamage > 1 || cumulativeDamage > 10) + if (Character.IsSecurity) { - Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityarrest"), null, 0.50f, "attackedbyfriendlysecurityarrest", minDurationBetweenSimilar: 30.0f); + if (attacker.TeamID != Character.TeamID && cumulativeDamage > 1 || cumulativeDamage > 10) + { + Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityarrest"), null, 0.50f, "attackedbyfriendlysecurityarrest", minDurationBetweenSimilar: 30.0f); + } + else + { + Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityresponse"), null, 0.50f, "attackedbyfriendlysecurityresponse", minDurationBetweenSimilar: 30.0f); + } } - else + else if (!Character.IsInstigator && cumulativeDamage > 1) { - Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityresponse"), null, 0.50f, "attackedbyfriendlysecurityresponse", minDurationBetweenSimilar: 30.0f); + Character.Speak(TextManager.Get("DialogAttackedByFriendly"), null, 0.50f, "attackedbyfriendly", minDurationBetweenSimilar: 30.0f); } } - else if (!Character.IsInstigator && cumulativeDamage > 1) - { - Character.Speak(TextManager.Get("DialogAttackedByFriendly"), null, 0.50f, "attackedbyfriendly", minDurationBetweenSimilar: 30.0f); - } if (cumulativeDamage > 1 && attacker.TeamID != Character.TeamID) { // If the attacker is using a low damage and high frequency weapon like a repair tool, we shouldn't use any delay. @@ -971,12 +1021,7 @@ namespace Barotrauma } else { - bool allowOffensive = HasItem(attacker, "handlocker", out _, requireEquipped: true); - if (attackResult.Afflictions.Any(a => a is AfflictionHusk)) - { - cumulativeDamage = 100; - } - // Don't react on minor (accidental) dmg done by characters that are in the same team + // Don't react to minor (accidental) dmg done by characters that are in the same team if (cumulativeDamage < 10) { if (!Character.IsSecurity && cumulativeDamage > 1) @@ -986,23 +1031,48 @@ namespace Barotrauma } else { - AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage, dmgThreshold: 20, allowOffensive: allowOffensive), attacker, GetReactionTime() * 2); + AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage, dmgThreshold: 50), attacker, GetReactionTime() * 2); } } } } } - else if (Character.IsBot) + else { // Non-friendly - AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage: realDamage), attacker); + InformOtherNPCs(GetDamageDoneByAttacker(attacker)); + if (Character.IsBot) + { + AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage: realDamage), attacker); + } } - AIObjectiveCombat.CombatMode DetermineCombatMode(Character c, float cumulativeDamage, float dmgThreshold = 10, bool allowOffensive = true) + void InformOtherNPCs(float cumulativeDamage) + { + foreach (Character otherCharacter in Character.CharacterList) + { + if (otherCharacter == Character || otherCharacter.IsDead || otherCharacter.IsUnconscious || otherCharacter.Removed) { continue; } + if (otherCharacter.Submarine != Character.Submarine) { continue; } + if (otherCharacter.Submarine != attacker.Submarine) { continue; } + if (otherCharacter.Info?.Job == null || otherCharacter.IsInstigator) { continue; } + if (otherCharacter.IsPlayer) { continue; } + if (!(otherCharacter.AIController is HumanAIController otherHumanAI)) { continue; } + if (!otherHumanAI.IsFriendly(Character)) { continue; } + bool isWitnessing = otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) || otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull); + if (!isWitnessing && !CheckReportRange(Character, otherCharacter, ReportRange)) { continue; } + var combatMode = DetermineCombatMode(otherCharacter, cumulativeDamage, isWitnessing, dmgThreshold: attacker.TeamID == Character.TeamID ? 50 : 10); + float delay = isWitnessing ? GetReactionTime() : Rand.Range(2.0f, 5.0f, Rand.RandSync.Unsynced); + otherHumanAI.AddCombatObjective(combatMode, attacker, delay); + } + } + + AIObjectiveCombat.CombatMode DetermineCombatMode(Character c, float cumulativeDamage, bool isWitnessing = false, float dmgThreshold = 10, bool allowOffensive = true) { if (!IsFriendly(attacker)) { - return c.IsSecurity ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Defensive; + return c.AIController is HumanAIController humanAI && + (humanAI.ObjectiveManager.IsCurrentOrder() || humanAI.ObjectiveManager.Objectives.Any(o => o is AIObjectiveFightIntruders)) + ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Defensive; } else { @@ -1011,7 +1081,11 @@ namespace Barotrauma { return AIObjectiveCombat.CombatMode.None; } - if (Character.IsInstigator && attacker.IsPlayer) + else if (isWitnessing && Character.CombatAction != null && !c.IsSecurity) + { + return Character.CombatAction.WitnessReaction; + } + else if (Character.IsInstigator && attacker.IsPlayer) { // The guards don't react when the player attacks instigators. return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : (Character.CombatAction != null ? Character.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat); @@ -1029,6 +1103,15 @@ namespace Barotrauma } else { + if (c.AIController is HumanAIController humanAI && humanAI.ObjectiveManager.GetActiveObjective()?.Enemy == attacker) + { + // Already targeting the attacker -> treat as a more serious threat. + cumulativeDamage *= 2; + } + if (attackResult.Afflictions.Any(a => a is AfflictionHusk)) + { + cumulativeDamage = 100; + } if (cumulativeDamage > dmgThreshold) { if (c.IsSecurity) @@ -1049,15 +1132,16 @@ namespace Barotrauma } } - private void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character attacker, float delay = 0, Func abortCondition = null, Action onAbort = null, Action onCompleted = null, bool allowHoldFire = false) + private void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character target, float delay = 0, Func abortCondition = null, Action onAbort = null, Action onCompleted = null, bool allowHoldFire = false) { if (mode == AIObjectiveCombat.CombatMode.None) { return; } - if (Character.IsDead || Character.IsIncapacitated) { return; } - if (ObjectiveManager.CurrentObjective is AIObjectiveCombat combatObjective) + if (Character.IsDead || Character.IsIncapacitated || Character.Removed) { return; } + if (!Character.IsBot) { return; } + if (ObjectiveManager.Objectives.FirstOrDefault(o => o is AIObjectiveCombat) is AIObjectiveCombat combatObjective) { // Don't replace offensive mode with something else if (combatObjective.Mode == AIObjectiveCombat.CombatMode.Offensive && mode != AIObjectiveCombat.CombatMode.Offensive) { return; } - if (combatObjective.Mode != mode || combatObjective.Enemy != attacker || (combatObjective.Enemy == null && attacker == null)) + if (combatObjective.Mode != mode || combatObjective.Enemy != target || (combatObjective.Enemy == null && target == null)) { // Replace the old objective with the new. ObjectiveManager.Objectives.Remove(combatObjective); @@ -1078,9 +1162,12 @@ namespace Barotrauma AIObjectiveCombat CreateCombatObjective() { - var objective = new AIObjectiveCombat(Character, attacker, mode, objectiveManager) + var objective = new AIObjectiveCombat(Character, target, mode, objectiveManager) { - HoldPosition = Character.Info?.Job?.Prefab.Identifier == "watchman" || Character.CurrentHull == null && ObjectiveManager.IsCurrentOrder(), + HoldPosition = + Character.Info?.Job?.Prefab.Identifier == "watchman" || + Character.CurrentHull == null || + Character.IsOnPlayerTeam && !target.IsPlayer && ObjectiveManager.GetActiveObjective()?.Target is Character followTarget && followTarget.IsPlayer, abortCondition = abortCondition, allowHoldFire = allowHoldFire, }; @@ -1096,11 +1183,20 @@ namespace Barotrauma } } - public void SetOrder(Order order, string option, Character orderGiver, bool speak = true) + public void SetOrder(Order order, string option, int priority, Character orderGiver, bool speak = true) { - CurrentOrderOption = option; - CurrentOrder = order; - objectiveManager.SetOrder(order, option, orderGiver, speak); + objectiveManager.SetOrder(order, option, priority, orderGiver, speak); + } + + public void SetForcedOrder(Order order, string option, Character orderGiver) + { + var objective = ObjectiveManager.CreateObjective(order, option, orderGiver, false); + ObjectiveManager.SetForcedOrder(objective); + } + + public void ClearForcedOrder() + { + ObjectiveManager.ClearForcedOrder(); } public override void SelectTarget(AITarget target) @@ -1112,7 +1208,7 @@ namespace Barotrauma { base.Reset(); objectiveManager.SortObjectives(); - sortTimer = sortObjectiveInterval; + SortTimer = sortObjectiveInterval; float waitDuration = characterWaitOnSwitch; if (ObjectiveManager.IsCurrentObjective()) { @@ -1305,7 +1401,7 @@ namespace Barotrauma Character thief = character; bool someoneSpoke = false; - if (item.SpawnedInOutpost && thief.TeamID != CharacterTeamType.FriendlyNPC && !item.HasTag("handlocker")) + if (item.SpawnedInOutpost && !item.AllowStealing && thief.TeamID != CharacterTeamType.FriendlyNPC && !item.HasTag("handlocker")) { foreach (Character otherCharacter in Character.CharacterList) { @@ -1338,6 +1434,9 @@ namespace Barotrauma item.StolenDuringRound = true; otherCharacter.Speak(TextManager.Get("dialogstealwarning"), null, Rand.Range(0.5f, 1.0f), "thief", 10.0f); someoneSpoke = true; +#if CLIENT + HintManager.OnStoleItem(thief, item); +#endif } // React if we are security if (!TriggerSecurity(otherHumanAI)) @@ -1354,7 +1453,7 @@ namespace Barotrauma } } } - else if (item.OwnInventory?.FindItem(it => it.SpawnedInOutpost, true) is { } foundItem) + else if (item.OwnInventory?.FindItem(it => it.SpawnedInOutpost && !item.AllowStealing, true) is { } foundItem) { ItemTaken(foundItem, character); } @@ -1474,7 +1573,7 @@ namespace Barotrauma targetAdded = true; } } - }); + }, range: (caller.AIController as HumanAIController)?.ReportRange ?? float.PositiveInfinity); return targetAdded; } @@ -1577,7 +1676,6 @@ namespace Barotrauma dangerousItemsFactor = 0; } } - float safety = oxygenFactor * waterFactor * fireFactor * enemyFactor * dangerousItemsFactor; return MathHelper.Clamp(safety * 100, 0, 100); } @@ -1624,7 +1722,7 @@ namespace Barotrauma public static bool IsFriendly(Character me, Character other, bool onlySameTeam = false) { bool sameTeam = me.TeamID == other.TeamID; - bool friendlyTeam = IsOnFriendlyTeam(GameMain.GameSession?.GameMode, me, other); + bool friendlyTeam = IsOnFriendlyTeam(me, other); bool teamGood = sameTeam || friendlyTeam && !onlySameTeam; if (!teamGood) { return false; } bool speciesGood = other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group); @@ -1640,18 +1738,27 @@ namespace Barotrauma return true; } - private static bool IsOnFriendlyTeam(GameMode mode, Character me, Character other) + public static bool IsOnFriendlyTeam(CharacterTeamType myTeam, CharacterTeamType otherTeam) { - // Only enemies are in the Team "None" - bool friendlyTeam = me.TeamID != CharacterTeamType.None && other.TeamID != CharacterTeamType.None; - // When playing a combat mission, we need to be on the same team to be friendlies - if (friendlyTeam && mode is MissionMode mm && mm.Mission is CombatMission) + if (myTeam == otherTeam) { return true; } + + switch (myTeam) { - friendlyTeam = me.TeamID == other.TeamID; + case CharacterTeamType.None: + case CharacterTeamType.Team1: + case CharacterTeamType.Team2: + // Only friendly to the same team and friendly NPCs + return otherTeam == CharacterTeamType.FriendlyNPC; + case CharacterTeamType.FriendlyNPC: + // Friendly NPCs are friendly to both teams + return otherTeam == CharacterTeamType.Team1 || otherTeam == CharacterTeamType.Team2; + default: + return true; } - return friendlyTeam; } + public static bool IsOnFriendlyTeam(Character me, Character other) => IsOnFriendlyTeam(me.TeamID, other.TeamID); + public static bool IsActive(Character other) => other != null && !other.Removed && !other.IsDead && !other.IsUnconscious; public static bool IsTrueForAllCrewMembers(Character character, Func predicate) @@ -1711,68 +1818,98 @@ namespace Barotrauma return count; } - public static void DoForEachCrewMember(Character character, Action action) + public static void DoForEachCrewMember(Character character, Action action, float range = float.PositiveInfinity) { if (character == null) { return; } foreach (var c in Character.CharacterList) { - if (FilterCrewMember(character, c)) + if (FilterCrewMember(character, c) && CheckReportRange(character, c, range)) { action(c.AIController as HumanAIController); } } } + private static bool CheckReportRange(Character character, Character target, float range) + { + if (float.IsPositiveInfinity(range)) { return true; } + if (character.CurrentHull == null || target.CurrentHull == null) + { + return Vector2.DistanceSquared(character.WorldPosition, target.WorldPosition) <= range * range; + } + else + { + return character.CurrentHull.GetApproximateDistance(character.Position, target.Position, target.CurrentHull, range, distanceMultiplierPerClosedDoor: 2) <= range; + } + } + private static bool FilterCrewMember(Character self, Character other) => other != null && !other.IsDead && !other.Removed && other.AIController is HumanAIController humanAi && humanAi.IsFriendly(self); public static bool IsItemOperatedByAnother(Character character, ItemComponent target, out Character operatingCharacter) { operatingCharacter = null; + if (character == null) { return false; } if (target?.Item == null) { return false; } + bool isOrder = IsOrderedToOperateThis(character.AIController); foreach (var c in Character.CharacterList) { - if (character == null) { continue; } if (c == character) { continue; } if (c.IsDead || c.IsIncapacitated) { continue; } - if (c.SelectedConstruction != target.Item) { continue; } if (!IsFriendly(character, c, onlySameTeam: true)) { continue; } operatingCharacter = c; - // If the other character is player, don't try to operate - if (c.IsPlayer) { return true; } - if (c.AIController is HumanAIController controllingHumanAi) + if (c.IsPlayer) { - Item otherTarget = controllingHumanAi.objectiveManager.GetActiveObjective()?.Component.Item ?? c.SelectedConstruction; - if (otherTarget != target.Item) { continue; } - // If the other character is ordered to operate the item, let him do it - if (controllingHumanAi.ObjectiveManager.IsCurrentOrder()) + if (c.SelectedConstruction == target.Item) { + // If the other character is player, don't try to operate + return true; + } + } + else if (c.AIController is HumanAIController operatingAI) + { + if (operatingAI.ObjectiveManager.Objectives.None(o => o is AIObjectiveOperateItem operateObjective && operateObjective.Component.Item == target.Item)) + { + // Not targeting the same item. + continue; + } + bool isTargetOrdered = IsOrderedToOperateThis(c.AIController); + if (!isOrder && isTargetOrdered) + { + // If the other bot is ordered to operate the item, let him do it, unless we are ordered too return true; } else { - if (character == null) + if (isOrder && !isTargetOrdered) { - return true; - } - else if (target is Steering) - { - // Steering is hard-coded -> cannot use the required skills collection defined in the xml - return character.GetSkillLevel("helm") <= c.GetSkillLevel("helm"); + // We are ordered and the target is not -> allow to operate + continue; } else { - return target.DegreeOfSuccess(character) <= target.DegreeOfSuccess(c); + if (!isTargetOrdered && operatingAI.ObjectiveManager.CurrentOrder == operatingAI.ObjectiveManager.CurrentObjective) + { + // The other bot is ordered to do something else + continue; + } + if (target is Steering) + { + // Steering is hard-coded -> cannot use the required skills collection defined in the xml + if (character.GetSkillLevel("helm") <= c.GetSkillLevel("helm")) + { + return true; + } + } + else if (target.DegreeOfSuccess(character) <= target.DegreeOfSuccess(c)) + { + return true; + } } } } - else - { - // Shouldn't go here, unless we allow non-humans to operate items - return false; - } - } return false; + bool IsOrderedToOperateThis(AIController ai) => ai is HumanAIController humanAI && humanAI.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateObjective && operateObjective.Component.Item == target.Item; } #region Wrappers diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs index 9a822be02..76b45c261 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs @@ -178,7 +178,7 @@ namespace Barotrauma { if (Timing.TotalTime < GameMain.GameSession.RoundStartTime + 120.0f && speaker?.CurrentHull != null && - speaker.TeamID == CharacterTeamType.FriendlyNPC && + (speaker.TeamID == CharacterTeamType.FriendlyNPC || speaker.TeamID == CharacterTeamType.None) && Character.CharacterList.Any(c => c.TeamID != speaker.TeamID && c.CurrentHull == speaker.CurrentHull)) { currentFlags.Add("EnterOutpost"); @@ -188,6 +188,11 @@ namespace Barotrauma { currentFlags.Add("Casual"); } + + if (GameMain.GameSession.IsCurrentLocationRadiated()) + { + currentFlags.Add("InRadiation"); + } } if (speaker != null) @@ -221,6 +226,19 @@ namespace Barotrauma { currentFlags.Add("CampaignNPC." + speaker.CampaignInteractionType); } + + if (GameMain.GameSession?.GameMode is CampaignMode campaignMode && + (campaignMode.Map?.CurrentLocation?.Type?.Identifier.Equals("abandoned", StringComparison.OrdinalIgnoreCase) ?? false)) + { + if (speaker.TeamID == CharacterTeamType.None) + { + currentFlags.Add("Bandit"); + } + else if (speaker.TeamID == CharacterTeamType.FriendlyNPC) + { + currentFlags.Add("Hostage"); + } + } } return currentFlags; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs index 502f2c559..23a43a38a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -68,7 +68,7 @@ namespace Barotrauma if (_abandon) { #if DEBUG - if (HumanAIController.debugai && objectiveManager.CurrentOrder == this) + if (HumanAIController.debugai && objectiveManager.IsOrder(this) && !objectiveManager.IsCurrentOrder()) { throw new Exception("Order abandoned!"); } @@ -230,7 +230,7 @@ namespace Barotrauma /// public virtual float GetPriority() { - bool isOrder = objectiveManager.CurrentOrder == this; + bool isOrder = objectiveManager.IsOrder(this); if (!IsAllowed) { Priority = 0; @@ -239,7 +239,7 @@ namespace Barotrauma } if (isOrder) { - Priority = AIObjectiveManager.OrderPriority; + Priority = objectiveManager.GetOrderPriority(this); } else { @@ -261,7 +261,7 @@ namespace Barotrauma public virtual void Update(float deltaTime) { - if (objectiveManager.CurrentOrder != this && objectiveManager.WaitTimer <= 0) + if (!objectiveManager.IsOrder(this) && objectiveManager.WaitTimer <= 0) { UpdateDevotion(deltaTime); } @@ -430,7 +430,7 @@ namespace Barotrauma subObjectives.Remove(subObjective); if (AbandonWhenCannotCompleteSubjectives) { - if (objectiveManager.CurrentOrder == this) + if (objectiveManager.IsOrder(this)) { Reset(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs index 5364a01b0..1835b0a4f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs @@ -64,7 +64,7 @@ namespace Barotrauma private bool IsReady(PowerContainer battery) { - if (battery.HasBeenTuned && character.CurrentOrder == null) { return true; } + if (battery.HasBeenTuned && character.IsDismissed) { return true; } if (Option == "charge") { return battery.RechargeRatio >= PowerContainer.aiRechargeTargetRatio; @@ -79,7 +79,7 @@ namespace Barotrauma new AIObjectiveOperateItem(battery, character, objectiveManager, Option, false, priorityModifier: PriorityModifier) { IsLoop = false, - Override = character.CurrentOrder != null, + Override = !character.IsDismissed, completionCondition = () => IsReady(battery) }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs index 8c110d2ae..6b99d3899 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs @@ -48,21 +48,35 @@ namespace Barotrauma float selectedBonus = isSelected ? 100 - MaxDevotion : 0; float devotion = (CumulatedDevotion + selectedBonus) / 100; float reduction = IsPriority ? 1 : isSelected ? 2 : 3; - float max = MathHelper.Min(AIObjectiveManager.OrderPriority - reduction, 90); + float max = AIObjectiveManager.LowestOrderPriority - reduction; Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (distanceFactor * PriorityModifier), 0, 1)); + if (decontainObjective == null) + { + // Halve the priority until there's a decontain objective (a valid container was found). + Priority /= 2; + } } return Priority; } protected override void Act(float deltaTime) { - // Only continue when the get item sub objectives have been completed. - if (subObjectives.Any()) { return; } if (item.IgnoreByAI) { Abandon = true; return; } + if (item.ParentInventory != null) + { + if (item.Container != null && !AIObjectiveCleanupItems.IsValidContainer(item.Container, character, allowUnloading: objectiveManager.HasOrders())) + { + // Target was picked up or moved by someone. + Abandon = true; + return; + } + } + // Only continue when the get item sub objectives have been completed. + if (subObjectives.Any()) { return; } if (HumanAIController.FindSuitableContainer(character, item, ignoredContainers, ref itemIndex, out Item suitableContainer)) { itemIndex = 0; @@ -79,6 +93,7 @@ namespace Barotrauma TryAddSubObjective(ref decontainObjective, () => new AIObjectiveDecontainItem(character, item, objectiveManager, targetContainer: suitableContainer.GetComponent()) { Equip = equip, + TakeWholeStack = true, DropIfFails = true }, onCompleted: () => @@ -125,5 +140,13 @@ namespace Barotrauma itemIndex = 0; decontainObjective = null; } + + public void DropTarget() + { + if (item != null && character.HasItem(item)) + { + item.Drop(character); + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs index f45c39440..cb1e3f299 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs @@ -2,6 +2,7 @@ using Barotrauma.Items.Components; using System.Collections.Generic; using System.Linq; +using System; namespace Barotrauma { @@ -29,7 +30,21 @@ namespace Barotrauma this.prioritizedItems.AddRange(prioritizedItems.Where(i => i != null)); } - protected override float TargetEvaluation() => Targets.Any() ? (objectiveManager.CurrentOrder == this ? AIObjectiveManager.OrderPriority : AIObjectiveManager.RunPriority - 1) : 0; + protected override float TargetEvaluation() + { + if (Targets.None()) { return 0; } + if (objectiveManager.IsOrder(this)) + { + float prio = objectiveManager.GetOrderPriority(this); + if (subObjectives.All(so => so.SubObjectives.None())) + { + // If none of the subobjectives have subobjectives, no valid container was found. In this case, let's reduce the priority below the run threshold. + prio = Math.Min(prio, AIObjectiveManager.RunPriority - 1); + } + return prio; + } + return AIObjectiveManager.RunPriority - 0.5f; + } protected override bool Filter(Item target) { @@ -65,10 +80,10 @@ namespace Barotrauma return true; } - public static bool IsValidContainer(Item item, Character character) => - !item.IgnoreByAI && item.IsInteractable(character) && item.HasTag("allowcleanup") && item.ParentInventory == null && item.OwnInventory != null && item.OwnInventory.AllItems.Any() && IsItemInsideValidSubmarine(item, character); + public static bool IsValidContainer(Item item, Character character, bool allowUnloading = true) => + !item.IgnoreByAI && item.IsInteractable(character) && item.HasTag("allowcleanup") && allowUnloading && item.ParentInventory == null && item.OwnInventory != null && item.OwnInventory.AllItems.Any() && IsItemInsideValidSubmarine(item, character); - public static bool IsValidTarget(Item item, Character character, bool checkInventory) + public static bool IsValidTarget(Item item, Character character, bool checkInventory, bool allowUnloading = true) { if (item == null) { return false; } if (item.IgnoreByAI) { return false; } @@ -76,7 +91,7 @@ namespace Barotrauma if (item.SpawnedInOutpost) { return false; } if (item.ParentInventory != null) { - if (item.Container == null || !IsValidContainer(item.Container, character)) { return false; } + if (item.Container == null || !IsValidContainer(item.Container, character, allowUnloading)) { return false; } } if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; } var pickable = item.GetComponent(); @@ -127,5 +142,17 @@ namespace Barotrauma } return canEquip; } + + public override void OnDeselected() + { + base.OnDeselected(); + foreach (var subObjective in SubObjectives) + { + if (subObjective is AIObjectiveCleanupItem cleanUpObjective) + { + cleanUpObjective.DropTarget(); + } + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index 245abc582..9a53951ea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -30,6 +30,7 @@ namespace Barotrauma private float holdFireTimer; private bool hasAimed; private bool isLethalWeapon; + private bool AllowCoolDown => !IsOffensiveOrArrest || Mode != initialMode; public Character Enemy { get; private set; } public bool HoldPosition { get; set; } @@ -79,11 +80,18 @@ namespace Barotrauma private float coolDownTimer; private IEnumerable myBodies; private float aimTimer; + private float reloadTimer; + private float spreadTimer; private bool canSeeTarget; private float visibilityCheckTimer; private readonly float visibilityCheckInterval = 0.2f; + private float sqrDistance; + private readonly float maxDistance = 2000; + private readonly float distanceCheckInterval = 0.2f; + private float distanceTimer; + /// /// Aborts the objective when this condition is true /// @@ -108,8 +116,12 @@ namespace Barotrauma public CombatMode Mode { get; private set; } private bool IsOffensiveOrArrest => initialMode == CombatMode.Offensive || initialMode == CombatMode.Arrest; - private bool TargetEliminated => Enemy == null || Enemy.Removed || Enemy.IsUnconscious; + private bool TargetEliminated => IsEnemyDisabled || Enemy.IsUnconscious; private bool IsEnemyDisabled => Enemy == null || Enemy.Removed || Enemy.IsDead; + + private float AimSpeed => HumanAIController.AimSpeed; + private float AimAccuracy => HumanAIController.AimAccuracy; + private bool EnemyIsClose() => Enemy != null && character.CurrentHull != null && character.CurrentHull == Enemy.CurrentHull || Vector2.DistanceSquared(character.Position, Enemy.Position) < 500; public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1, float coolDown = 10.0f) @@ -136,6 +148,8 @@ namespace Barotrauma { Mode = CombatMode.Retreat; } + spreadTimer = Rand.Range(-10, 10); + HumanAIController.SortTimer = 0; } public override float GetPriority() @@ -159,6 +173,10 @@ namespace Barotrauma base.Update(deltaTime); ignoreWeaponTimer -= deltaTime; checkWeaponsTimer -= deltaTime; + if (reloadTimer > 0) + { + reloadTimer -= deltaTime; + } if (ignoreWeaponTimer < 0) { ignoredWeapons.Clear(); @@ -168,17 +186,25 @@ namespace Barotrauma { findSafety.Priority = 0; } + if (!character.IsOnPlayerTeam && !objectiveManager.IsCurrentObjective()) + { + distanceTimer -= deltaTime; + if (distanceTimer < 0) + { + distanceTimer = distanceCheckInterval; + sqrDistance = Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition); + } + } } protected override bool Check() { - if (IsOffensiveOrArrest && Mode != initialMode) + if (sqrDistance > maxDistance * maxDistance) { - Abandon = true; - SteeringManager.Reset(); - return false; + // The target escaped from us. + return true; } - return IsEnemyDisabled || (!IsOffensiveOrArrest && coolDownTimer <= 0); + return IsEnemyDisabled || (AllowCoolDown && coolDownTimer <= 0); } protected override void Act(float deltaTime) @@ -186,10 +212,9 @@ namespace Barotrauma if (abortCondition != null && abortCondition()) { Abandon = true; - SteeringManager.Reset(); return; } - if (!IsOffensiveOrArrest) + if (AllowCoolDown) { coolDownTimer -= deltaTime; } @@ -199,7 +224,11 @@ namespace Barotrauma { OperateWeapon(deltaTime); } - if (!HoldPosition && seekAmmunitionObjective == null && seekWeaponObjective == null) + if (HoldPosition) + { + SteeringManager.Reset(); + } + else if (seekAmmunitionObjective == null && seekWeaponObjective == null) { Move(deltaTime); } @@ -431,7 +460,7 @@ namespace Barotrauma priority /= 2; } } - if (Enemy.Stun > 1) + if (Enemy.IsKnockedDown) { // Enemy is stunned, reduce the priority of stunner weapons. Attack attack = GetAttackDefinition(weapon); @@ -621,7 +650,7 @@ namespace Barotrauma var slots = Weapon.AllowedSlots.Where(s => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand)); if (character.Inventory.TryPutItem(Weapon, character, slots)) { - aimTimer = Rand.Range(0.5f, 1f); + aimTimer = Rand.Range(0.2f, 0.4f) / AimSpeed; } else { @@ -704,15 +733,12 @@ namespace Barotrauma { IgnoreIfTargetDead = true, DialogueIdentifier = "dialogcannotreachtarget", - TargetName = Enemy.DisplayName + TargetName = Enemy.DisplayName, + AlwaysUseEuclideanDistance = false }, - onAbandon: () => - { - Abandon = true; - SteeringManager.Reset(); - }); + onAbandon: () => Abandon = true); if (followTargetObjective == null) { return; } - if (Mode == CombatMode.Arrest && Enemy.Stun > 2) + if (Mode == CombatMode.Arrest && (Enemy.Stun > 1 || Enemy.IsKnockedDown)) { if (HumanAIController.HasItem(character, "handlocker", out _)) { @@ -720,8 +746,8 @@ namespace Barotrauma { arrestingRegistered = true; followTargetObjective.Completed += OnArrestTargetReached; + followTargetObjective.CloseEnough = 100; } - followTargetObjective.CloseEnough = 100; } else { @@ -737,7 +763,7 @@ namespace Barotrauma SteeringManager.Reset(); } } - if (followTargetObjective != null) + if (!arrestingRegistered && followTargetObjective != null) { followTargetObjective.CloseEnough = WeaponComponent is RangedWeapon ? 1000 : @@ -760,7 +786,7 @@ namespace Barotrauma private void OnArrestTargetReached() { - if (HumanAIController.HasItem(character, "handlocker", out IEnumerable matchingItems) && Enemy.Stun > 0 && character.CanInteractWith(Enemy)) + if (HumanAIController.HasItem(character, "handlocker", out IEnumerable matchingItems) && !Enemy.IsUnconscious && Enemy.IsKnockedDown && character.CanInteractWith(Enemy)) { var handCuffs = matchingItems.First(); if (!HumanAIController.TakeItem(handCuffs, Enemy.Inventory, equip: true)) @@ -780,8 +806,8 @@ namespace Barotrauma } } character.Speak(TextManager.Get("DialogTargetArrested"), null, 3.0f, "targetarrested", 30.0f); - IsCompleted = true; } + IsCompleted = true; } /// @@ -818,25 +844,7 @@ namespace Barotrauma if (WeaponComponent == null) { return false; } if (Weapon.OwnInventory == null) { return true; } // Eject empty ammo - if (Weapon.OwnInventory.AllItems.Any(it => it.Condition <= 0.0f)) - { - foreach (Item containedItem in Weapon.OwnInventory.AllItemsMod) - { - if (containedItem.Condition <= 0) - { - if (character.Submarine == null) - { - // If we are outside of main sub, try to put the ammo in the inventory instead dropping it in the sea. - if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.anySlot)) - { - continue; - } - } - containedItem.Drop(character); - } - } - } - + HumanAIController.UnequipEmptyItems(Weapon); RelatedItem item = null; Item ammunition = null; string[] ammunitionIdentifiers = null; @@ -869,22 +877,13 @@ namespace Barotrauma if (ammunition != null) { var container = Weapon.GetComponent(); - if (container.Item.ParentInventory == character.Inventory) + if (!container.Inventory.TryPutItem(ammunition, null)) { - if (!container.Inventory.CanBePut(ammunition)) - { - return false; - } - character.Inventory.RemoveItem(ammunition); - if (!container.Inventory.TryPutItem(ammunition, null)) + if (ammunition.ParentInventory == character.Inventory) { ammunition.Drop(character); } } - else - { - container.Combine(ammunition, character); - } } } } @@ -902,6 +901,15 @@ namespace Barotrauma private void Attack(float deltaTime) { character.CursorPosition = Enemy.WorldPosition; + if (AimAccuracy < 1) + { + spreadTimer += deltaTime * Rand.Range(0.01f, 1f); + float shake = Rand.Range(0.95f, 1.05f); + float offsetAmount = (1 - AimAccuracy) * Rand.Range(300f, 500f); + float distanceFactor = MathUtils.InverseLerp(0, 1000 * 1000, sqrDistance); + float offset = (float)Math.Sin(spreadTimer * shake) * offsetAmount * distanceFactor; + character.CursorPosition += new Vector2(0, offset); + } if (character.Submarine != null) { character.CursorPosition -= character.Submarine.Position; @@ -912,7 +920,11 @@ namespace Barotrauma canSeeTarget = character.CanSeeTarget(Enemy); visibilityCheckTimer = visibilityCheckInterval; } - if (!canSeeTarget) { return; } + if (!canSeeTarget) + { + aimTimer = Rand.Range(0.2f, 0.4f) / AimSpeed; + return; + } if (Weapon.RequireAimToUse) { character.SetInput(InputType.Aim, false, true); @@ -928,7 +940,15 @@ namespace Barotrauma aimTimer -= deltaTime; return; } - if (Mode == CombatMode.Arrest && isLethalWeapon && Enemy.Stun > 1) { return; } + if (reloadTimer > 0) { return; } + if (Mode == CombatMode.Arrest) + { + // If the target is arrested or if it's stunned and we can't lock the target up, consider the objective done. + if (Enemy.IsKnockedDown && !HumanAIController.HasItem(character, "handlocker", out _, requireEquipped: false) || HumanAIController.HasItem(Enemy, "handlocker", out _, requireEquipped: true)) + { + IsCompleted = true; + } + } if (holdFireCondition != null && holdFireCondition()) { return; } float sqrDist = Vector2.DistanceSquared(character.Position, Enemy.Position); if (WeaponComponent is MeleeWeapon meleeWeapon) @@ -963,14 +983,12 @@ namespace Barotrauma } if (closeEnough) { - SteeringManager.Reset(); - character.SetInput(InputType.Shoot, false, true); - Weapon.Use(deltaTime, character); + UseWeapon(deltaTime); } else if (!character.IsFacing(Enemy.WorldPosition)) { // Don't do the facing check if we are close to the target, because it easily causes the character to get stuck here when it flips around. - aimTimer = Rand.Range(1f, 1.5f); + aimTimer = Rand.Range(1f, 1.5f) / AimSpeed; } } else @@ -979,14 +997,15 @@ namespace Barotrauma { if (sqrDist > repairTool.Range * repairTool.Range) { return; } } - if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.Position - Weapon.Position) < MathHelper.PiOver4) + float aimFactor = MathHelper.PiOver2 * (1 - AimAccuracy); + if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.Position - Weapon.Position) < MathHelper.PiOver4 + aimFactor) { if (myBodies == null) { myBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody); } var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel; - var pickedBody = Submarine.PickBody(Weapon.SimPosition, Enemy.SimPosition, myBodies, collisionCategories); + var pickedBody = Submarine.PickBody(Weapon.SimPosition, Enemy.SimPosition, myBodies, collisionCategories, allowInsideFixture: true); if (pickedBody != null) { Character target = null; @@ -1000,31 +1019,62 @@ namespace Barotrauma } if (target != null && (target == Enemy || !HumanAIController.IsFriendly(target))) { - character.SetInput(InputType.Shoot, false, true); - Weapon.Use(deltaTime, character); - float reloadTime = 0; - if (WeaponComponent is RangedWeapon rangedWeapon) - { - reloadTime = rangedWeapon.Reload; - } - if (WeaponComponent is MeleeWeapon mw) - { - reloadTime = mw.Reload; - } - aimTimer = reloadTime * Rand.Range(1f, 1.5f); + UseWeapon(deltaTime); } } } } } + private void UseWeapon(float deltaTime) + { + // Never allow to attack characters with deadly weapons while trying to arrest. + if (Mode == CombatMode.Arrest && isLethalWeapon) { return; } + float reloadTime = 0; + if (WeaponComponent is RangedWeapon rangedWeapon) + { + // If the weapon is just equipped, we can't shoot just yet. + if (rangedWeapon.ReloadTimer <= 0) + { + reloadTime = rangedWeapon.Reload; + } + } + if (WeaponComponent is MeleeWeapon mw) + { + if (!((HumanoidAnimController)character.AnimController).Crouching) + { + reloadTime = mw.Reload; + } + } + character.SetInput(InputType.Shoot, false, true); + Weapon.Use(deltaTime, character); + reloadTimer = Math.Max(reloadTime, reloadTime * Rand.Range(1f, 1.25f) / AimSpeed); + } + + private bool ShouldUnequipWeapon => + Weapon != null && + character.Submarine != null && + character.Submarine.TeamID == character.TeamID && + Character.CharacterList.None(c => c.Submarine == character.Submarine && HumanAIController.IsActive(c) && !HumanAIController.IsFriendly(character, c) && HumanAIController.VisibleHulls.Contains(c.CurrentHull)); + protected override void OnCompleted() { base.OnCompleted(); - if (Weapon != null) + if (ShouldUnequipWeapon) { Unequip(); } + SteeringManager.Reset(); + } + + protected override void OnAbandon() + { + base.OnAbandon(); + if (ShouldUnequipWeapon) + { + Unequip(); + } + SteeringManager.Reset(); } public override void Reset() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs index dbaa4c350..705ad946b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -34,6 +34,10 @@ namespace Barotrauma public float ConditionLevel { get; set; } = 1; public bool Equip { get; set; } public bool RemoveEmpty { get; set; } = true; + public bool RemoveExisting { get; set; } + + public bool MoveWholeStack { get; set; } + public AIObjectiveContainItem(Character character, Item item, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) @@ -102,47 +106,38 @@ namespace Barotrauma } if (character.CanInteractWith(container.Item, checkLinked: false)) { - if (RemoveEmpty && container.Inventory.AllItems.Any(it => it.Condition <= 0.0f)) + if (RemoveExisting) { - foreach (var emptyItem in container.Inventory.AllItemsMod) - { - if (emptyItem.Condition <= 0) - { - emptyItem.Drop(character); - } - } + HumanAIController.UnequipContainedItems(container.Item); } - // Contain the item - if (ItemToContain.ParentInventory == character.Inventory) + else if (RemoveEmpty) { - if (!container.Inventory.CanBePut(ItemToContain)) + HumanAIController.UnequipEmptyItems(container.Item); + } + Inventory originalInventory = ItemToContain.ParentInventory; + var slots = originalInventory?.FindIndices(ItemToContain); + if (container.Inventory.TryPutItem(ItemToContain, null)) + { + if (MoveWholeStack && slots != null) { - Abandon = true; - } - else - { - character.Inventory.RemoveItem(ItemToContain); - if (container.Inventory.TryPutItem(ItemToContain, null)) + foreach (int slot in slots) { - IsCompleted = true; - } - else - { - ItemToContain.Drop(character); - Abandon = true; + foreach (Item item in originalInventory.GetItemsAt(slot).ToList()) + { + container.Inventory.TryPutItem(item, null); + } } + + IsCompleted = true; } } else { - if (container.Combine(ItemToContain, character)) + if (ItemToContain.ParentInventory == character.Inventory && character.Submarine == Submarine.MainSub) { - IsCompleted = true; - } - else - { - Abandon = true; + ItemToContain.Drop(character); } + Abandon = true; } } else @@ -151,7 +146,8 @@ namespace Barotrauma { DialogueIdentifier = "dialogcannotreachtarget", TargetName = container.Item.Name, - abortCondition = () => !ItemToContain.IsOwnedBy(character) + abortCondition = obj => !ItemToContain.IsOwnedBy(character), + SpeakIfFails = !objectiveManager.IsCurrentOrder() }, onAbandon: () => Abandon = true, onCompleted: () => RemoveSubObjective(ref goToObjective)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs index d0ba8e751..63db2719a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs @@ -22,8 +22,13 @@ namespace Barotrauma public AIObjectiveGetItem GetItemObjective => getItemObjective; public AIObjectiveContainItem ContainObjective => containObjective; + public Item TargetItem => targetItem; + public ItemContainer TargetContainer => targetContainer; + public bool Equip { get; set; } + public bool TakeWholeStack { get; set; } + /// /// If true drops the item when containing the item fails. /// In both cases abandons the objective. @@ -90,7 +95,7 @@ namespace Barotrauma if (getItemObjective == null && !itemToDecontain.IsOwnedBy(character)) { TryAddSubObjective(ref getItemObjective, - constructor: () => new AIObjectiveGetItem(character, targetItem, objectiveManager, Equip), + constructor: () => new AIObjectiveGetItem(character, targetItem, objectiveManager, Equip) { TakeWholeStack = this.TakeWholeStack }, onAbandon: () => Abandon = true); return; } @@ -99,6 +104,7 @@ namespace Barotrauma TryAddSubObjective(ref containObjective, constructor: () => new AIObjectiveContainItem(character, itemToDecontain, targetContainer, objectiveManager) { + MoveWholeStack = TakeWholeStack, Equip = Equip, RemoveEmpty = false, GetItemPriority = GetItemPriority, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs index 2a685c43f..25f2082ee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs @@ -35,7 +35,7 @@ namespace Barotrauma Abandon = true; return Priority; } - bool isOrder = objectiveManager.IsCurrentOrder(); + bool isOrder = objectiveManager.HasOrder(); if (!isOrder && Character.CharacterList.Any(c => c.CurrentHull == targetHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { // Don't go into rooms with any enemies, unless it's an order @@ -78,7 +78,7 @@ namespace Barotrauma { TryAddSubObjective(ref getExtinguisherObjective, () => { - if (!character.HasEquippedItem("fireextinguisher", allowBroken: false)) + if (character.IsOnPlayerTeam && !character.HasEquippedItem("fireextinguisher", allowBroken: false)) { character.Speak(TextManager.Get("DialogFindExtinguisher"), null, 2.0f, "findextinguisher", 30.0f); } @@ -88,7 +88,7 @@ namespace Barotrauma // If the item is inside an unsafe hull, decrease the priority GetItemPriority = i => HumanAIController.UnsafeHulls.Contains(i.CurrentHull) ? 0.1f : 1 }; - if (objectiveManager.IsCurrentOrder()) + if (objectiveManager.HasOrder()) { getItemObjective.Abandoned += () => character.Speak(TextManager.Get("dialogcannotfindfireextinguisher"), null, 0.0f, "dialogcannotfindfireextinguisher", 10.0f); }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs index b877fd2ce..2422534e2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs @@ -42,6 +42,15 @@ namespace Barotrauma if (hull.Submarine == null) { return false; } if (character.Submarine == null) { return false; } if (!character.Submarine.IsEntityFoundOnThisSub(hull, includingConnectedSubs: true)) { return false; } + if (hull.BallastFlora != null) { return false; } + foreach (var ballastFlora in MapCreatures.Behavior.BallastFloraBehavior.EntityList) + { + if (ballastFlora.Parent?.Submarine != character.Submarine) { continue; } + if (ballastFlora.Branches.Any(b => !b.Removed && b.Health > 0 && b.CurrentHull == hull)) + { + return false; + } + } return true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs index ef57dc156..8187e6092 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs @@ -10,6 +10,8 @@ namespace Barotrauma protected override float IgnoreListClearInterval => 30; public override bool IgnoreUnsafeHulls => true; + protected override float TargetUpdateTimeMultiplier => 0.2f; + public AIObjectiveFightIntruders(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { } @@ -48,16 +50,14 @@ namespace Barotrauma public static bool IsValidTarget(Character target, Character character) { - if (target == null || target.IsDead || target.Removed) { return false; } + if (target == null || target.Removed) { return false; } + if (target.IsDead || target.IsUnconscious) { return false; } if (target == character) { return false; } - if (HumanAIController.IsFriendly(character, target)) { return false; } if (target.Submarine == null) { return false; } - if (target.Submarine.TeamID != character.TeamID) { return false; } + if (character.Submarine == null) { return false; } if (target.CurrentHull == null) { return false; } - if (character.Submarine != null) - { - if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; } - } + if (HumanAIController.IsFriendly(character, target)) { return false; } + if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; } return true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs index bd4b4314a..fd29e08db 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs @@ -1,6 +1,7 @@ using Barotrauma.Items.Components; using Barotrauma.Extensions; using System.Collections.Generic; +using System.Linq; namespace Barotrauma { @@ -37,11 +38,11 @@ namespace Barotrauma return; } targetItem = character.Inventory.FindItemByTag(gearTag, true); - if (targetItem == null || !character.HasEquippedItem(targetItem)) + if (targetItem == null || !character.HasEquippedItem(targetItem) && targetItem.ContainedItems.Any(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > 0)) { TryAddSubObjective(ref getDivingGear, () => { - if (targetItem == null) + if (targetItem == null && character.IsOnPlayerTeam) { character.Speak(TextManager.Get("DialogGetDivingGear"), null, 0.0f, "getdivinggear", 30.0f); } @@ -57,46 +58,78 @@ namespace Barotrauma } else { - if (!EjectEmptyTanks(character, targetItem, out var containedItems)) + HumanAIController.UnequipContainedItems(targetItem, it => !it.HasTag("oxygensource")); + HumanAIController.UnequipEmptyItems(targetItem); + // Seek oxygen that has at least 10% condition left, if we are inside a friendly sub. + // The margin helps us to survive, because we might need some oxygen before we can find more oxygen. + // When we are venturing outside of our sub, let's just suppose that we have enough oxygen with us and optimize it so that we don't keep switching off half used tanks. + float min = character.Submarine != Submarine.MainSub ? 0.01f : MIN_OXYGEN; + if (targetItem.OwnInventory != null && targetItem.OwnInventory.AllItems.None(it => it != null && it.HasTag(OXYGEN_SOURCE) && it.Condition > min)) { -#if DEBUG - DebugConsole.ThrowError($"{character.Name}: AIObjectiveFindDivingGear failed - the item \"" + targetItem + "\" has no proper inventory"); -#endif - Abandon = true; - return; - } - if (containedItems.None(it => it != null && it.HasTag(OXYGEN_SOURCE) && it.Condition > MIN_OXYGEN)) - { - // No valid oxygen source loaded. - // Seek oxygen that has min 10% condition left. TryAddSubObjective(ref getOxygen, () => { - if (!HumanAIController.HasItem(character, "oxygensource", out _, conditionPercentage: 10)) + if (character.IsOnPlayerTeam) { - character.Speak(TextManager.Get("DialogGetOxygenTank"), null, 0, "getoxygentank", 30.0f); + if (HumanAIController.HasItem(character, "oxygensource", out _, conditionPercentage: min)) + { + character.Speak(TextManager.Get("dialogswappingoxygentank"), null, 0, "swappingoxygentank", 30.0f); + } + else + { + character.Speak(TextManager.Get("DialogGetOxygenTank"), null, 0, "getoxygentank", 30.0f); + } } return new AIObjectiveContainItem(character, OXYGEN_SOURCE, targetItem.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC) { AllowToFindDivingGear = false, AllowDangerousPressure = true, - ConditionLevel = MIN_OXYGEN + ConditionLevel = MIN_OXYGEN, + RemoveExisting = true }; }, onAbandon: () => { - // Try to seek any oxygen sources. + getOxygen = null; + int remainingTanks = ReportOxygenTankCount(); + // Try to seek any oxygen sources, even if they have minimal amount of oxygen. TryAddSubObjective(ref getOxygen, () => { return new AIObjectiveContainItem(character, OXYGEN_SOURCE, targetItem.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC) { AllowToFindDivingGear = false, - AllowDangerousPressure = true + AllowDangerousPressure = true, + RemoveExisting = true }; }, - onAbandon: () => Abandon = true, + onAbandon: () => + { + Abandon = true; + if (remainingTanks > 0 && !HumanAIController.HasItem(character, "oxygensource", out _, conditionPercentage: 0.01f)) + { + character.Speak(TextManager.Get("dialogcantfindtoxygen"), null, 0, "cantfindoxygen", 30.0f); + } + }, onCompleted: () => RemoveSubObjective(ref getOxygen)); }, - onCompleted: () => RemoveSubObjective(ref getOxygen)); + onCompleted: () => + { + RemoveSubObjective(ref getOxygen); + ReportOxygenTankCount(); + }); + + int ReportOxygenTankCount() + { + int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag("oxygensource") && i.Condition > 1); + if (remainingOxygenTanks == 0) + { + character.Speak(TextManager.Get("DialogOutOfOxygenTanks"), null, 0.0f, "outofoxygentanks", 30.0f); + } + else if (remainingOxygenTanks < 10) + { + character.Speak(TextManager.Get("DialogLowOnOxygenTanks"), null, 0.0f, "lowonoxygentanks", 30.0f); + } + return remainingOxygenTanks; + } } } } @@ -108,21 +141,7 @@ namespace Barotrauma { containedItems = target.OwnInventory?.AllItems; if (containedItems == null) { return false; } - foreach (Item containedItem in target.OwnInventory.AllItemsMod) - { - if (containedItem.Condition <= 0.0f) - { - if (actor.Submarine == null) - { - // If we are outside of main sub, try to put the tank in the inventory instead dropping it in the sea. - if (actor.Inventory.TryPutItem(containedItem, actor, CharacterInventory.anySlot)) - { - continue; - } - } - containedItem.Drop(actor); - } - } + AIController.UnequipEmptyItems(actor, target); return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs index 191d16d99..e8d0f4fb4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -46,19 +46,27 @@ namespace Barotrauma } if (character.CurrentHull == null) { - Priority = (objectiveManager.IsCurrentOrder() || objectiveManager.Objectives.Any(o => o is AIObjectiveCombat)) && HumanAIController.HasDivingSuit(character) ? 0 : 100; + Priority = (objectiveManager.IsCurrentOrder() || objectiveManager.HasActiveObjective()) && HumanAIController.HasDivingSuit(character) ? 0 : 100; } else { - if (HumanAIController.NeedsDivingGear(character.CurrentHull, out _) && !HumanAIController.HasDivingGear(character)) + if (HumanAIController.NeedsDivingGear(character.CurrentHull, out bool needsSuit) && + (needsSuit ? + !HumanAIController.HasDivingSuit(character, conditionPercentage: AIObjectiveFindDivingGear.MIN_OXYGEN) : + !HumanAIController.HasDivingMask(character, conditionPercentage: AIObjectiveFindDivingGear.MIN_OXYGEN))) { Priority = 100; } + else if (objectiveManager.IsCurrentOrder() && character.Submarine != null && !HumanAIController.IsOnFriendlyTeam(character.TeamID, character.Submarine.TeamID)) + { + // Ordered to follow/hold position inside a hostile sub -> ignore find safety unless we need to find a diving gear + Priority = 0; + } Priority = MathHelper.Clamp(Priority, 0, 100); if (divingGearObjective != null && !divingGearObjective.IsCompleted && divingGearObjective.CanBeCompleted) { // Boost the priority while seeking the diving gear - Priority = Math.Max(Priority, Math.Min(AIObjectiveManager.OrderPriority + 20, 100)); + Priority = Math.Max(Priority, Math.Min(AIObjectiveManager.HighestOrderPriority + 20, 100)); } } return Priority; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index 415158c6f..3b6b38a69 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -38,7 +38,7 @@ namespace Barotrauma Priority = 0; Abandon = true; } - else if (HumanAIController.IsTrueForAnyCrewMember(other => other != HumanAIController && other.ObjectiveManager.GetActiveObjective()?.Leak == Leak)) + else if (HumanAIController.IsTrueForAnyCrewMember(other => other != HumanAIController && other.Character.IsBot && other.ObjectiveManager.GetActiveObjective()?.Leak == Leak)) { Priority = 0; Abandon = true; @@ -52,7 +52,7 @@ namespace Barotrauma float distanceFactor = isPriority || xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 3000, xDist + yDist * 3.0f)); float severity = isPriority ? 1 : AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100; float reduction = isPriority ? 1 : 2; - float max = MathHelper.Min(AIObjectiveManager.OrderPriority - reduction, 90); + float max = AIObjectiveManager.LowestOrderPriority - reduction; float devotion = CumulatedDevotion / 100; Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1)); } @@ -67,7 +67,7 @@ namespace Barotrauma TryAddSubObjective(ref getWeldingTool, () => new AIObjectiveGetItem(character, "weldingequipment", objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC), onAbandon: () => { - if (objectiveManager.IsCurrentOrder()) + if (character.IsOnPlayerTeam && objectiveManager.IsCurrentOrder()) { character.Speak(TextManager.Get("dialogcannotfindweldingequipment"), null, 0.0f, "dialogcannotfindweldingequipment", 10.0f); } @@ -86,23 +86,34 @@ namespace Barotrauma Abandon = true; return; } - // Drop empty tanks - if (weldingTool.OwnInventory.AllItems.Any(it => it.Condition <= 0.0f)) + HumanAIController.UnequipContainedItems(weldingTool, it => !it.HasTag("weldingfuel")); + HumanAIController.UnequipEmptyItems(weldingTool); + if (weldingTool.OwnInventory != null && weldingTool.OwnInventory.AllItems.None(i => i.HasTag("weldingfuel") && i.Condition > 0.0f)) { - foreach (Item containedItem in weldingTool.OwnInventory.AllItemsMod) - { - if (containedItem.Condition <= 0.0f) + TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC), + onAbandon: () => { - containedItem.Drop(character); + Abandon = true; + ReportWeldingFuelTankCount(); + }, + onCompleted: () => + { + RemoveSubObjective(ref refuelObjective); + ReportWeldingFuelTankCount(); + }); + + void ReportWeldingFuelTankCount() + { + int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag("weldingfuel") && i.Condition > 1); + if (remainingOxygenTanks == 0) + { + character.Speak(TextManager.Get("DialogOutOfWeldingFuel"), null, 0.0f, "outofweldingfuel", 30.0f); + } + else if (remainingOxygenTanks < 4) + { + character.Speak(TextManager.Get("DialogLowOnWeldingFuel"), null, 0.0f, "lowonweldingfuel", 30.0f); } } - } - - if (weldingTool.OwnInventory.AllItems.None(i => i.HasTag("weldingfuel") && i.Condition > 0.0f)) - { - TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC), - onAbandon: () => Abandon = true, - onCompleted: () => RemoveSubObjective(ref refuelObjective)); return; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs index cddc1dd30..4e3e1f6a1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs @@ -42,7 +42,7 @@ namespace Barotrauma if (totalLeaks == 0) { return 0; } int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective() && !c.Character.IsIncapacitated, onlyBots: true); bool anyFixers = otherFixers > 0; - if (objectiveManager.CurrentOrder == this) + if (objectiveManager.IsOrder(this)) { float ratio = anyFixers ? totalLeaks / (float)otherFixers : 1; return Targets.Sum(t => GetLeakSeverity(t)) * ratio; @@ -72,7 +72,11 @@ namespace Barotrauma { if (gap == null) { return false; } // Don't fix a leak on a wall section set to be ignored - if (gap.ConnectedWall?.Sections?.Any(s => s.gap == gap && s.IgnoreByAI) ?? false) { return false; } + if (gap.ConnectedWall != null) + { + if (gap.ConnectedWall.Sections.Any(s => s.gap == gap && s.IgnoreByAI)) { return false; } + if (gap.ConnectedWall.MaxHealth <= 0.0f) { return false; } + } if (gap.ConnectedWall == null || gap.ConnectedDoor != null || gap.Open <= 0 || gap.linkedTo.All(l => l == null)) { return false; } if (gap.Submarine == null || character.Submarine == null) { return false; } // Don't allow going into another sub, unless it's connected and of the same team and type. diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index 1cd5d6b3d..45b90df47 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -10,6 +10,8 @@ namespace Barotrauma { public override string DebugTag => "get item"; + public override bool AbandonWhenCannotCompleteSubjectives => false; + private readonly bool equip; public HashSet ignoredItems = new HashSet(); @@ -44,6 +46,8 @@ namespace Barotrauma /// public bool AllowStealing { get; set; } + public bool TakeWholeStack { get; set; } + public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { @@ -191,8 +195,20 @@ namespace Barotrauma return; } + Inventory itemInventory = targetItem.ParentInventory; + var slots = itemInventory?.FindIndices(targetItem); if (HumanAIController.TakeItem(targetItem, character.Inventory, equip, storeUnequipped: true)) { + if (TakeWholeStack && slots != null) + { + foreach (int slot in slots) + { + foreach (Item item in itemInventory.GetItemsAt(slot).ToList()) + { + HumanAIController.TakeItem(item, character.Inventory, equip: false, storeUnequipped: true); + } + } + } IsCompleted = true; } else @@ -211,9 +227,8 @@ namespace Barotrauma return new AIObjectiveGoTo(moveToTarget, character, objectiveManager, repeat: false, getDivingGearIfNeeded: AllowToFindDivingGear, closeEnough: DefaultReach) { // If the root container changes, the item is no longer where it was (taken by someone -> need to find another item) - abortCondition = () => targetItem == null || targetItem.GetRootInventoryOwner() != moveToTarget, - DialogueIdentifier = "dialogcannotreachtarget", - TargetName = (moveToTarget as MapEntity)?.Name ?? (moveToTarget as Character)?.Name ?? moveToTarget.ToString() + abortCondition = obj => targetItem == null || targetItem.GetRootInventoryOwner() != moveToTarget, + SpeakIfFails = false }; }, onAbandon: () => @@ -240,13 +255,18 @@ namespace Barotrauma if (targetItem == null) { #if DEBUG - DebugConsole.NewMessage($"{character.Name}: Cannot find the item, because neither identifiers nor item was defined.", Color.Red); + DebugConsole.NewMessage($"{character.Name}: Cannot find an item, because neither identifiers nor item was defined.", Color.Red); #endif Abandon = true; } return; } - for (int i = 0; i < 10 && currSearchIndex < Item.ItemList.Count - 1; i++) + + float priority = Math.Clamp(objectiveManager.GetCurrentPriority(), 10, 100); + bool checkPath = priority >= AIObjectiveManager.LowestOrderPriority && (objectiveManager.IsCurrentOrder() || objectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.followControlledCharacter); + bool hasCalledPathFinder = false; + int itemsPerFrame = (int)priority; + for (int i = 0; i < itemsPerFrame && currSearchIndex < Item.ItemList.Count - 1; i++) { currSearchIndex++; var item = Item.ItemList[currSearchIndex]; @@ -259,9 +279,13 @@ namespace Barotrauma if (character.TeamID == CharacterTeamType.FriendlyNPC != item.SpawnedInOutpost) { continue; } } if (!CheckItem(item)) { continue; } - if (ignoredContainerIdentifiers != null && item.Container != null) + if (item.Container != null) { - if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; } + if (item.Container.HasTag("donttakeitems")) { continue; } + if (ignoredContainerIdentifiers != null) + { + if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; } + } } // Don't allow going into another sub, unless it's connected and of the same team and type. if (!character.Submarine.IsEntityFoundOnThisSub(item, includingConnectedSubs: true)) { continue; } @@ -287,8 +311,18 @@ namespace Barotrauma float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 10000, dist)); itemPriority *= distanceFactor; itemPriority *= item.Condition / item.MaxCondition; - //ignore if the item has a lower priority than the currently selected one + // Ignore if the item has a lower priority than the currently selected one if (itemPriority < currItemPriority) { continue; } + if (!hasCalledPathFinder && PathSteering != null && checkPath) + { + // While following the player, let's ensure that there's a valid path to the target before accepting it. + // Otherwise it will take some time for us to find a valid item when there are multiple items that we can't reach and some that we can. + // This is relatively expensive, so let's do this only when it significantly improves the behavior. + // Only allow one path find call per frame. + hasCalledPathFinder = true; + var path = PathSteering.PathFinder.FindPath(character.SimPosition, item.SimPosition, errorMsgStr: $"AIObjectiveGetItem {character.DisplayName}", nodeFilter: node => node.Waypoint.CurrentHull != null); + if (path.Unreachable) { continue; } + } currItemPriority = itemPriority; targetItem = item; moveToTarget = rootInventoryOwner ?? item; @@ -303,7 +337,7 @@ namespace Barotrauma if (!(MapEntityPrefab.List.FirstOrDefault(me => me is ItemPrefab ip && identifiersOrTags.Any(id => id == ip.Identifier || ip.Tags.Contains(id))) is ItemPrefab prefab)) { #if DEBUG - DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s) or tag(s): {string.Join(", ", identifiersOrTags)}, tried to spawn the item but no matching item prefabs were found.", Color.Yellow); + DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", identifiersOrTags)}, tried to spawn the item but no matching item prefabs were found.", Color.Yellow); #endif Abandon = true; } @@ -322,8 +356,9 @@ namespace Barotrauma else { #if DEBUG - DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s) or tag(s): {string.Join(", ", identifiersOrTags)}", Color.Yellow); + DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", identifiersOrTags)}", Color.Yellow); #endif + SpeakCannotFind(); Abandon = true; } } @@ -370,11 +405,48 @@ namespace Barotrauma /// private void ResetInternal() { - goToObjective = null; + RemoveSubObjective(ref goToObjective); targetItem = originalTarget; moveToTarget = targetItem?.GetRootInventoryOwner(); isDoneSeeking = false; currSearchIndex = 0; + currItemPriority = 0; + } + + protected override void OnAbandon() + { + base.OnAbandon(); + if (moveToTarget == null) { return; } +#if DEBUG + DebugConsole.NewMessage($"{character.Name}: Get item failed to reach {moveToTarget}", Color.Yellow); +#endif + } + + private void SpeakCannotFind() + { + // TODO: Use the item name as the variable here. + if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective) + { + string msg = TextManager.Get("dialogcannotfinditem", true); + if (msg != null) + { + character.Speak(msg, identifier: "dialogcannotfinditem", minDurationBetweenSimilar: 20.0f); + } + } + } + + // TODO: remove? + private void SpeakCannotReach() + { + if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective) + { + string TargetName = (moveToTarget as MapEntity)?.Name ?? (moveToTarget as Character)?.Name ?? moveToTarget.ToString(); + string msg = TargetName == null ? TextManager.Get("dialogcannotreachtarget", true) : TextManager.GetWithVariable("dialogcannotreachtarget", "[name]", TargetName, formatCapitals: !(moveToTarget is Character)); + if (msg != null) + { + character.Speak(msg, identifier: "dialogcannotreachtarget", minDurationBetweenSimilar: 20.0f); + } + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index ed776cb32..2afb92417 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -23,13 +23,14 @@ namespace Barotrauma /// /// Aborts the objective when this condition is true /// - public Func abortCondition; + public Func abortCondition; public Func endNodeFilter; public Func priorityGetter; public bool followControlledCharacter; public bool mimic; + public bool SpeakIfFails { get; set; } = true; public float extraDistanceWhileSwimming; public float extraDistanceOutsideSub; @@ -66,6 +67,8 @@ namespace Barotrauma public bool IgnoreIfTargetDead { get; set; } public bool AllowGoingOutside { get; set; } + public bool AlwaysUseEuclideanDistance { get; set; } = true; + public override bool AbandonWhenCannotCompleteSubjectives => !repeat; public override bool AllowOutsideSubmarine => AllowGoingOutside; @@ -80,19 +83,14 @@ namespace Barotrauma public override float GetPriority() { - bool isOrder = objectiveManager.CurrentOrder == this; + bool isOrder = objectiveManager.IsOrder(this); if (!IsAllowed) { Priority = 0; Abandon = !isOrder; return Priority; } - if (followControlledCharacter && Character.Controlled == null) - { - Priority = 0; - Abandon = !isOrder; - } - if (Target is Entity e && e.Removed) + if (Target == null || Target is Entity e && e.Removed) { Priority = 0; Abandon = !isOrder; @@ -114,7 +112,7 @@ namespace Barotrauma } else { - Priority = isOrder ? AIObjectiveManager.OrderPriority : 10; + Priority = isOrder ? objectiveManager.GetOrderPriority(this) : 10; } } return Priority; @@ -149,7 +147,7 @@ namespace Barotrauma #if DEBUG DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {Target}", Color.Yellow); #endif - if (objectiveManager.CurrentOrder != null && DialogueIdentifier != null) + if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective && DialogueIdentifier != null && SpeakIfFails) { string msg = TargetName == null ? TextManager.Get(DialogueIdentifier, true) : TextManager.GetWithVariable(DialogueIdentifier, "[name]", TargetName, formatCapitals: !(Target is Character)); if (msg != null) @@ -163,13 +161,15 @@ namespace Barotrauma { if (followControlledCharacter) { - if (Character.Controlled == null) + if (Character.Controlled != null && HumanAIController.IsFriendly(Character.Controlled)) + { + Target = Character.Controlled; + } + if (Target == null) { Abandon = true; - SteeringManager.Reset(); return; } - Target = Character.Controlled; } if (Target == character || character.SelectedBy != null && HumanAIController.IsFriendly(character.SelectedBy)) { @@ -187,7 +187,6 @@ namespace Barotrauma if (e.Removed) { Abandon = true; - SteeringManager.Reset(); return; } else @@ -199,7 +198,7 @@ namespace Barotrauma if (!followControlledCharacter) { // Abandon if going through unsafe paths. Note ignores unsafe nodes when following an order or when the objective is set to ignore unsafe hulls. - bool containsUnsafeNodes = HumanAIController.CurrentOrder == null && !HumanAIController.ObjectiveManager.CurrentObjective.IgnoreUnsafeHulls + bool containsUnsafeNodes = character.IsDismissed && !HumanAIController.ObjectiveManager.CurrentObjective.IgnoreUnsafeHulls && PathSteering != null && PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)); if (containsUnsafeNodes || HumanAIController.UnreachableHulls.Contains(targetHull)) @@ -249,16 +248,18 @@ namespace Barotrauma } } bool needsEquipment = false; + float minOxygen = character.Submarine == null ? 0 : AIObjectiveFindDivingGear.MIN_OXYGEN; if (needsDivingSuit) { - needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.MIN_OXYGEN); + needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen); } else if (needsDivingGear) { - needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.MIN_OXYGEN); + needsEquipment = !HumanAIController.HasDivingGear(character, minOxygen); } if (needsEquipment) { + SteeringManager.Reset(); if (findDivingGear != null && !findDivingGear.CanBeCompleted) { TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager), @@ -287,9 +288,14 @@ namespace Barotrauma } } } + float maxGapDistance = 500; + Character targetCharacter = Target as Character; if (character.AnimController.InWater) { - if (character.CurrentHull == null) + if (character.CurrentHull == null || + followControlledCharacter && + targetCharacter != null && (targetCharacter.CurrentHull == null) != (character.CurrentHull == null) && + Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) < maxGapDistance * maxGapDistance) { if (seekGapsTimer > 0) { @@ -297,7 +303,7 @@ namespace Barotrauma } else { - SeekGaps(maxDistance: 500); + SeekGaps(maxGapDistance); seekGapsTimer = seekGapsInterval * Rand.Range(0.1f, 1.1f); if (TargetGap != null) { @@ -326,7 +332,7 @@ namespace Barotrauma } if (TargetGap != null) { - if (TargetGap.FlowTargetHull != null && HumanAIController.SteerThroughGap(TargetGap, TargetGap.FlowTargetHull.WorldPosition, deltaTime)) + if (TargetGap.FlowTargetHull != null && HumanAIController.SteerThroughGap(TargetGap, followControlledCharacter ? Target.WorldPosition : TargetGap.FlowTargetHull.WorldPosition, deltaTime)) { SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 1); return; @@ -346,7 +352,7 @@ namespace Barotrauma float closeEnough = 250; float squaredDistance = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition); bool shouldUseScooter = squaredDistance > closeEnough * closeEnough && (!mimic || - (Target is Character targetCharacter && targetCharacter.HasEquippedItem(scooterTag, allowBroken: false)) || squaredDistance > Math.Pow(closeEnough * 2, 2)); + (targetCharacter != null && targetCharacter.HasEquippedItem(scooterTag, allowBroken: false)) || squaredDistance > Math.Pow(closeEnough * 2, 2)); if (HumanAIController.HasItem(character, scooterTag, out IEnumerable equippedScooters, recursive: false, requireEquipped: true)) { // Currently equipped scooter @@ -527,17 +533,24 @@ namespace Barotrauma { Gap selectedGap = null; float selectedDistance = -1; + Vector2 toTargetNormalized = Vector2.Normalize(Target.WorldPosition - character.WorldPosition); foreach (Gap gap in Gap.GapList) { if (gap.Open < 1) { continue; } - if (gap.FlowTargetHull == null) { continue; } - if (gap.Submarine != Target.Submarine) { continue; } - float distance = Vector2.DistanceSquared(character.WorldPosition, gap.WorldPosition); - if (distance > maxDistance * maxDistance) { continue; } - if (selectedGap == null || distance < selectedDistance) + if (gap.Submarine == null) { continue; } + if (!followControlledCharacter) + { + if (gap.FlowTargetHull == null) { continue; } + if (gap.Submarine != Target.Submarine) { continue; } + } + Vector2 toGap = gap.WorldPosition - character.WorldPosition; + if (Vector2.Dot(Vector2.Normalize(toGap), toTargetNormalized) < 0) { continue; } + float squaredDistance = toGap.LengthSquared(); + if (squaredDistance > maxDistance * maxDistance) { continue; } + if (selectedGap == null || squaredDistance < selectedDistance) { selectedGap = gap; - selectedDistance = distance; + selectedDistance = squaredDistance; } } TargetGap = selectedGap; @@ -554,6 +567,13 @@ namespace Barotrauma //otherwise characters can let go of the ladders too soon once they're close enough to the target if (PathSteering.CurrentPath.NextNode != null) { return false; } } + if (!AlwaysUseEuclideanDistance && !character.AnimController.InWater) + { + float yDiff = Math.Abs(Target.WorldPosition.Y - character.WorldPosition.Y); + if (yDiff > CloseEnough) { return false; } + float xDiff = Math.Abs(Target.WorldPosition.X - character.WorldPosition.X); + return xDiff <= CloseEnough; + } return Vector2.DistanceSquared(Target.WorldPosition, character.WorldPosition) < CloseEnough * CloseEnough; } } @@ -569,7 +589,7 @@ namespace Barotrauma Abandon = true; return false; } - if (abortCondition != null && abortCondition()) + if (abortCondition != null && abortCondition(this)) { Abandon = true; return false; @@ -617,7 +637,7 @@ namespace Barotrauma private void StopMovement() { - character.AIController.SteeringManager.Reset(); + SteeringManager.Reset(); if (Target != null) { character.AnimController.TargetDir = Target.WorldPosition.X > character.WorldPosition.X ? Direction.Right : Direction.Left; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index ae690ab40..9ec668204 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -21,9 +21,9 @@ namespace Barotrauma set { behavior = value; - if (behavior == BehaviorType.StayInHull && character.TeamID != CharacterTeamType.FriendlyNPC) + if (behavior == BehaviorType.StayInHull && TargetHull == null) { - DebugConsole.NewMessage($"AIObjectiveIdle.BehaviorType.StayInHull is implemented only for outpost NPCs. Using passive behavior for {character.Name} ({character.Info.Job.Prefab.Identifier})", color: Color.Red); + DebugConsole.AddWarning($"Trying to set a character's behavior type to StayInHull, but target hull is not set. {character.Name} ({character.Info.Job.Prefab.Identifier})"); behavior = BehaviorType.Passive; } switch (behavior) @@ -495,7 +495,7 @@ namespace Barotrauma foreach (Item item in Item.ItemList) { if (item.CurrentHull != hull) { continue; } - if (AIObjectiveCleanupItems.IsValidTarget(item, character, checkInventory: true) && !ignoredItems.Contains(item)) + if (AIObjectiveCleanupItems.IsValidTarget(item, character, checkInventory: true, allowUnloading: false) && !ignoredItems.Contains(item)) { itemsToClean.Add(item); } @@ -540,5 +540,17 @@ namespace Barotrauma ignoredItems.Clear(); autonomousObjectiveRetryTimer = 10; } + + public override void OnDeselected() + { + base.OnDeselected(); + foreach (var subObjective in SubObjectives) + { + if (subObjective is AIObjectiveCleanupItem cleanUpObjective) + { + cleanUpObjective.DropTarget(); + } + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs index a31401560..9635b79d2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs @@ -11,6 +11,7 @@ namespace Barotrauma protected HashSet ignoreList = new HashSet(); private float ignoreListTimer; protected float targetUpdateTimer; + protected virtual float TargetUpdateTimeMultiplier { get; } = 1; private float syncTimer; private readonly float syncTime = 1; @@ -61,7 +62,7 @@ namespace Barotrauma ignoreListTimer += deltaTime; } } - if (targetUpdateTimer < 0) + if (targetUpdateTimer <= 0) { UpdateTargets(); } @@ -69,9 +70,9 @@ namespace Barotrauma { targetUpdateTimer -= deltaTime; } - if (syncTimer < 0) + if (syncTimer <= 0) { - syncTimer = syncTime * Rand.Range(0.9f, 1.1f); + syncTimer = Math.Min(syncTime * Rand.Range(0.9f, 1.1f), targetUpdateTimer); // Sync objectives, subobjectives and targets foreach (var objective in Objectives) { @@ -95,7 +96,7 @@ namespace Barotrauma } // the timer is set between 1 and 10 seconds, depending on the priority modifier and a random +-25% - private float SetTargetUpdateTimer() => targetUpdateTimer = 1 / MathHelper.Clamp(PriorityModifier * Rand.Range(0.75f, 1.25f), 0.1f, 1); + private float CalculateTargetUpdateTimer() => targetUpdateTimer = 1 / MathHelper.Clamp(PriorityModifier * Rand.Range(0.75f, 1.25f), 0.1f, 1) * TargetUpdateTimeMultiplier; public override void Reset() { @@ -139,13 +140,13 @@ namespace Barotrauma } else { - if (objectiveManager.CurrentOrder == this) + if (objectiveManager.IsOrder(this)) { - Priority = ForceOrderPriority ? AIObjectiveManager.OrderPriority : targetValue; + Priority = ForceOrderPriority ? objectiveManager.GetOrderPriority(this) : targetValue; } else { - float max = MathHelper.Min(AIObjectiveManager.OrderPriority - 1, 90); + float max = AIObjectiveManager.LowestOrderPriority - 1; float value = MathHelper.Clamp((CumulatedDevotion + (targetValue * PriorityModifier)) / 100, 0, 1); Priority = MathHelper.Lerp(0, max, value); } @@ -156,7 +157,7 @@ namespace Barotrauma protected void UpdateTargets() { - SetTargetUpdateTimer(); + CalculateTargetUpdateTimer(); Targets.Clear(); FindTargets(); CreateObjectives(); @@ -167,7 +168,7 @@ namespace Barotrauma foreach (T target in GetList()) { // The bots always find targets when the objective is an order. - if (objectiveManager.CurrentOrder != this) + if (!objectiveManager.IsOrder(this)) { // Battery or pump states cannot currently be reported (not implemented) and therefore we must ignore them -> the bots always know if they require attention. bool ignore = this is AIObjectiveChargeBatteries || this is AIObjectivePumpWater; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index f5d6ff93d..efacf53b2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -1,6 +1,6 @@ using Barotrauma.Extensions; using Barotrauma.Items.Components; -using Barotrauma.Networking; +using Barotrauma.Networking; // used by the server using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -10,8 +10,8 @@ namespace Barotrauma { class AIObjectiveManager { - // TODO: expose - public const float OrderPriority = 70; + public const float HighestOrderPriority = 70; + public const float LowestOrderPriority = 60; public const float RunPriority = 50; // Constantly increases the priority of the selected objective, unless overridden public const float baseDevotion = 5; @@ -25,7 +25,6 @@ namespace Barotrauma public HumanAIController HumanAIController => character.AIController as HumanAIController; - private float _waitTimer; /// /// When set above zero, the character will stand still doing nothing until the timer runs out. Does not affect orders, find safety or combat. @@ -39,26 +38,25 @@ namespace Barotrauma } } - public AIObjective CurrentOrder { get; private set; } + public List CurrentOrders { get; } = new List(); + /// + /// The AIObjective in with the highest + /// + public AIObjective CurrentOrder + { + get + { + return ForcedOrder ?? currentOrder; + } + private set + { + currentOrder = value; + } + } + private AIObjective currentOrder; + public AIObjective ForcedOrder { get; private set; } public AIObjective CurrentObjective { get; private set; } - public bool IsCurrentOrder() where T : AIObjective => CurrentOrder is T; - public bool IsCurrentObjective() where T : AIObjective => CurrentObjective is T; - public bool IsActiveObjective() where T : AIObjective => GetActiveObjective() is T; - - public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective(); - /// - /// Returns the last active objective of the specific type. - /// - public T GetActiveObjective() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T; - - /// - /// Returns all active objectives of the specific type. Creates a new collection -> don't use too frequently. - /// - public IEnumerable GetActiveObjectives() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).Where(so => so is T).Select(so => so as T); - - public bool HasActiveObjective() where T : AIObjective => CurrentObjective is T || CurrentObjective != null && CurrentObjective.GetSubObjectivesRecursive().Any(so => so is T); - public AIObjectiveManager(Character character) { this.character = character; @@ -134,7 +132,13 @@ namespace Barotrauma } var order = new Order(orderPrefab, item ?? character.CurrentHull as Entity, orderPrefab.GetTargetItemComponent(item), orderGiver: character); if (order == null) { continue; } - if (autonomousObjective.ignoreAtOutpost && Level.IsLoadedOutpost && character.TeamID != CharacterTeamType.FriendlyNPC) { continue; } + if (autonomousObjective.ignoreAtOutpost && Level.IsLoadedOutpost && character.TeamID != CharacterTeamType.FriendlyNPC) + { + if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID)) + { + continue; + } + } var objective = CreateObjective(order, autonomousObjective.option, character, isAutonomous: true, autonomousObjective.priorityModifier); if (objective != null && objective.CanBeCompleted) { @@ -162,7 +166,11 @@ namespace Barotrauma coroutine = CoroutineManager.InvokeAfter(() => { //round ended before the coroutine finished +#if CLIENT + if (GameMain.GameSession == null || Level.Loaded == null && !(GameMain.GameSession.GameMode is TestGameMode)) { return; } +#else if (GameMain.GameSession == null || Level.Loaded == null) { return; } +#endif DelayedObjectives.Remove(objective); AddObjective(objective); callback?.Invoke(); @@ -200,21 +208,34 @@ namespace Barotrauma public void UpdateObjectives(float deltaTime) { - if (CurrentOrder != null) + UpdateOrderObjective(ForcedOrder); + + if (CurrentOrders.Any()) { + foreach(var order in CurrentOrders) + { + var orderObjective = order.Objective; + UpdateOrderObjective(orderObjective); + } + } + + void UpdateOrderObjective(AIObjective orderObjective) + { + if (orderObjective == null) { return; } #if DEBUG // Note: don't automatically remove orders here. Removing orders needs to be done via dismissing. - if (CurrentOrder.IsCompleted) + if (orderObjective.IsCompleted) { - DebugConsole.NewMessage($"{character.Name}: ORDER {CurrentOrder.DebugTag} IS COMPLETED. CURRENTLY ALL ORDERS SHOULD BE LOOPING.", Color.Red); + DebugConsole.NewMessage($"{character.Name}: ORDER {orderObjective.DebugTag} IS COMPLETED. CURRENTLY ALL ORDERS SHOULD BE LOOPING.", Color.Red); } - else if (!CurrentOrder.CanBeCompleted) + else if (!orderObjective.CanBeCompleted) { - DebugConsole.NewMessage($"{character.Name}: ORDER {CurrentOrder.DebugTag}, CANNOT BE COMPLETED.", Color.Red); + DebugConsole.NewMessage($"{character.Name}: ORDER {orderObjective.DebugTag}, CANNOT BE COMPLETED.", Color.Red); } #endif - CurrentOrder.Update(deltaTime); + orderObjective.Update(deltaTime); } + if (WaitTimer > 0) { WaitTimer -= deltaTime; @@ -248,7 +269,29 @@ namespace Barotrauma public void SortObjectives() { - CurrentOrder?.GetPriority(); + ForcedOrder?.GetPriority(); + + AIObjective orderWithHighestPriority = null; + float highestPriority = 0; + foreach (var currentOrder in CurrentOrders) + { + var orderObjective = currentOrder.Objective; + if (orderObjective == null) { continue; } + orderObjective.GetPriority(); + if (orderWithHighestPriority == null || orderObjective.Priority > highestPriority) + { + orderWithHighestPriority = orderObjective; + highestPriority = orderObjective.Priority; + } + } +#if SERVER + if (orderWithHighestPriority != null && orderWithHighestPriority != currentOrder) + { + GameMain.NetworkMember.CreateEntityEvent(character, new object[] { NetEntityEvent.Type.ObjectiveManagerOrderState }); + } +#endif + CurrentOrder = orderWithHighestPriority; + for (int i = Objectives.Count - 1; i >= 0; i--) { Objectives[i].GetPriority(); @@ -257,6 +300,7 @@ namespace Barotrauma { Objectives.Sort((x, y) => y.Priority.CompareTo(x.Priority)); } + GetCurrentObjective()?.SortSubObjectives(); } @@ -272,13 +316,19 @@ namespace Barotrauma } } - public void SetOrder(AIObjective objective) + public void SetForcedOrder(AIObjective objective) { - CurrentOrder = objective; + ForcedOrder = objective; + } + + public void ClearForcedOrder() + { + ForcedOrder = null; + SortObjectives(); } private CoroutineHandle speakRoutine; - public void SetOrder(Order order, string option, Character orderGiver, bool speak) + public void SetOrder(Order order, string option, int priority, Character orderGiver, bool speak) { if (character.IsDead) { @@ -289,8 +339,53 @@ namespace Barotrauma #endif } ClearIgnored(); - CurrentOrder = CreateObjective(order, option, orderGiver, isAutonomous: false); - if (CurrentOrder == null) + + if (order == null || order.Identifier == "dismissed") + { + if (!string.IsNullOrEmpty(option)) + { + if (CurrentOrders.Any(o => o.MatchesDismissedOrder(option))) + { + var dismissedOrderInfo = CurrentOrders.First(o => o.MatchesDismissedOrder(option)); + CurrentOrders.Remove(dismissedOrderInfo); + } + } + else + { + CurrentOrders.Clear(); + } + } + + // Make sure the order priorities reflect those set by the player + for (int i = CurrentOrders.Count - 1; i >= 0; i--) + { + var currentOrder = CurrentOrders[i]; + if (currentOrder.Objective == null || currentOrder.MatchesOrder(order, option)) + { + CurrentOrders.RemoveAt(i); + continue; + } + var currentOrderInfo = character.GetCurrentOrder(currentOrder.Order, currentOrder.OrderOption); + if (currentOrderInfo.HasValue) + { + int currentPriority = currentOrderInfo.Value.ManualPriority; + if (currentOrder.ManualPriority != currentPriority) + { + CurrentOrders[i] = new OrderInfo(currentOrder, currentPriority); + } + } + else + { + CurrentOrders.RemoveAt(i); + } + } + + var newCurrentOrder = CreateObjective(order, option, orderGiver, isAutonomous: false); + if (newCurrentOrder != null) + { + CurrentOrders.Add(new OrderInfo(order, option, priority, newCurrentOrder)); + } + if (!HasOrders()) { // Recreate objectives, because some of them may be removed, if impossible to complete (e.g. due to path finding) CreateAutonomousObjectives(); @@ -298,56 +393,57 @@ namespace Barotrauma else { // This should be redundant, because all the objectives are reset when they are selected as active. - CurrentOrder.Reset(); - if (speak) + newCurrentOrder?.Reset(); + + if (speak && character.IsOnPlayerTeam) { character.Speak(TextManager.Get("DialogAffirmative"), null, 1.0f); - if (speakRoutine != null) - { - CoroutineManager.StopCoroutines(speakRoutine); - } - speakRoutine = CoroutineManager.InvokeAfter(() => - { - if (GameMain.GameSession == null || Level.Loaded == null) { return; } - if (CurrentOrder != null && character.SpeechImpediment < 100.0f) - { - if (CurrentOrder is AIObjectiveRepairItems repairItems && repairItems.Targets.None()) - { - character.Speak(TextManager.Get("DialogNoRepairTargets"), null, 3.0f, "norepairtargets"); - } - else if (CurrentOrder is AIObjectiveChargeBatteries chargeBatteries && chargeBatteries.Targets.None()) - { - character.Speak(TextManager.Get("DialogNoBatteries"), null, 3.0f, "nobatteries"); - } - else if (CurrentOrder is AIObjectiveExtinguishFires extinguishFires && extinguishFires.Targets.None()) - { - character.Speak(TextManager.Get("DialogNoFire"), null, 3.0f, "nofire"); - } - else if (CurrentOrder is AIObjectiveFixLeaks fixLeaks && fixLeaks.Targets.None()) - { - character.Speak(TextManager.Get("DialogNoLeaks"), null, 3.0f, "noleaks"); - } - else if (CurrentOrder is AIObjectiveFightIntruders fightIntruders && fightIntruders.Targets.None()) - { - character.Speak(TextManager.Get("DialogNoEnemies"), null, 3.0f, "noenemies"); - } - else if (CurrentOrder is AIObjectiveRescueAll rescueAll && rescueAll.Targets.None()) - { - character.Speak(TextManager.Get("DialogNoRescueTargets"), null, 3.0f, "norescuetargets"); - } - else if (CurrentOrder is AIObjectivePumpWater pumpWater && pumpWater.Targets.None()) - { - character.Speak(TextManager.Get("DialogNoPumps"), null, 3.0f, "nopumps"); - } - } - }, 3); + //if (speakRoutine != null) + //{ + // CoroutineManager.StopCoroutines(speakRoutine); + //} + //speakRoutine = CoroutineManager.InvokeAfter(() => + //{ + // if (GameMain.GameSession == null || Level.Loaded == null) { return; } + // if (newCurrentOrder != null && character.SpeechImpediment < 100.0f) + // { + // if (newCurrentOrder is AIObjectiveRepairItems repairItems && repairItems.Targets.None()) + // { + // character.Speak(TextManager.Get("DialogNoRepairTargets"), null, 3.0f, "norepairtargets"); + // } + // else if (newCurrentOrder is AIObjectiveChargeBatteries chargeBatteries && chargeBatteries.Targets.None()) + // { + // character.Speak(TextManager.Get("DialogNoBatteries"), null, 3.0f, "nobatteries"); + // } + // else if (newCurrentOrder is AIObjectiveExtinguishFires extinguishFires && extinguishFires.Targets.None()) + // { + // character.Speak(TextManager.Get("DialogNoFire"), null, 3.0f, "nofire"); + // } + // else if (newCurrentOrder is AIObjectiveFixLeaks fixLeaks && fixLeaks.Targets.None()) + // { + // character.Speak(TextManager.Get("DialogNoLeaks"), null, 3.0f, "noleaks"); + // } + // else if (newCurrentOrder is AIObjectiveFightIntruders fightIntruders && fightIntruders.Targets.None()) + // { + // character.Speak(TextManager.Get("DialogNoEnemies"), null, 3.0f, "noenemies"); + // } + // else if (newCurrentOrder is AIObjectiveRescueAll rescueAll && rescueAll.Targets.None()) + // { + // character.Speak(TextManager.Get("DialogNoRescueTargets"), null, 3.0f, "norescuetargets"); + // } + // else if (newCurrentOrder is AIObjectivePumpWater pumpWater && pumpWater.Targets.None()) + // { + // character.Speak(TextManager.Get("DialogNoPumps"), null, 3.0f, "nopumps"); + // } + // } + //}, 3); } } } public AIObjective CreateObjective(Order order, string option, Character orderGiver, bool isAutonomous, float priorityModifier = 1) { - if (order == null) { return null; } + if (order == null || order.Identifier == "dismissed") { return null; } AIObjective newObjective; switch (order.Identifier.ToLowerInvariant()) { @@ -360,7 +456,7 @@ namespace Barotrauma extraDistanceWhileSwimming = 100, AllowGoingOutside = true, IgnoreIfTargetDead = true, - followControlledCharacter = orderGiver == character, + followControlledCharacter = true, mimic = true, DialogueIdentifier = "dialogcannotreachplace" }; @@ -430,7 +526,7 @@ namespace Barotrauma newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, false, priorityModifier: priorityModifier) { IsLoop = false, - Override = character.CurrentOrder != null, + Override = !character.IsDismissed, completionCondition = () => { if (float.TryParse(option, out float pct)) @@ -483,21 +579,9 @@ namespace Barotrauma return newObjective; } - private void DismissSelf() - { -#if CLIENT - if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.IsSinglePlayer) - { - GameMain.GameSession?.CrewManager?.SetCharacterOrder(character, Order.GetPrefab("dismissed"), null, character); - } -#else - GameMain.Server?.SendOrderChatMessage(new OrderChatMessage(Order.GetPrefab("dismissed"), null, null, character, character)); -#endif - } - private bool IsAllowedToWait() { - if (CurrentOrder != null) { return false; } + if (HasOrders()) { return false; } if (CurrentObjective is AIObjectiveCombat || CurrentObjective is AIObjectiveFindSafety) { return false; } if (character.AnimController.InWater) { return false; } if (character.IsClimbing) { return false; } @@ -508,5 +592,61 @@ namespace Barotrauma if (AIObjectiveIdle.IsForbidden(character.CurrentHull)) { return false; } return true; } + + public bool IsCurrentOrder() where T : AIObjective => CurrentOrder is T; + public bool IsCurrentObjective() where T : AIObjective => CurrentObjective is T; + public bool IsActiveObjective() where T : AIObjective => GetActiveObjective() is T; + + public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective(); + /// + /// Returns the last active objective of the specific type. + /// + public T GetActiveObjective() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T; + + /// + /// Returns all active objectives of the specific type. Creates a new collection -> don't use too frequently. + /// + public IEnumerable GetActiveObjectives() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).Where(so => so is T).Select(so => so as T); + + public bool HasActiveObjective() where T : AIObjective => CurrentObjective is T || CurrentObjective != null && CurrentObjective.GetSubObjectivesRecursive().Any(so => so is T); + + public bool IsOrder(AIObjective objective) + { + return objective == ForcedOrder || CurrentOrders.Any(o => o.Objective == objective); + } + + public bool HasOrders() + { + return ForcedOrder != null || CurrentOrders.Any(); + } + + public bool HasOrder() where T : AIObjective + { + return ForcedOrder is T || CurrentOrders.Any(o => o.Objective is T); + } + + public float GetOrderPriority(AIObjective objective) + { + if (objective == ForcedOrder) { return HighestOrderPriority; } + var currentOrder = CurrentOrders.FirstOrDefault(o => o.Objective == objective); + if (currentOrder.Objective == null) + { + return HighestOrderPriority; + } + else if (currentOrder.ManualPriority > 0) + { + return MathHelper.Lerp(LowestOrderPriority, HighestOrderPriority, MathUtils.InverseLerp(1, CharacterInfo.HighestManualOrderPriority, currentOrder.ManualPriority)); + } +#if DEBUG + DebugConsole.AddWarning("Error in order priority: shouldn't return 0!"); +#endif + return 0; + } + + public OrderInfo? GetCurrentOrderInfo() + { + if (currentOrder == null) { return null; } + return CurrentOrders.FirstOrDefault(o => o.Objective == CurrentOrder); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs index 1eff075eb..de9a89281 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -36,7 +36,7 @@ namespace Barotrauma public override float GetPriority() { - bool isOrder = objectiveManager.CurrentOrder == this; + bool isOrder = objectiveManager.IsOrder(this); if (!IsAllowed || character.LockHands) { Priority = 0; @@ -51,7 +51,7 @@ namespace Barotrauma { if (isOrder) { - Priority = AIObjectiveManager.OrderPriority; + Priority = objectiveManager.GetOrderPriority(this); } ItemComponent target = GetTarget(); Item targetItem = target?.Item; @@ -69,10 +69,9 @@ namespace Barotrauma { if (!isOrder) { - if (reactor.LastUserWasPlayer && character.TeamID != CharacterTeamType.FriendlyNPC || - HumanAIController.IsTrueForAnyCrewMember(c => - c.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder.GetTarget() == target)) + if (reactor.LastUserWasPlayer && character.TeamID != CharacterTeamType.FriendlyNPC) { + // The reactor was previously operated by a player -> ignore. Priority = 0; return Priority; } @@ -89,11 +88,15 @@ namespace Barotrauma case "powerup": // Check that we don't already have another order that is targeting the same item. // Without this the autonomous objective will tell the bot to turn the reactor on again. - if (objectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder != this && operateOrder.GetTarget() == target && operateOrder.Option != Option) + if (IsAnotherOrderTargetingSameItem(objectiveManager.ForcedOrder) || objectiveManager.CurrentOrders.Any(o => IsAnotherOrderTargetingSameItem(o.Objective))) { Priority = 0; return Priority; } + bool IsAnotherOrderTargetingSameItem(AIObjective objective) + { + return objective is AIObjectiveOperateItem operateObjective && operateObjective != this && operateObjective.GetTarget() == target && operateObjective.Option != Option; + } break; } } @@ -108,14 +111,23 @@ namespace Barotrauma } else { - float value = CumulatedDevotion + (AIObjectiveManager.OrderPriority * PriorityModifier); - float max = isOrder ? MathHelper.Min(AIObjectiveManager.OrderPriority, 90) : AIObjectiveManager.RunPriority - 1; - if (!isOrder && reactor != null && reactor.PowerOn && Option == "powerup") + if (isOrder) { - // Decrease the priority when targeting a reactor that is already on. - value /= 2; + float max = objectiveManager.GetOrderPriority(this); + float value = CumulatedDevotion + (max * PriorityModifier); + Priority = MathHelper.Clamp(value, 0, max); + } + else + { + float value = CumulatedDevotion + (AIObjectiveManager.LowestOrderPriority * PriorityModifier); + float max = AIObjectiveManager.LowestOrderPriority - 1; + if (reactor != null && reactor.PowerOn && reactor.FissionRate > 1 && Option == "powerup") + { + // Decrease the priority when targeting a reactor that is already on. + value /= 2; + } + Priority = MathHelper.Clamp(value, 0, max); } - Priority = MathHelper.Clamp(value, 0, max); } } return Priority; @@ -154,15 +166,18 @@ namespace Barotrauma ItemComponent target = GetTarget(); if (useController && controller == null) { - character.Speak(TextManager.GetWithVariable("DialogCantFindController", "[item]", component.Item.Name, true), null, 2.0f, "cantfindcontroller", 30.0f); + if (character.IsOnPlayerTeam) + { + character.Speak(TextManager.GetWithVariable("DialogCantFindController", "[item]", component.Item.Name, true), null, 2.0f, "cantfindcontroller", 30.0f); + } Abandon = true; return; } if (operateTarget != null) { - if (HumanAIController.IsTrueForAnyCrewMember(other => other != HumanAIController && other.ObjectiveManager.GetActiveObjective() is AIObjectiveOperateItem operateObjective && operateObjective.operateTarget == operateTarget)) + if (HumanAIController.IsTrueForAnyCrewMember(other => other != HumanAIController && other.Character.IsBot && other.ObjectiveManager.GetActiveObjective() is AIObjectiveOperateItem operateObjective && operateObjective.operateTarget == operateTarget)) { - // Another crew member is already targeting this entity. + // Another crew member is already targeting this entity (leak). Abandon = true; return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs index 738de3d32..6e4046bb5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -59,13 +59,13 @@ namespace Barotrauma float dist = Math.Abs(character.WorldPosition.X - Item.WorldPosition.X) + yDist; distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 4000, dist)); } - float requiredSuccessFactor = objectiveManager.IsCurrentOrder() ? 0 : AIObjectiveRepairItems.RequiredSuccessFactor; + float requiredSuccessFactor = objectiveManager.HasOrder() ? 0 : AIObjectiveRepairItems.RequiredSuccessFactor; float severity = isPriority ? 1 : AIObjectiveRepairItems.GetTargetPriority(Item, character, requiredSuccessFactor) / 100; bool isSelected = IsRepairing(); float selectedBonus = isSelected ? 100 - MaxDevotion : 0; float devotion = (CumulatedDevotion + selectedBonus) / 100; float reduction = isPriority ? 1 : isSelected ? 2 : 3; - float max = MathHelper.Min(AIObjectiveManager.OrderPriority - reduction, 90); + float max = AIObjectiveManager.LowestOrderPriority - reduction; Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1)); } return Priority; @@ -74,7 +74,7 @@ namespace Barotrauma protected override bool Check() { IsCompleted = Item.IsFullCondition; - if (IsCompleted && IsRepairing()) + if (character.IsOnPlayerTeam && IsCompleted && IsRepairing()) { character.Speak(TextManager.GetWithVariable("DialogItemRepaired", "[itemname]", Item.Name, true), null, 0.0f, "itemrepaired", 10.0f); } @@ -97,7 +97,10 @@ namespace Barotrauma var getItemObjective = new AIObjectiveGetItem(character, requiredItem.Identifiers, objectiveManager, true); if (objectiveManager.IsCurrentOrder()) { - getItemObjective.Abandoned += () => character.Speak(TextManager.Get("dialogcannotfindrequireditemtorepair"), null, 0.0f, "dialogcannotfindrequireditemtorepair", 10.0f); + if (character.IsOnPlayerTeam) + { + getItemObjective.Abandoned += () => character.Speak(TextManager.Get("dialogcannotfindrequireditemtorepair"), null, 0.0f, "dialogcannotfindrequireditemtorepair", 10.0f); + } } subObjectives.Add(getItemObjective); } @@ -119,27 +122,8 @@ namespace Barotrauma Abandon = true; return; } - // Eject empty tanks - if (repairTool.Item.OwnInventory.AllItems.Any(it => it.Condition <= 0.0f)) - { - foreach (Item containedItem in repairTool.Item.OwnInventory.AllItemsMod) - { - if (containedItem == null) { continue; } - if (containedItem.Condition <= 0.0f) - { - if (character.Submarine == null) - { - // If we are outside of main sub, try to put the tank in the inventory instead dropping it in the sea. - if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.anySlot)) - { - continue; - } - } - containedItem.Drop(character); - } - } - } - + HumanAIController.UnequipContainedItems(repairTool.Item, it => !it.HasTag("weldingfuel")); + HumanAIController.UnequipEmptyItems(repairTool.Item); RelatedItem item = null; Item fuel = null; foreach (RelatedItem requiredItem in repairTool.requiredItems[RelatedItem.RelationType.Contained]) @@ -193,7 +177,7 @@ namespace Barotrauma } if (Abandon) { - if (IsRepairing()) + if (character.IsOnPlayerTeam && IsRepairing()) { character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f); } @@ -228,7 +212,7 @@ namespace Barotrauma onAbandon: () => { Abandon = true; - if (IsRepairing()) + if (character.IsOnPlayerTeam && IsRepairing()) { character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs index aa3f65c4a..1c020742f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs @@ -104,7 +104,7 @@ namespace Barotrauma } bool anyFixers = otherFixers > 0; float ratio = anyFixers ? items / (float)otherFixers : 1; - if (objectiveManager.CurrentOrder == this) + if (objectiveManager.IsOrder(this)) { return Targets.Sum(t => 100 - t.ConditionPercentage); } @@ -153,6 +153,8 @@ namespace Barotrauma if (item.IsFullCondition) { return false; } if (item.CurrentHull == null) { return false; } if (item.Submarine == null || character.Submarine == null) { return false; } + //player crew ignores items in outposts + if (character.IsOnPlayerTeam && item.Submarine.Info.IsOutpost) { return false; } if (!character.Submarine.IsEntityFoundOnThisSub(item, includingConnectedSubs: true)) { return false; } if (item.Repairables.None()) { return false; } return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index f17b9accd..3c0fdd972 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -78,14 +78,14 @@ namespace Barotrauma // Check if the character needs more oxygen if (!ignoreOxygen && character.SelectedCharacter == targetCharacter || character.CanInteractWith(targetCharacter)) { - // Replace empty oxygen tank - // First remove empty tanks + // Replace empty oxygen and welding fuel. if (HumanAIController.HasItem(targetCharacter, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out IEnumerable suits, requireEquipped: true)) { Item suit = suits.FirstOrDefault(); if (suit != null) { - AIObjectiveFindDivingGear.EjectEmptyTanks(character, suit, out _); + AIController.UnequipEmptyItems(character, suit); + AIController.UnequipContainedItems(character, suit, it => it.HasTag("weldingfuel")); } } else if (HumanAIController.HasItem(targetCharacter, AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR, out IEnumerable masks, requireEquipped: true)) @@ -93,7 +93,8 @@ namespace Barotrauma Item mask = masks.FirstOrDefault(); if (mask != null) { - AIObjectiveFindDivingGear.EjectEmptyTanks(character, mask, out _); + AIController.UnequipEmptyItems(character, mask); + AIController.UnequipContainedItems(character, mask, it => it.HasTag("weldingfuel")); } } bool ShouldRemoveDivingSuit() => targetCharacter.OxygenAvailable < CharacterHealth.InsufficientOxygenThreshold && targetCharacter.CurrentHull?.LethalPressure <= 0; @@ -322,7 +323,7 @@ namespace Barotrauma { itemListStr = string.Join(" or ", string.Join(", ", itemNameList.Take(itemNameList.Count - 1)), itemNameList.Last()); } - if (targetCharacter != character) + if (targetCharacter != character && character.IsOnPlayerTeam) { character.Speak(TextManager.GetWithVariables("DialogListRequiredTreatments", new string[2] { "[targetname]", "[treatmentlist]" }, new string[2] { targetCharacter.Name, itemListStr }, new bool[2] { false, true }), @@ -336,7 +337,10 @@ namespace Barotrauma onAbandon: () => { Abandon = true; - character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: false), identifier: "cannottreatpatient", minDurationBetweenSimilar: 20.0f); + if (character != targetCharacter && character.IsOnPlayerTeam) + { + character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: false), identifier: "cannottreatpatient", minDurationBetweenSimilar: 20.0f); + } }); } } @@ -383,8 +387,10 @@ namespace Barotrauma Abandon = true; return false; } - bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter); - if (isCompleted && targetCharacter != character) + bool isCompleted = + AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter) || + targetCharacter.CharacterHealth.GetAllAfflictions().All(a => a.Strength < a.Prefab.TreatmentThreshold); + if (isCompleted && targetCharacter != character && character.IsOnPlayerTeam) { character.Speak(TextManager.GetWithVariable("DialogTargetHealed", "[targetname]", targetCharacter.Name), null, 1.0f, "targethealed" + targetCharacter.Name, 60.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs index 11a7cfb38..70147a07a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs @@ -25,8 +25,8 @@ namespace Barotrauma { // When targeting player characters, always treat them when ordered, else use the threshold so that minor/non-severe damage is ignored. // If we ignore any damage when the player orders a bot to do healings, it's observed to cause confusion among the players. - // On the other hand, if the bots too eagerly heal characters when it's not nevessary, it's inefficient and can feel frustrating, because it can't be controlled. - return character == target || manager.CurrentOrder is AIObjectiveRescueAll ? (target.IsPlayer ? 100 : vitalityThresholdForOrders) : vitalityThreshold; + // On the other hand, if the bots too eagerly heal characters when it's not necessary, it's inefficient and can feel frustrating, because it can't be controlled. + return character == target || manager.HasOrder() ? (target.IsPlayer ? 100 : vitalityThresholdForOrders) : vitalityThreshold; } } @@ -40,7 +40,7 @@ namespace Barotrauma protected override float TargetEvaluation() { if (Targets.None()) { return 100; } - if (objectiveManager.CurrentOrder != this) + if (!objectiveManager.IsOrder(this)) { if (!character.IsMedic && HumanAIController.IsTrueForAnyCrewMember(c => c != HumanAIController && c.Character.IsMedic && !c.Character.IsUnconscious)) { @@ -82,8 +82,12 @@ namespace Barotrauma if (!HumanAIController.IsFriendly(character, target, onlySameTeam: true)) { return false; } if (character.AIController is HumanAIController humanAI) { - if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target)) { return false; } - if (!humanAI.ObjectiveManager.IsCurrentOrder()) + if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target) || + target.CharacterHealth.GetAllAfflictions().All(a => a.Strength < a.Prefab.TreatmentThreshold)) + { + return false; + } + if (!humanAI.ObjectiveManager.HasOrder()) { if (!character.IsMedic && target != character) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index 0a970aca9..fd318964e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -19,27 +19,62 @@ namespace Barotrauma struct OrderInfo { - public string ComponentIdentifier { get; set; } - public Order Order { get; private set; } - public string OrderOption { get; private set; } + public Order Order { get; } + public string OrderOption { get; } + public int ManualPriority { get; } + public OrderType Type { get; } + public AIObjective Objective { get; } + public bool IsCurrentOrder => Type == OrderType.Current; - public OrderInfo(Order order, string orderOption) + public enum OrderType + { + Current, + Previous + } + + private OrderInfo(Order order, string orderOption, int manualPriority, OrderType orderType, AIObjective objective) { - ComponentIdentifier = "currentorder"; Order = order; OrderOption = orderOption; + ManualPriority = Math.Min(manualPriority, CharacterInfo.HighestManualOrderPriority); + Type = orderType; + Objective = objective; } - public OrderInfo(OrderInfo orderInfo) - { - ComponentIdentifier = "previousorder"; - Order = orderInfo.Order; - OrderOption = orderInfo.OrderOption; - } + public OrderInfo(Order order, string orderOption, int manualPriority) : this(order, orderOption, manualPriority, OrderType.Current, null) { } + + public OrderInfo(Order order, string orderOption, int manualPriority, AIObjective objective) : this(order, orderOption, manualPriority, OrderType.Current, objective) { } + + public OrderInfo(OrderInfo orderInfo, int manualPriority) : this(orderInfo.Order, orderInfo.OrderOption, manualPriority, orderInfo.Type, orderInfo.Objective) { } + + public OrderInfo(OrderInfo orderInfo, OrderType type) : this(orderInfo.Order, orderInfo.OrderOption, orderInfo.ManualPriority, type, orderInfo.Objective) { } + + public bool MatchesOrder(string orderIdentifier, string orderOption) => + (orderIdentifier == Order?.Identifier || (string.IsNullOrEmpty(orderIdentifier) && string.IsNullOrEmpty(Order?.Identifier))) && + (orderOption == OrderOption || (string.IsNullOrEmpty(orderOption) && string.IsNullOrEmpty(OrderOption))); public bool MatchesOrder(Order order, string option) => - order.Identifier == Order.Identifier && - option == OrderOption; + MatchesOrder(order?.Identifier, option); + + public bool MatchesOrder(OrderInfo orderInfo) => + MatchesOrder(orderInfo.Order?.Identifier, orderInfo.OrderOption); + + public bool MatchesDismissedOrder(string dismissOrderOption) + { + string[] dismissedOrder = dismissOrderOption?.Split('.'); + if (dismissedOrder != null && dismissedOrder.Length > 0) + { + string dismissedOrderIdentifier = dismissedOrder.Length > 0 ? dismissedOrder[0] : null; + if (dismissedOrderIdentifier == null || dismissedOrderIdentifier != Order?.Identifier) { return false; } + string dismissedOrderOption = dismissedOrder.Length > 1 ? dismissedOrder[1] : null; + if (dismissedOrderOption == null && string.IsNullOrEmpty(OrderOption)) { return true; } + return dismissedOrderOption == OrderOption; + } + else + { + return false; + } + } } class Order @@ -412,7 +447,7 @@ namespace Barotrauma orderOption ??= ""; string messageTag = (givingOrderToSelf && !TargetAllCharacters ? "OrderDialogSelf." : "OrderDialog.") + Identifier; - if (!string.IsNullOrEmpty(orderOption)) { messageTag += "." + orderOption; } + if (Identifier != "dismissed" && !string.IsNullOrEmpty(orderOption)) { messageTag += "." + orderOption; } if (targetCharacterName == null) { targetCharacterName = ""; } if (targetRoomName == null) { targetRoomName = ""; } @@ -498,5 +533,23 @@ namespace Barotrauma if (index < 0 || index >= Options.Length) { return null; } return GetOptionName(Options[index]); } + + /// + /// Used to create the order option for the Dismiss order to know which order it targets + /// + /// The order to target with the dismiss order + public static string GetDismissOrderOption(OrderInfo orderInfo) + { + if (orderInfo.Order != null) + { + string option = orderInfo.Order.Identifier; + if (!string.IsNullOrEmpty(orderInfo.OrderOption)) + { + option += $".{orderInfo.OrderOption}"; + } + return option; + } + return ""; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs index 92de10a84..08ed530f3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs @@ -35,7 +35,7 @@ namespace Barotrauma WayPointID = Waypoint.ID; } - public static List GenerateNodes(List wayPoints) + public static List GenerateNodes(List wayPoints, bool removeOrphans) { var nodes = new Dictionary(); foreach (WayPoint wayPoint in wayPoints) @@ -63,7 +63,10 @@ namespace Barotrauma } var nodeList = nodes.Values.ToList(); - nodeList.RemoveAll(n => n.connections.Count == 0); + if (removeOrphans) + { + nodeList.RemoveAll(n => n.connections.Count == 0); + } foreach (PathNode node in nodeList) { node.distances = new List(); @@ -90,7 +93,7 @@ namespace Barotrauma public PathFinder(List wayPoints, bool indoorsSteering = false) { - nodes = PathNode.GenerateNodes(wayPoints.FindAll(w => w.Submarine != null == indoorsSteering)); + nodes = PathNode.GenerateNodes(wayPoints.FindAll(w => w.Submarine != null == indoorsSteering), removeOrphans: true); foreach (WayPoint wp in wayPoints) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringManager.cs index 290a8a3d0..780ec45e4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringManager.cs @@ -94,20 +94,24 @@ namespace Barotrauma { Vector2 targetVel = target - host.SimPosition; - if (targetVel.LengthSquared() < 0.00001f) return Vector2.Zero; + if (targetVel.LengthSquared() < 0.00001f) { return Vector2.Zero; } targetVel = Vector2.Normalize(targetVel) * weight; - Vector2 newSteering = targetVel - host.Steering; + // TODO: the code below doesn't quite work as it should, and I'm not sure what the purpose of it is/was. + // So, we'll just return the targetVel for now, as it produces smooth results. + return targetVel; - if (newSteering == Vector2.Zero) return Vector2.Zero; + //Vector2 newSteering = targetVel - host.Steering; - float steeringSpeed = (newSteering + host.Steering).Length(); - if (steeringSpeed > Math.Abs(weight)) - { - newSteering = Vector2.Normalize(newSteering) * Math.Abs(weight); - } + //if (newSteering == Vector2.Zero) return Vector2.Zero; - return newSteering; + //float steeringSpeed = (newSteering + host.Steering).Length(); + //if (steeringSpeed > Math.Abs(weight)) + //{ + // newSteering = Vector2.Normalize(newSteering) * Math.Abs(weight); + //} + + //return newSteering; } protected virtual Vector2 DoSteeringWander(float weight) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs index 4ec8d6ee6..2f961341e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs @@ -35,7 +35,7 @@ namespace Barotrauma private static IEnumerable GetThalamusEntities(Submarine wreck, string tag) => MapEntity.mapEntityList.Where(e => e.Submarine == wreck && e.prefab != null && IsThalamus(e.prefab, tag)); - private static bool IsThalamus(MapEntityPrefab entityPrefab, string tag) => entityPrefab.Category == MapEntityCategory.Thalamus || entityPrefab.Tags.Contains(tag); + private static bool IsThalamus(MapEntityPrefab entityPrefab, string tag) => entityPrefab.HasSubCategory("thalamus") || entityPrefab.Tags.Contains(tag); public WreckAI(Submarine wreck) { @@ -246,7 +246,7 @@ namespace Barotrauma initialCellsSpawned = true; } - private void Kill() + public void Kill() { thalamusItems.ForEach(i => i.Condition = 0); foreach (var turret in turrets) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs index 98b7f762f..6904391ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs @@ -1,18 +1,9 @@ using Microsoft.Xna.Framework; -using System; namespace Barotrauma { partial class AICharacter : Character - { - //characters that are further than this from the camera (and all clients) - //have all their limb physics bodies disabled - const float EnableSimplePhysicsDist = 6000.0f; - const float DisableSimplePhysicsDist = EnableSimplePhysicsDist * 0.9f; - - const float EnableSimplePhysicsDistSqr = EnableSimplePhysicsDist * EnableSimplePhysicsDist; - const float DisableSimplePhysicsDistSqr = DisableSimplePhysicsDist * DisableSimplePhysicsDist; - + { private AIController aiController; public override AIController AIController @@ -20,8 +11,8 @@ namespace Barotrauma get { return aiController; } } - public AICharacter(CharacterPrefab prefab, string speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, bool isNetworkPlayer = false, RagdollParams ragdoll = null) - : base(prefab, speciesName, position, seed, characterInfo, id: Entity.NullEntityID, isRemotePlayer: isNetworkPlayer, ragdollParams: ragdoll) + public AICharacter(CharacterPrefab prefab, string speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isNetworkPlayer = false, RagdollParams ragdoll = null) + : base(prefab, speciesName, position, seed, characterInfo, id: id, isRemotePlayer: isNetworkPlayer, ragdollParams: ragdoll) { InitProjSpecific(); } @@ -63,11 +54,11 @@ namespace Barotrauma if (!IsRemotePlayer && !(AIController is HumanAIController)) { float characterDistSqr = GetDistanceSqrToClosestPlayer(); - if (characterDistSqr > EnableSimplePhysicsDistSqr) + if (characterDistSqr > MathUtils.Pow2(Params.DisableDistance * 0.5f)) { AnimController.SimplePhysicsEnabled = true; } - else if (characterDistSqr < DisableSimplePhysicsDistSqr) + else if (characterDistSqr < MathUtils.Pow2(Params.DisableDistance * 0.5f * 0.9f)) { AnimController.SimplePhysicsEnabled = false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index cf8585f14..0ca3c9c1c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -423,23 +423,25 @@ namespace Barotrauma if (CurrentSwimParams == null) { return; } movement = TargetMovement; bool isMoving = movement.LengthSquared() > 0.00001f; + var mainLimb = MainLimb; if (isMoving) { float t = 0.5f; - if (CurrentSwimParams.RotateTowardsMovement && VectorExtensions.Angle(VectorExtensions.Forward(Collider.Rotation + MathHelper.PiOver2), movement) > MathHelper.PiOver2) + if (!SimplePhysicsEnabled && CurrentSwimParams.RotateTowardsMovement) { - // Reduce the linear movement speed when not facing the movement direction - t /= 5; + Vector2 forward = VectorExtensions.Forward(Collider.Rotation + MathHelper.PiOver2); + float dot = Vector2.Dot(forward, Vector2.Normalize(movement)); + if (dot < 0) + { + // Reduce the linear movement speed when not facing the movement direction + t = MathHelper.Clamp((1 + dot) / 10, 0.01f, 0.1f); + } } Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t); } - //limbs are disabled when simple physics is enabled, no need to move them if (SimplePhysicsEnabled) { return; } - var mainLimb = MainLimb; mainLimb.PullJointEnabled = true; - //mainLimb.PullJointWorldAnchorB = Collider.SimPosition; - if (!isMoving) { WalkPos = MathHelper.SmoothStep(WalkPos, MathHelper.PiOver2, deltaTime * 5); @@ -645,7 +647,7 @@ namespace Barotrauma } if (limb.Params.BlinkFrequency > 0) { - limb.Blink(deltaTime, MainLimb.Rotation); + limb.UpdateBlink(deltaTime, MainLimb.Rotation); } } @@ -787,7 +789,7 @@ namespace Barotrauma } if (limb.Params.BlinkFrequency > 0) { - limb.Blink(deltaTime, MainLimb.Rotation); + limb.UpdateBlink(deltaTime, MainLimb.Rotation); } switch (limb.type) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index b601d6b30..eae42212a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -281,7 +281,7 @@ namespace Barotrauma } } - public const float MAX_SPEED = 15; + public const float MAX_SPEED = 20; public Vector2 TargetMovement { @@ -472,7 +472,7 @@ namespace Barotrauma if (joint == null) { continue; } float angle = (joint.LowerLimit + joint.UpperLimit) / 2.0f; joint.LimbB?.body?.SetTransform( - (joint.WorldAnchorA - MathUtils.RotatePointAroundTarget(joint.LocalAnchorB, Vector2.Zero, MathHelper.ToDegrees(joint.BodyA.Rotation + angle), true)), + (joint.WorldAnchorA - MathUtils.RotatePointAroundTarget(joint.LocalAnchorB, Vector2.Zero, joint.BodyA.Rotation + angle, true)), joint.BodyA.Rotation + angle); } } @@ -636,9 +636,12 @@ namespace Barotrauma //always collides with bodies other than structures if (!(f2.Body.UserData is Structure structure)) { - lock (impactQueue) + if (!f2.IsSensor) { - impactQueue.Enqueue(new Impact(f1, f2, contact, velocity)); + lock (impactQueue) + { + impactQueue.Enqueue(new Impact(f1, f2, contact, velocity)); + } } return true; } @@ -1120,6 +1123,32 @@ namespace Barotrauma splashSoundTimer -= deltaTime; + if (character.Submarine == null && Level.Loaded != null) + { + if (Collider.SimPosition.Y > Level.Loaded.TopBarrier.Position.Y) + { + Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, Math.Min(Collider.LinearVelocity.Y, -1)); + } + else if (Collider.SimPosition.Y < Level.Loaded.BottomBarrier.Position.Y) + { + Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, + MathHelper.Clamp(Collider.LinearVelocity.Y, Level.Loaded.BottomBarrier.Position.Y - Collider.SimPosition.Y, 10.0f)); + } + foreach (Limb limb in Limbs) + { + if (limb.SimPosition.Y > Level.Loaded.TopBarrier.Position.Y) + { + limb.body.LinearVelocity = new Vector2(limb.LinearVelocity.X, Math.Min(limb.LinearVelocity.Y, -1)); + } + else if (limb.SimPosition.Y < Level.Loaded.BottomBarrier.Position.Y) + { + limb.body.LinearVelocity = new Vector2( + limb.LinearVelocity.X, + MathHelper.Clamp(limb.LinearVelocity.Y, Level.Loaded.BottomBarrier.Position.Y - limb.SimPosition.Y, 10.0f)); + } + } + } + if (forceStanding) { inWater = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index 0271f0f03..247b898b4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -217,6 +217,9 @@ namespace Barotrauma [Serialize("0.0, 0.0", true, description: "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards). The attacker's facing direction is taken into account."), Editable] public Vector2 TargetForceWorld { get; private set; } + [Serialize(1.0f, true, description: "Affects the strength of the impact effects the limb causes when it hits a submarine."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] + public float SubmarineImpactMultiplier { get; private set; } + [Serialize(0.0f, true, description: "How likely the attack causes target limbs to be severed."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] public float SeverLimbsProbability { get; set; } @@ -228,6 +231,9 @@ namespace Barotrauma [Serialize(0.0f, true, description: ""), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] public float Priority { get; private set; } + [Serialize(false, true, description: ""), Editable] + public bool Blink { get; private set; } + public IEnumerable StatusEffects { get { return statusEffects; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 9b2ad9925..9367444cb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -69,8 +69,8 @@ namespace Barotrauma /// public bool IsRemotelyControlled { - get - { + get + { if (GameMain.NetworkMember == null) { return false; @@ -145,17 +145,13 @@ namespace Barotrauma } private readonly List lastAttackers = new List(); - public IEnumerable LastAttackers - { - get { return lastAttackers; } - } - public Character LastAttacker - { - get { return lastAttackers.Count > 0 ? lastAttackers[lastAttackers.Count - 1].Character : null; } - } + public IEnumerable LastAttackers => lastAttackers; + public Character LastAttacker => lastAttackers.LastOrDefault()?.Character; public Entity LastDamageSource; + public AttackResult LastDamage; + public float InvisibleTimer; private CharacterPrefab prefab; @@ -199,7 +195,12 @@ namespace Barotrauma set => Params.Visibility = value; } - public bool IsTraitor; + public bool IsTraitor + { + get; + set; + } + public string TraitorCurrentObjective = ""; public bool IsHuman => SpeciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase); public bool IsMale => Info != null && Info.HasGenders && Info.Gender == Gender.Male; @@ -207,31 +208,8 @@ namespace Barotrauma private float attackCoolDown; - public Order CurrentOrder - { - get - { - return Info?.CurrentOrder; - } - private set - { - if (Info != null) { Info.CurrentOrder = value; } - } - } - - public string CurrentOrderOption - { - get - { - return Info?.CurrentOrderOption; - } - private set - { - if (Info != null) { Info.CurrentOrderOption = value; } - } - } - - public bool IsDismissed => Info != null && Info.IsDismissed; + public List CurrentOrders => Info?.CurrentOrders; + public bool IsDismissed => !GetCurrentOrderWithTopPriority().HasValue; private readonly List statusEffects = new List(); @@ -356,6 +334,7 @@ namespace Barotrauma //text displayed when the character is highlighted if custom interact is set public string customInteractHUDText; private Action onCustomInteract; + public ConversationAction ActiveConversation; public bool AllowCustomInteract { @@ -372,6 +351,9 @@ namespace Barotrauma set { lockHandsTimer = MathHelper.Clamp(lockHandsTimer + (value ? 1.0f : -0.5f), 0.0f, 10.0f); +#if CLIENT + HintManager.OnHandcuffed(this); +#endif } } @@ -454,7 +436,7 @@ namespace Barotrauma /// public IEnumerable HeldItems { - get + get { var item1 = Inventory?.GetItemInLimbSlot(InvSlotType.RightHand); var item2 = Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand); @@ -483,16 +465,21 @@ namespace Barotrauma } } + private double pressureProtectionLastSet; private float pressureProtection; public float PressureProtection { get { return pressureProtection; } set { - pressureProtection = MathHelper.Clamp(value, 0.0f, 100.0f); + pressureProtection = Math.Max(value, 0.0f); + pressureProtectionLastSet = Timing.TotalTime; } } + public const float KnockbackCooldown = 5.0f; + public float KnockbackCooldownTimer; + private float ragdollingLockTimer; public bool IsRagdolled; public bool IsForceRagdolled; @@ -534,14 +521,13 @@ namespace Barotrauma } public bool UseHullOxygen { get; set; } = true; - + public float Stun { - get { return IsRagdolled ? 1.0f : CharacterHealth.StunTimer; } + get { return IsRagdolled ? 1.0f : CharacterHealth.Stun; } set { - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) return; - + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } SetStun(value, true); } } @@ -609,7 +595,7 @@ namespace Barotrauma { get; set; - } + } /// /// Current speed of the character's collider. Can be used by status effects to check if the character is moving. @@ -625,8 +611,12 @@ namespace Barotrauma get => _selectedConstruction; set { +#if CLIENT + var prevSelectedConstruction = _selectedConstruction; +#endif _selectedConstruction = value; #if CLIENT + HintManager.OnSetSelectedConstruction(this, prevSelectedConstruction, _selectedConstruction); if (Controlled == this) { if (_selectedConstruction == null) @@ -660,11 +650,11 @@ namespace Barotrauma } private bool isDead; - public bool IsDead - { + public bool IsDead + { get { return isDead; } - set - { + set + { if (isDead == value) { return; } if (value) { @@ -703,7 +693,7 @@ namespace Barotrauma { if (!canBeDragged) { return false; } if (Removed || !AnimController.Draggable) { return false; } - return IsDead || Stun > 0.0f || LockHands || IsIncapacitated || IsPet; + return IsKnockedDown || LockHands || IsPet; } set { canBeDragged = value; } } @@ -721,7 +711,7 @@ namespace Barotrauma } else { - return IsDead || Stun > 0.0f || LockHands || IsIncapacitated; + return IsKnockedDown || LockHands; } } set { canInventoryBeAccessed = value; } @@ -827,7 +817,7 @@ namespace Barotrauma speciesName = Path.GetFileNameWithoutExtension(speciesName).ToLowerInvariant(); } - var prefab = CharacterPrefab.FindBySpeciesName(speciesName); + var prefab = CharacterPrefab.FindBySpeciesName(speciesName); if (prefab == null) { DebugConsole.ThrowError($"Failed to create character \"{speciesName}\". Matching prefab not found.\n" + Environment.StackTrace); @@ -837,21 +827,21 @@ namespace Barotrauma Character newCharacter = null; if (!speciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase)) { - var aiCharacter = new AICharacter(prefab, speciesName, position, seed, characterInfo, isRemotePlayer, ragdoll); + var aiCharacter = new AICharacter(prefab, speciesName, position, seed, characterInfo, id, isRemotePlayer, ragdoll); var ai = new EnemyAIController(aiCharacter, seed); aiCharacter.SetAI(ai); newCharacter = aiCharacter; } else if (hasAi) { - var aiCharacter = new AICharacter(prefab, speciesName, position, seed, characterInfo, isRemotePlayer, ragdoll); + var aiCharacter = new AICharacter(prefab, speciesName, position, seed, characterInfo, id, isRemotePlayer, ragdoll); var ai = new HumanAIController(aiCharacter); aiCharacter.SetAI(ai); newCharacter = aiCharacter; } else { - newCharacter = new Character(prefab, speciesName, position, seed, characterInfo, id: id, isRemotePlayer: isRemotePlayer, ragdollParams: ragdoll); + newCharacter = new Character(prefab, speciesName, position, seed, characterInfo, id, isRemotePlayer, ragdoll); } float healthRegen = newCharacter.Params.Health.ConstantHealthRegeneration; @@ -1022,7 +1012,7 @@ namespace Barotrauma { // Get the non husked name and find the ragdoll with it var matchingAffliction = AfflictionPrefab.List - .Where(p => p.AfflictionType == "huskinfection") + .Where(p => p is AfflictionPrefabHusk) .Select(p => p as AfflictionPrefabHusk) .FirstOrDefault(p => p.TargetSpecies.Any(t => t.Equals(AfflictionHusk.GetNonHuskedSpeciesName(speciesName, p), StringComparison.OrdinalIgnoreCase))); string nonHuskedSpeciesName = string.Empty; @@ -1058,7 +1048,7 @@ namespace Barotrauma else { AnimController = new FishAnimController(this, seed, ragdollParams as FishRagdollParams); - PressureProtection = 100.0f; + PressureProtection = int.MaxValue; } AnimController.SetPosition(ConvertUnits.ToSimUnits(position)); @@ -1277,7 +1267,13 @@ namespace Barotrauma public float GetSkillLevel(string skillIdentifier) { - return (Info == null || Info.Job == null) ? 0.0f : Info.Job.GetSkillLevel(skillIdentifier); + if (Info?.Job == null) { return 0.0f; } + float skillLevel = Info.Job.GetSkillLevel(skillIdentifier); + foreach (Affliction affliction in CharacterHealth.GetAllAfflictions()) + { + skillLevel *= affliction.GetSkillMultiplier(); + } + return skillLevel; } // TODO: reposition? there's also the overrideTargetMovement variable, but it's not in the same manner @@ -1348,6 +1344,22 @@ namespace Barotrauma /// public float SpeedMultiplier { get; private set; } = 1; + + private double propulsionSpeedMultiplierLastSet; + private float propulsionSpeedMultiplier; + /// + /// Can be used to modify the speed at which Propulsion ItemComponents move the character via StatusEffects (e.g. heavy suit can slow down underwater scooters) + /// + public float PropulsionSpeedMultiplier + { + get { return propulsionSpeedMultiplier; } + set + { + propulsionSpeedMultiplier = value; + propulsionSpeedMultiplierLastSet = Timing.TotalTime; + } + } + public void StackSpeedMultiplier(float val) { if (val < 1f) @@ -1370,6 +1382,10 @@ namespace Barotrauma { greatestPositiveSpeedMultiplier = 1f; greatestNegativeSpeedMultiplier = 1f; + if (Timing.TotalTime > propulsionSpeedMultiplierLastSet + 0.1) + { + propulsionSpeedMultiplier = 1.0f; + } } private float greatestNegativeHealthMultiplier = 1f; @@ -1681,6 +1697,12 @@ namespace Barotrauma { item.Use(deltaTime, this); } +#if CLIENT + else if (item.RequireAimToUse && !IsKeyDown(InputType.Aim)) + { + HintManager.OnShootWithoutAiming(this, item); + } +#endif } } } @@ -1873,6 +1895,19 @@ namespace Barotrauma return false; } + public Item GetEquippedItem(string tagOrIdentifier) + { + if (Inventory == null) { return null; } + for (int i = 0; i < Inventory.Capacity; i++) + { + if (Inventory.SlotTypes[i] == InvSlotType.Any) { continue; } + var item = Inventory.GetItemAt(i); + if (item == null) { continue; } + if (item.Prefab.Identifier == tagOrIdentifier || item.HasTag(tagOrIdentifier)) { return item; } + } + return null; + } + public bool CanAccessInventory(Inventory inventory) { if (!CanInteract || inventory.Locked) { return false; } @@ -2171,8 +2206,7 @@ namespace Barotrauma #if CLIENT if (isLocalPlayer) { - if (GUI.MouseOn == null && - (!CharacterInventory.IsMouseOnInventory() || CharacterInventory.DraggingItemToWorld)) + if (!IsMouseOnUI) { if (findFocusedTimer <= 0.0f || Screen.Selected == GameMain.SubEditorScreen) { @@ -2336,7 +2370,7 @@ namespace Barotrauma else { float closestPlayerDist = c.GetDistanceToClosestPlayer(); - if (closestPlayerDist > NetConfig.DisableCharacterDist) + if (closestPlayerDist > c.Params.DisableDistance) { c.Enabled = false; if (c.IsDead && c.AIController is EnemyAIController) @@ -2344,7 +2378,7 @@ namespace Barotrauma Spawner?.AddToRemoveQueue(c); } } - else if (closestPlayerDist < NetConfig.EnableCharacterDist) + else if (closestPlayerDist < c.Params.DisableDistance * 0.9f) { c.Enabled = true; } @@ -2363,7 +2397,7 @@ namespace Barotrauma distSqr = Math.Min(distSqr, Vector2.DistanceSquared(GameMain.GameScreen.Cam.GetPosition(), c.WorldPosition)); } - if (distSqr > NetConfig.DisableCharacterDistSqr) + if (distSqr > MathUtils.Pow2(c.Params.DisableDistance)) { c.Enabled = false; if (c.IsDead && c.AIController is EnemyAIController) @@ -2371,7 +2405,7 @@ namespace Barotrauma Entity.Spawner?.AddToRemoveQueue(c); } } - else if (distSqr < NetConfig.EnableCharacterDistSqr) + else if (distSqr < MathUtils.Pow2(c.Params.DisableDistance * 0.9f)) { c.Enabled = true; } @@ -2389,6 +2423,8 @@ namespace Barotrauma { UpdateProjSpecific(deltaTime, cam); + KnockbackCooldownTimer -= deltaTime; + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && this == Controlled && !isSynced) { return; } UpdateDespawn(deltaTime); @@ -2451,11 +2487,8 @@ namespace Barotrauma if (NeedsAir) { - bool protectedFromPressure = PressureProtection > 0.0f; - //cannot be protected from pressure when below crush depth - protectedFromPressure = protectedFromPressure && WorldPosition.Y > CharacterHealth.CrushDepth; //implode if not protected from pressure, and either outside or in a high-pressure hull - if (!protectedFromPressure && + if (!IsProtectedFromPressure() && (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f)) { if (CharacterHealth.PressureKillDelay <= 0.0f) @@ -2585,7 +2618,7 @@ namespace Barotrauma partial void UpdateProjSpecific(float deltaTime, Camera cam); - partial void SetOrderProjSpecific(Order order, string orderOption); + partial void SetOrderProjSpecific(Order order, string orderOption, int priority); public void AddAttacker(Character character, float damage) @@ -2641,7 +2674,10 @@ namespace Barotrauma { if (NeedsAir) { - PressureProtection -= deltaTime * 100.0f; + if (Timing.TotalTime > pressureProtectionLastSet + 0.1) + { + PressureProtection = 0.0f; + } } if (NeedsWater) { @@ -2739,7 +2775,7 @@ namespace Barotrauma } float distToClosestPlayer = GetDistanceToClosestPlayer(); - if (distToClosestPlayer > NetConfig.DisableCharacterDist) + if (distToClosestPlayer > Params.DisableDistance) { //despawn in 1 minute if very far from all human players despawnTimer = Math.Max(despawnTimer, GameMain.Config.CorpseDespawnDelay - 60.0f); @@ -2863,7 +2899,7 @@ namespace Barotrauma return !string.IsNullOrEmpty(ChatMessage.ApplyDistanceEffect("message", messageType, speaker, this)); } - public void SetOrder(Order order, string orderOption, Character orderGiver, bool speak = true) + public void SetOrder(Order order, string orderOption, int priority, Character orderGiver, bool speak = true) { //set the character order only if the character is close enough to hear the message if (orderGiver != null && !CanHearCharacter(orderGiver)) { return; } @@ -2871,25 +2907,138 @@ namespace Barotrauma // If there's another character operating the same device, make them dismiss themself if (order != null && order.Category == OrderCategory.Operate && order.TargetEntity != null) { - CharacterList.FindAll(c => c != this && c.TeamID == TeamID && c.CurrentOrder is Order characterOrder && characterOrder.Category == OrderCategory.Operate && - characterOrder.Identifier.Equals(order.Identifier) && characterOrder.TargetEntity == order.TargetEntity)? - .ForEach(c => c.SetOrder(Order.GetPrefab("dismissed"), null, c, speak: true)); + foreach (var character in CharacterList) + { + if (character == this) { continue; } + if (character.TeamID != TeamID) { continue; } + if (!HumanAIController.IsActive(character)) { continue; } + foreach (var currentOrder in character.CurrentOrders) + { + if (currentOrder.Order == null) { continue; } + if (currentOrder.Order.Category != OrderCategory.Operate) { continue; } + if (currentOrder.Order.Identifier != order.Identifier) { continue; } + if (currentOrder.Order.TargetEntity != order.TargetEntity) { continue; } + character.SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(currentOrder), currentOrder.ManualPriority, character); + break; + } + } } + // Prevent adding duplicate orders + RemoveDuplicateOrders(order, orderOption); + + OrderInfo newOrderInfo = new OrderInfo(order, orderOption, priority); + AddCurrentOrder(newOrderInfo); if (AIController is HumanAIController humanAI) { - humanAI.SetOrder(order, orderOption, orderGiver, speak); + humanAI.SetOrder(order, orderOption, priority, orderGiver, speak); } - - SetOrderProjSpecific(order, orderOption); - CurrentOrder = order; - CurrentOrderOption = orderOption; + SetOrderProjSpecific(order, orderOption, priority); } - /// - /// Reset order data so it doesn't carry into further rounds, as the AI is "recreated" always in between rounds anyway. - /// - public void ResetCurrentOrder() => Info?.ResetCurrentOrder(); + private void AddCurrentOrder(OrderInfo newOrder) + { + if (newOrder.Order == null || newOrder.Order.Identifier == "dismissed") + { + if (!string.IsNullOrEmpty(newOrder.OrderOption)) + { + if (CurrentOrders.Any(o => o.MatchesDismissedOrder(newOrder.OrderOption))) + { + var dismissedOrderInfo = CurrentOrders.First(o => o.MatchesDismissedOrder(newOrder.OrderOption)); + int dismissedOrderPriority = dismissedOrderInfo.ManualPriority; + CurrentOrders.Remove(dismissedOrderInfo); + for (int i = 0; i < CurrentOrders.Count; i++) + { + var orderInfo = CurrentOrders[i]; + if (orderInfo.ManualPriority < dismissedOrderPriority) + { + CurrentOrders[i] = new OrderInfo(orderInfo, orderInfo.ManualPriority + 1); + } + } + } + } + else + { + CurrentOrders.Clear(); + } + } + else + { + for (int i = 0; i < CurrentOrders.Count; i++) + { + var orderInfo = CurrentOrders[i]; + if (orderInfo.ManualPriority <= newOrder.ManualPriority) + { + CurrentOrders[i] = new OrderInfo(orderInfo, orderInfo.ManualPriority - 1); + } + } + CurrentOrders.RemoveAll(order => order.ManualPriority <= 0); + CurrentOrders.Add(newOrder); + // Sort the current orders so the one with the highest priority comes first + CurrentOrders.Sort((x, y) => y.ManualPriority.CompareTo(x.ManualPriority)); + } + } + + private void RemoveDuplicateOrders(Order order, string option) + { + int? priorityOfRemoved = null; + for (int i = CurrentOrders.Count - 1; i >= 0; i--) + { + var orderInfo = CurrentOrders[i]; + if (order?.Identifier == orderInfo.Order?.Identifier) + { + priorityOfRemoved = orderInfo.ManualPriority; + CurrentOrders.RemoveAt(i); + break; + } + } + + if (!priorityOfRemoved.HasValue) { return; } + + for (int i = 0; i < CurrentOrders.Count; i++) + { + var orderInfo = CurrentOrders[i]; + if (orderInfo.ManualPriority < priorityOfRemoved.Value) + { + CurrentOrders[i] = new OrderInfo(orderInfo, orderInfo.ManualPriority + 1); + } + } + + CurrentOrders.RemoveAll(order => order.ManualPriority <= 0); + // Sort the current orders so the one with the highest priority comes first + CurrentOrders.Sort((x, y) => y.ManualPriority.CompareTo(x.ManualPriority)); + } + + public OrderInfo? GetCurrentOrderWithTopPriority() + { + return GetCurrentOrder(orderInfo => + { + if (orderInfo.Order == null) { return false; } + if (orderInfo.Order.Identifier == "dismissed") { return false; } + if (orderInfo.ManualPriority < 1) { return false; } + return true; + }); + } + + public OrderInfo? GetCurrentOrder(Order order, string option) + { + return GetCurrentOrder(orderInfo => + { + return orderInfo.MatchesOrder(order, option); + }); + } + + private OrderInfo? GetCurrentOrder(Func predicate) + { + if (CurrentOrders != null && CurrentOrders.Any(predicate)) + { + return CurrentOrders.First(predicate); + } + else + { + return null; + } + } private readonly List aiChatMessageQueue = new List(); @@ -3099,7 +3248,6 @@ namespace Barotrauma otherLimb.body.ApplyLinearImpulse(targetLimb.LinearVelocity * targetLimb.Mass, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.5f); ApplyStatusEffects(ActionType.OnSevered, 1.0f); targetLimb.ApplyStatusEffects(ActionType.OnSevered, 1.0f); - otherLimb.ApplyStatusEffects(ActionType.OnSevered, 1.0f); } } if (wasSevered && targetLimb.character.AIController is EnemyAIController enemyAI) @@ -3154,7 +3302,7 @@ namespace Barotrauma GameMain.Config.RecentlyEncounteredCreatures.Add(other.SpeciesName); } - public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable afflictions, float stun, bool playSound, float attackImpulse, Character attacker = null, float damageMultiplier = 1) + public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable afflictions, float stun, bool playSound, float attackImpulse, Character attacker = null, float damageMultiplier = 1, bool allowStacking = true) { if (Removed) { return new AttackResult(); } @@ -3175,12 +3323,20 @@ namespace Barotrauma //#endif // } + SetStun(stun); + if (attacker != null && attacker != this && GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.AllowFriendlyFire) { if (attacker.TeamID == TeamID) { return new AttackResult(); } } - SetStun(stun); +#if CLIENT + if (Params.UseBossHealthBar && Controlled != null && Controlled.teamID == attacker?.teamID) + { + CharacterHUD.ShowBossHealthBar(this); + } +#endif + Vector2 dir = hitLimb.WorldPosition - worldPosition; if (Math.Abs(attackImpulse) > 0.0f) { @@ -3199,7 +3355,7 @@ namespace Barotrauma bool wasDead = IsDead; Vector2 simPos = hitLimb.SimPosition + ConvertUnits.ToSimUnits(dir); AttackResult attackResult = hitLimb.AddDamage(simPos, afflictions, playSound, damageMultiplier: damageMultiplier); - CharacterHealth.ApplyDamage(hitLimb, attackResult); + CharacterHealth.ApplyDamage(hitLimb, attackResult, allowStacking); if (attacker != this) { OnAttacked?.Invoke(attacker, attackResult); @@ -3215,14 +3371,15 @@ namespace Barotrauma }; if (attackResult.Damage > 0) { - ApplyStatusEffects(ActionType.OnDamaged, 1.0f); - hitLimb.ApplyStatusEffects(ActionType.OnDamaged, 1.0f); + LastDamage = attackResult; if (attacker != null) { AddAttacker(attacker, attackResult.Damage); AddEncounter(attacker); attacker.AddEncounter(this); } + ApplyStatusEffects(ActionType.OnDamaged, 1.0f); + hitLimb.ApplyStatusEffects(ActionType.OnDamaged, 1.0f); } return attackResult; } @@ -3253,6 +3410,12 @@ namespace Barotrauma } } + /// + /// Is the character knocked down regardless whether the technical state is dead, unconcious, paralyzed, or stunned. + /// With stunning, the parameter uses a half a second delay before the character is treated as knocked down. The purpose of this is to ignore minor stunning. If you don't want to to ignore any stun, use the Stun property. + /// + public bool IsKnockedDown => IsDead || IsIncapacitated || CharacterHealth.StunTimer > 0.5f; + public void SetStun(float newStun, bool allowStunDecrease = false, bool isNetworkMessage = false) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && !isNetworkMessage) { return; } @@ -3262,7 +3425,7 @@ namespace Barotrauma { AnimController.ResetPullJoints(); } - CharacterHealth.StunTimer = newStun; + CharacterHealth.Stun = newStun; if (newStun > 0.0f) { SelectedConstruction = null; @@ -3276,6 +3439,20 @@ namespace Barotrauma foreach (StatusEffect statusEffect in statusEffects) { if (statusEffect.type != actionType) { continue; } + if (statusEffect.type == ActionType.OnDamaged) + { + if (statusEffect.AllowedAfflictions != null && (LastDamage.Afflictions == null || LastDamage.Afflictions.None(a => statusEffect.AllowedAfflictions.Contains(a.Prefab.AfflictionType) || statusEffect.AllowedAfflictions.Contains(a.Prefab.Identifier)))) + { + continue; + } + if (statusEffect.OnlyPlayerTriggered) + { + if (LastAttacker == null || !LastAttacker.IsPlayer) + { + continue; + } + } + } if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) || statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { @@ -3308,6 +3485,12 @@ namespace Barotrauma Limb limb = AnimController.GetLimb(limbType); statusEffect.Apply(actionType, deltaTime, this, limb); } + else if (statusEffect.HasTargetType(StatusEffect.TargetType.LastLimb)) + { + // Target just the last matching limb + Limb limb = AnimController.Limbs.LastOrDefault(l => l.type == limbType && !l.IsSevered && !l.Hidden); + statusEffect.Apply(actionType, deltaTime, this, limb); + } } } } @@ -3460,16 +3643,18 @@ namespace Barotrauma return; } - isDead = false; - if (aiTarget != null) { aiTarget.Remove(); } aiTarget = new AITarget(this); - SetAllDamage(0.0f, 0.0f, 0.0f); CharacterHealth.RemoveAllAfflictions(); + SetAllDamage(0.0f, 0.0f, 0.0f); + Oxygen = 100.0f; + Bloodloss = 0.0f; + SetStun(0.0f, true); + isDead = false; foreach (LimbJoint joint in AnimController.LimbJoints) { @@ -3631,6 +3816,10 @@ namespace Barotrauma } } } + else + { + canBePutInOriginalInventory = inventory.CanBePut(newItem, slotIndices[0]); + } if (canBePutInOriginalInventory) { @@ -3704,7 +3893,6 @@ namespace Barotrauma } } - private readonly HashSet currentContexts = new HashSet(); public IEnumerable GetAttackContexts() @@ -3828,5 +4016,10 @@ namespace Barotrauma public bool IsWatchman => HasJob("watchman"); public bool HasJob(string identifier) => Info?.Job?.Prefab.Identifier == identifier; + + public bool IsProtectedFromPressure() + { + return PressureProtection >= (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index 38435fe0d..ff478bab3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -153,6 +153,8 @@ namespace Barotrauma private static ushort idCounter; private const string disguiseName = "???"; + public bool HasNickname => Name != OriginalName; + public string OriginalName { get; private set; } public string Name; public string DisplayName { @@ -349,9 +351,9 @@ namespace Barotrauma private readonly NPCPersonalityTrait personalityTrait; - public Order CurrentOrder { get; set; } - public string CurrentOrderOption { get; set; } - public bool IsDismissed => CurrentOrder == null || CurrentOrder.Identifier.Equals("dismissed", StringComparison.OrdinalIgnoreCase); + public const int MaxCurrentOrders = 3; + public static int HighestManualOrderPriority => MaxCurrentOrders; + public List CurrentOrders { get; } = new List(); //unique ID given to character infos in MP //used by clients to identify which infos are the same to prevent duplicate characters in round summary @@ -453,7 +455,7 @@ namespace Barotrauma public bool IsAttachmentsLoaded => HairIndex > -1 && BeardIndex > -1 && MoustacheIndex > -1 && FaceAttachmentIndex > -1; // Used for creating the data - public CharacterInfo(string speciesName, string name = "", JobPrefab jobPrefab = null, string ragdollFileName = null, int variant = 0, Rand.RandSync randSync = Rand.RandSync.Unsynced) + public CharacterInfo(string speciesName, string name = "", string originalName = "", JobPrefab jobPrefab = null, string ragdollFileName = null, int variant = 0, Rand.RandSync randSync = Rand.RandSync.Unsynced) { if (speciesName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase)) { @@ -503,6 +505,7 @@ namespace Barotrauma } } } + OriginalName = !string.IsNullOrEmpty(originalName) ? originalName : Name; personalityTrait = NPCPersonalityTrait.GetRandom(name + HeadSpriteId); Salary = CalculateSalary(); if (ragdollFileName != null) @@ -518,6 +521,7 @@ namespace Barotrauma ID = idCounter; idCounter++; Name = infoElement.GetAttributeString("name", ""); + OriginalName = infoElement.GetAttributeString("originalname", null); string genderStr = infoElement.GetAttributeString("gender", "male").ToLowerInvariant(); Salary = infoElement.GetAttributeInt("salary", 1000); Enum.TryParse(infoElement.GetAttributeString("race", "White"), true, out Race race); @@ -576,6 +580,11 @@ namespace Barotrauma } } + if (string.IsNullOrEmpty(OriginalName)) + { + OriginalName = Name; + } + StartItemsGiven = infoElement.GetAttributeBool("startitemsgiven", false); string personalityName = infoElement.GetAttributeString("personality", ""); ragdollFileName = infoElement.GetAttributeString("ragdoll", string.Empty); @@ -622,7 +631,17 @@ namespace Barotrauma public int GetIdentifier() { - int id = ToolBox.StringToInt(Name); + return GetIdentifier(Name); + } + + public int GetIdentifierUsingOriginalName() + { + return GetIdentifier(OriginalName); + } + + private int GetIdentifier(string name) + { + int id = ToolBox.StringToInt(name); id ^= HeadSpriteId; id ^= (int)Race << 6; id ^= HairIndex << 12; @@ -939,12 +958,38 @@ namespace Barotrauma partial void OnSkillChanged(string skillIdentifier, float prevLevel, float newLevel, Vector2 textPopupPos); + public void Rename(string newName) + { + if (string.IsNullOrEmpty(newName)) { return; } + // Replace the name tag of any existing id cards or duffel bags + foreach (var item in Item.ItemList) + { + if (item.Prefab.Identifier != "idcard" && !item.Tags.Contains("despawncontainer")) { continue; } + foreach (var tag in item.Tags.Split(',')) + { + var splitTag = tag.Split(":"); + if (splitTag.Length < 2) { continue; } + if (splitTag[0] != "name") { continue; } + if (splitTag[1] != Name) { continue; } + item.ReplaceTag(tag, $"name:{newName}"); + break; + } + } + Name = newName; + } + + public void ResetName() + { + Name = OriginalName; + } + public XElement Save(XElement parentElement) { XElement charElement = new XElement("Character"); charElement.Add( new XAttribute("name", Name), + new XAttribute("originalname", OriginalName), new XAttribute("speciesname", SpeciesName), new XAttribute("gender", Head.gender == Gender.Male ? "male" : "female"), new XAttribute("race", Head.race.ToString()), @@ -957,7 +1002,7 @@ namespace Barotrauma new XAttribute("startitemsgiven", StartItemsGiven), new XAttribute("ragdoll", ragdollFileName), new XAttribute("personality", personalityTrait == null ? "" : personalityTrait.Name)); - + // TODO: animations? if (Character != null) @@ -1004,13 +1049,9 @@ namespace Barotrauma faceAttachments = null; } - /// - /// Reset order data so it doesn't carry into further rounds, as the AI is "recreated" always in between rounds anyway. - /// - public void ResetCurrentOrder() + public void ClearCurrentOrders() { - CurrentOrder = null; - CurrentOrderOption = ""; + CurrentOrders.Clear(); } public void Remove() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs index 62fe02062..8d7ae53f6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs @@ -14,6 +14,9 @@ namespace Barotrauma public Dictionary SerializableProperties { get; set; } + public float PendingAdditionStrenght { get; set; } + public float AdditionStrength { get; set; } + protected float _strength; [Serialize(0f, true), Editable] @@ -26,7 +29,12 @@ namespace Barotrauma { _nonClampedStrength = value; } - _strength = MathHelper.Clamp(value, 0.0f, Prefab.MaxStrength); + float newValue = MathHelper.Clamp(value, 0.0f, Prefab.MaxStrength); + if (newValue > _strength) + { + PendingAdditionStrenght = Prefab.GrainBurst; + } + _strength = newValue; } } @@ -56,6 +64,7 @@ namespace Barotrauma public Affliction(AfflictionPrefab prefab, float strength) { Prefab = prefab; + PendingAdditionStrenght = Prefab.GrainBurst; _strength = strength; Identifier = prefab?.Identifier; @@ -101,13 +110,33 @@ namespace Barotrauma return currVitalityDecrease; } + + public float GetScreenGrainStrength() + { + if (Strength < Prefab.ActivationThreshold) { return 0.0f; } + AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength); + if (currentEffect == null) { return 0.0f; } + if (MathUtils.NearlyEqual(currentEffect.MaxGrainStrength, 0f)) { return 0.0f; } + + float amount = MathHelper.Lerp( + currentEffect.MinGrainStrength, + currentEffect.MaxGrainStrength, + (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + + if (Prefab.GrainBurst > 0 && AdditionStrength > amount) + { + return AdditionStrength; + } + + return amount; + } public float GetScreenDistortStrength() { - if (Strength < Prefab.ActivationThreshold) return 0.0f; + if (Strength < Prefab.ActivationThreshold) { return 0.0f; } AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength); - if (currentEffect == null) return 0.0f; - if (currentEffect.MaxScreenDistortStrength - currentEffect.MinScreenDistortStrength <= 0.0f) return 0.0f; + if (currentEffect == null) { return 0.0f; } + if (currentEffect.MaxScreenDistortStrength - currentEffect.MinScreenDistortStrength < 0.0f) { return 0.0f; } return MathHelper.Lerp( currentEffect.MinScreenDistortStrength, @@ -117,10 +146,10 @@ namespace Barotrauma public float GetRadialDistortStrength() { - if (Strength < Prefab.ActivationThreshold) return 0.0f; + if (Strength < Prefab.ActivationThreshold) { return 0.0f; } AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength); - if (currentEffect == null) return 0.0f; - if (currentEffect.MaxRadialDistortStrength - currentEffect.MinRadialDistortStrength <= 0.0f) return 0.0f; + if (currentEffect == null) { return 0.0f; } + if (currentEffect.MaxRadialDistortStrength - currentEffect.MinRadialDistortStrength < 0.0f) { return 0.0f; } return MathHelper.Lerp( currentEffect.MinRadialDistortStrength, @@ -130,10 +159,10 @@ namespace Barotrauma public float GetChromaticAberrationStrength() { - if (Strength < Prefab.ActivationThreshold) return 0.0f; + if (Strength < Prefab.ActivationThreshold) { return 0.0f; } AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength); - if (currentEffect == null) return 0.0f; - if (currentEffect.MaxChromaticAberrationStrength - currentEffect.MinChromaticAberrationStrength <= 0.0f) return 0.0f; + if (currentEffect == null) { return 0.0f; } + if (currentEffect.MaxChromaticAberrationStrength - currentEffect.MinChromaticAberrationStrength < 0.0f) { return 0.0f; } return MathHelper.Lerp( currentEffect.MinChromaticAberrationStrength, @@ -143,10 +172,10 @@ namespace Barotrauma public float GetScreenBlurStrength() { - if (Strength < Prefab.ActivationThreshold) return 0.0f; + if (Strength < Prefab.ActivationThreshold) { return 0.0f; } AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength); - if (currentEffect == null) return 0.0f; - if (currentEffect.MaxScreenBlurStrength - currentEffect.MinScreenBlurStrength <= 0.0f) return 0.0f; + if (currentEffect == null) { return 0.0f; } + if (currentEffect.MaxScreenBlurStrength - currentEffect.MinScreenBlurStrength < 0.0f) { return 0.0f; } return MathHelper.Lerp( currentEffect.MinScreenBlurStrength, @@ -154,6 +183,20 @@ namespace Barotrauma (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); } + public float GetSkillMultiplier() + { + if (Strength < Prefab.ActivationThreshold) { return 1.0f; } + AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength); + if (currentEffect == null) { return 1.0f; } + + float amount = MathHelper.Lerp( + currentEffect.MinSkillMultiplier, + currentEffect.MaxSkillMultiplier, + (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + + return amount; + } + public void CalculateDamagePerSecond(float currentVitalityDecrease) { DamagePerSecond = Math.Max(DamagePerSecond, currentVitalityDecrease - PreviousVitalityDecrease); @@ -232,6 +275,21 @@ namespace Barotrauma { ApplyStatusEffect(statusEffect, deltaTime, characterHealth, targetLimb); } + + float amount = deltaTime; + if (Prefab.GrainBurst > 0) + { + amount /= Prefab.GrainBurst; + } + if (PendingAdditionStrenght >= 0) + { + AdditionStrength += amount; + PendingAdditionStrenght -= deltaTime; + } + else if (AdditionStrength > 0) + { + AdditionStrength -= amount; + } } public void ApplyStatusEffect(StatusEffect statusEffect, float deltaTime, CharacterHealth characterHealth, Limb targetLimb) @@ -254,16 +312,19 @@ namespace Barotrauma { var targets = new List(); statusEffect.GetNearbyTargets(characterHealth.Character.WorldPosition, targets); - statusEffect.Apply(ActionType.OnActive, deltaTime, targetLimb.character, targets); + statusEffect.Apply(ActionType.OnActive, deltaTime, characterHealth.Character, targets); } } /// /// Use this method to skip clamping and additional logic of the setters. - /// Intended only to be used when the value is already clamped! (networking code) /// Ideally we would keep this private, but doing so would require too much refactoring. /// - public void SetStrength(float strength) => _strength = strength; + public void SetStrength(float strength) + { + _nonClampedStrength = strength; + _strength = _nonClampedStrength; + } public bool ShouldShowIcon(Character afflictedCharacter) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index a6b266d9e..70de29313 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -102,7 +102,7 @@ namespace Barotrauma private void ApplyDamage(float deltaTime, bool applyForce) { - int limbCount = character.AnimController.Limbs.Count(l => !l.IgnoreCollisions && !l.IsSevered); + int limbCount = character.AnimController.Limbs.Count(l => !l.IgnoreCollisions && !l.IsSevered && !l.Hidden); foreach (Limb limb in character.AnimController.Limbs) { if (limb.IsSevered) { continue; } @@ -148,10 +148,9 @@ namespace Barotrauma } } - public void Remove() + public void UnsubscribeFromDeathEvent() { - if (character == null) { return; } - DeactivateHusk(); + if (character == null || !subscribedToDeathEvent) { return; } character.OnDeath -= CharacterDead; subscribedToDeathEvent = false; } @@ -159,7 +158,11 @@ namespace Barotrauma private void CharacterDead(Character character, CauseOfDeath causeOfDeath) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } - if (Strength < ActiveThreshold || character.Removed) { return; } + if (Strength < ActiveThreshold || character.Removed) + { + UnsubscribeFromDeathEvent(); + return; + } //don't turn the character into a husk if any of its limbs are severed if (character.AnimController?.LimbJoints != null) @@ -170,18 +173,22 @@ namespace Barotrauma } } - //character already in remove queue (being removed by something else, for example a modded affliction that uses AfflictionHusk as the base) - // -> don't spawn the AI husk - if (Entity.Spawner.IsInRemoveQueue(character)) { return; } - //create the AI husk in a coroutine to ensure that we don't modify the character list while enumerating it CoroutineManager.StartCoroutine(CreateAIHusk()); } private IEnumerable CreateAIHusk() { + //character already in remove queue (being removed by something else, for example a modded affliction that uses AfflictionHusk as the base) + // -> don't spawn the AI husk + if (Entity.Spawner.IsInRemoveQueue(character)) + { + yield return CoroutineStatus.Success; + } + character.Enabled = false; Entity.Spawner.AddToRemoveQueue(character); + UnsubscribeFromDeathEvent(); string huskedSpeciesName = GetHuskedSpeciesName(character.SpeciesName, Prefab as AfflictionPrefabHusk); CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(huskedSpeciesName); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index e9367b576..645fefcea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -111,7 +111,7 @@ namespace Barotrauma public readonly bool NeedsAir; } - class AfflictionPrefab : IPrefab, IDisposable + class AfflictionPrefab : IPrefab, IDisposable, IHasUintIdentifier { public class Effect { @@ -128,11 +128,14 @@ namespace Barotrauma public float MinScreenBlurStrength, MaxScreenBlurStrength; public float MinScreenDistortStrength, MaxScreenDistortStrength; + public float MinGrainStrength, MaxGrainStrength; public float MinRadialDistortStrength, MaxRadialDistortStrength; public float MinChromaticAberrationStrength, MaxChromaticAberrationStrength; public float MinSpeedMultiplier, MaxSpeedMultiplier; public float MinBuffMultiplier, MaxBuffMultiplier; + public float MinSkillMultiplier, MaxSkillMultiplier; + public float MinResistance, MaxResistance; public string ResistanceFor; public string DialogFlag; @@ -163,10 +166,17 @@ namespace Barotrauma MaxChromaticAberrationStrength = element.GetAttributeFloat("maxchromaticaberration", 0.0f); MaxChromaticAberrationStrength = Math.Max(MinChromaticAberrationStrength, MaxChromaticAberrationStrength); + MinGrainStrength = element.GetAttributeFloat(nameof(MinGrainStrength).ToLower(), 0.0f); + MaxGrainStrength = element.GetAttributeFloat(nameof(MaxGrainStrength).ToLower(), 0.0f); + MaxGrainStrength = Math.Max(MinGrainStrength, MaxGrainStrength); + MinScreenBlurStrength = element.GetAttributeFloat("minscreenblur", 0.0f); MaxScreenBlurStrength = element.GetAttributeFloat("maxscreenblur", 0.0f); MaxScreenBlurStrength = Math.Max(MinScreenBlurStrength, MaxScreenBlurStrength); + MinSkillMultiplier = element.GetAttributeFloat("minskillmultiplier", 1.0f); + MaxSkillMultiplier = element.GetAttributeFloat("maxskillmultiplier", 1.0f); + ResistanceFor = element.GetAttributeString("resistancefor", ""); MinResistance = element.GetAttributeFloat("minresistance", 0.0f); MaxResistance = element.GetAttributeFloat("maxresistance", 0.0f); @@ -228,6 +238,7 @@ namespace Barotrauma public static AfflictionPrefab Bloodloss; public static AfflictionPrefab Pressure; public static AfflictionPrefab Stun; + public static AfflictionPrefab RadiationSickness; public static readonly PrefabCollection Prefabs = new PrefabCollection(); @@ -256,7 +267,7 @@ namespace Barotrauma /// Unique identifier that's generated by hashing the prefab's string identifier. /// Used to reduce the amount of bytes needed to write affliction data into network messages in multiplayer. /// - public uint UIntIdentifier; + public uint UIntIdentifier { get; set; } // Arbitrary string that is used to identify the type of the affliction. public readonly string AfflictionType; @@ -273,6 +284,7 @@ namespace Barotrauma public ContentPackage ContentPackage { get; private set; } public readonly string Name, Description; + public readonly string TranslationOverride; public readonly bool IsBuff; public readonly string CauseOfDeathDescription, SelfCauseOfDeathDescription; @@ -285,9 +297,14 @@ namespace Barotrauma public readonly float ShowIconToOthersThreshold = 0.05f; public readonly float MaxStrength = 100.0f; + public readonly float GrainBurst; + //how high the strength has to be for the affliction icon to be shown with a health scanner public readonly float ShowInHealthScannerThreshold = 0.05f; + //how strong the affliction needs to be before bots attempt to treat it + public readonly float TreatmentThreshold = 5.0f; + //how much karma changes when a player applies this affliction to someone (per strength of the affliction) public float KarmaChangeOnApplied; @@ -337,6 +354,7 @@ namespace Barotrauma Bloodloss = null; Pressure = null; Stun = null; + RadiationSickness = null; #if CLIENT CharacterHealth.DamageOverlay?.Remove(); CharacterHealth.DamageOverlay = null; @@ -361,6 +379,7 @@ namespace Barotrauma if (Bloodloss == null) { DebugConsole.ThrowError("Affliction \"Bloodloss\" not defined in the affliction prefabs."); } if (Pressure == null) { DebugConsole.ThrowError("Affliction \"Pressure\" not defined in the affliction prefabs."); } if (Stun == null) { DebugConsole.ThrowError("Affliction \"Stun\" not defined in the affliction prefabs."); } + if (RadiationSickness == null) { DebugConsole.ThrowError("Affliction \"RadiationSickness\" not defined in the affliction prefabs."); } } public static void LoadFromFile(ContentFile file) @@ -372,6 +391,9 @@ namespace Barotrauma { DebugConsole.ThrowError("Cannot override all afflictions, because many of them are required by the main game! Please try overriding them one by one."); } + + List<(AfflictionPrefab prefab, XElement element)> loadedAfflictions = new List<(AfflictionPrefab prefab, XElement element)>(); + foreach (XElement element in mainElement.Elements()) { bool isOverride = element.IsOverride(); @@ -436,6 +458,7 @@ namespace Barotrauma prefab = new AfflictionPrefab(sourceElement, file.Path, typeof(AfflictionBleeding)); break; case "huskinfection": + case "alieninfection": prefab = new AfflictionPrefabHusk(sourceElement, file.Path, typeof(AfflictionHusk)); break; case "cprsettings": @@ -498,27 +521,25 @@ namespace Barotrauma case "stun": Stun = prefab; break; + case "radiationsickness": + RadiationSickness = prefab; + break; } if (ImpactDamage == null) { ImpactDamage = InternalDamage; } if (prefab != null) { + loadedAfflictions.Add((prefab, sourceElement)); Prefabs.Add(prefab, isOverride); + prefab.CalculatePrefabUIntIdentifier(Prefabs); } } - using MD5 md5 = MD5.Create(); - foreach (AfflictionPrefab prefab in Prefabs) + //load the effects after all the afflictions in the file have been instantiated + //otherwise afflictions can't inflict other afflictions that are defined at a later point in the file + foreach ((AfflictionPrefab prefab, XElement element) in loadedAfflictions) { - prefab.UIntIdentifier = ToolBox.StringToUInt32Hash(prefab.Identifier, md5); - - //it's theoretically possible for two different values to generate the same hash, but the probability is astronomically small - var collision = Prefabs.Find(p => p != prefab && p.UIntIdentifier == prefab.UIntIdentifier); - if (collision != null) - { - DebugConsole.ThrowError("Hashing collision when generating uint identifiers for Afflictions: " + prefab.Identifier + " has the same identifier as " + collision.Identifier + " (" + prefab.UIntIdentifier + ")"); - collision.UIntIdentifier++; - } + prefab.LoadEffects(element); } } @@ -549,8 +570,10 @@ namespace Barotrauma Identifier = element.GetAttributeString("identifier", ""); AfflictionType = element.GetAttributeString("type", ""); - Name = TextManager.Get("AfflictionName." + Identifier, true) ?? element.GetAttributeString("name", ""); - Description = TextManager.Get("AfflictionDescription." + Identifier, true) ?? element.GetAttributeString("description", ""); + TranslationOverride = element.GetAttributeString("translationoverride", null); + string translationId = TranslationOverride ?? Identifier; + Name = TextManager.Get("AfflictionName." + translationId, true) ?? element.GetAttributeString("name", ""); + Description = TextManager.Get("AfflictionDescription." + translationId, true) ?? element.GetAttributeString("description", ""); IsBuff = element.GetAttributeBool("isbuff", false); LimbSpecific = element.GetAttributeBool("limbspecific", false); @@ -567,16 +590,18 @@ namespace Barotrauma ShowIconThreshold = element.GetAttributeFloat("showiconthreshold", Math.Max(ActivationThreshold, 0.05f)); ShowIconToOthersThreshold = element.GetAttributeFloat("showicontoothersthreshold", ShowIconThreshold); MaxStrength = element.GetAttributeFloat("maxstrength", 100.0f); + GrainBurst = element.GetAttributeFloat(nameof(GrainBurst).ToLower(), 0.0f); ShowInHealthScannerThreshold = element.GetAttributeFloat("showinhealthscannerthreshold", Math.Max(ActivationThreshold, 0.05f)); + TreatmentThreshold = element.GetAttributeFloat("treatmentthreshold", Math.Max(ActivationThreshold, 5.0f)); DamageOverlayAlpha = element.GetAttributeFloat("damageoverlayalpha", 0.0f); BurnOverlayAlpha = element.GetAttributeFloat("burnoverlayalpha", 0.0f); KarmaChangeOnApplied = element.GetAttributeFloat("karmachangeonapplied", 0.0f); - CauseOfDeathDescription = TextManager.Get("AfflictionCauseOfDeath." + Identifier, true) ?? element.GetAttributeString("causeofdeathdescription", ""); - SelfCauseOfDeathDescription = TextManager.Get("AfflictionCauseOfDeathSelf." + Identifier, true) ?? element.GetAttributeString("selfcauseofdeathdescription", ""); + CauseOfDeathDescription = TextManager.Get("AfflictionCauseOfDeath." + translationId, true) ?? element.GetAttributeString("causeofdeathdescription", ""); + SelfCauseOfDeathDescription = TextManager.Get("AfflictionCauseOfDeathSelf." + translationId, true) ?? element.GetAttributeString("selfcauseofdeathdescription", ""); IconColors = element.GetAttributeColorArray("iconcolors", null); AchievementOnRemoved = element.GetAttributeString("achievementonremoved", ""); @@ -588,12 +613,6 @@ namespace Barotrauma case "icon": Icon = new Sprite(subElement); break; - case "effect": - effects.Add(new Effect(subElement, Name)); - break; - case "periodiceffect": - periodicEffects.Add(new PeriodicEffect(subElement, Name)); - break; } } @@ -618,6 +637,22 @@ namespace Barotrauma constructor = type.GetConstructor(new[] { typeof(AfflictionPrefab), typeof(float) }); } + private void LoadEffects(XElement element) + { + foreach (XElement subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "effect": + effects.Add(new Effect(subElement, Name)); + break; + case "periodiceffect": + periodicEffects.Add(new PeriodicEffect(subElement, Name)); + break; + } + } + } + public override string ToString() { return "AfflictionPrefab (" + Name + ")"; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index b23faa20c..fd7e4e4ea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -36,7 +36,11 @@ namespace Barotrauma public LimbHealth(XElement element, CharacterHealth characterHealth) { - Name = TextManager.Get("HealthLimbName." + element.GetAttributeString("name", "")); + string limbName = element.GetAttributeString("name", null) ?? "generic"; + if (limbName != "generic") + { + Name = TextManager.Get("HealthLimbName." + limbName); + } this.characterHealth = characterHealth; foreach (XElement subElement in element.Elements()) { @@ -186,12 +190,14 @@ namespace Barotrauma set { bloodlossAffliction.Strength = MathHelper.Clamp(value, 0.0f, 100.0f); } } - public float StunTimer + public float Stun { get { return stunAffliction.Strength; } set { stunAffliction.Strength = MathHelper.Clamp(value, 0.0f, stunAffliction.Prefab.MaxStrength); } } + public float StunTimer { get; private set; } + public Affliction PressureAffliction { get { return pressureAffliction; } @@ -484,7 +490,7 @@ namespace Barotrauma CalculateVitality(); } - public void ApplyDamage(Limb hitLimb, AttackResult attackResult) + public void ApplyDamage(Limb hitLimb, AttackResult attackResult, bool allowStacking = true) { if (Unkillable || Character.GodMode) { return; } if (hitLimb.HealthIndex < 0 || hitLimb.HealthIndex >= limbHealths.Count) @@ -498,11 +504,11 @@ namespace Barotrauma { if (newAffliction.Prefab.LimbSpecific) { - AddLimbAffliction(hitLimb, newAffliction); + AddLimbAffliction(hitLimb, newAffliction, allowStacking); } else { - AddAffliction(newAffliction); + AddAffliction(newAffliction, allowStacking); } } } @@ -569,7 +575,7 @@ namespace Barotrauma CalculateVitality(); } - private void AddLimbAffliction(Limb limb, Affliction newAffliction) + private void AddLimbAffliction(Limb limb, Affliction newAffliction, bool allowStacking = true) { if (!newAffliction.Prefab.LimbSpecific || limb == null) { return; } if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count) @@ -578,10 +584,10 @@ namespace Barotrauma "\" only has health configured for" + limbHealths.Count + " limbs but the limb " + limb.type + " is targeting index " + limb.HealthIndex); return; } - AddLimbAffliction(limbHealths[limb.HealthIndex], newAffliction); + AddLimbAffliction(limbHealths[limb.HealthIndex], newAffliction, allowStacking); } - private void AddLimbAffliction(LimbHealth limbHealth, Affliction newAffliction) + private void AddLimbAffliction(LimbHealth limbHealth, Affliction newAffliction, bool allowStacking = true) { if (!DoesBleed && newAffliction is AfflictionBleeding) { return; } if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; } @@ -590,7 +596,15 @@ namespace Barotrauma { if (newAffliction.Prefab == affliction.Prefab) { - affliction.Strength = Math.Min(affliction.Prefab.MaxStrength, affliction.Strength + (newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(affliction.Prefab.Identifier)))); + float newStrength = newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(affliction.Prefab.Identifier)); + if (allowStacking) + { + // Add the existing strength + newStrength += affliction.Strength; + } + newStrength = Math.Min(affliction.Prefab.MaxStrength, newStrength); + if (affliction == stunAffliction) { Character.SetStun(newStrength, true, true); } + affliction.Strength = newStrength; affliction.Source = newAffliction.Source; CalculateVitality(); if (Vitality <= MinVitality) @@ -620,13 +634,12 @@ namespace Barotrauma #endif } - private void AddAffliction(Affliction newAffliction) + private void AddAffliction(Affliction newAffliction, bool allowStacking = true) { if (!DoesBleed && newAffliction is AfflictionBleeding) { return; } if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; } - if (newAffliction.Prefab.AfflictionType == "huskinfection") + if (newAffliction.Prefab is AfflictionPrefabHusk huskPrefab) { - var huskPrefab = newAffliction.Prefab as AfflictionPrefabHusk; if (huskPrefab.TargetSpecies.None(s => s.Equals(Character.SpeciesName, StringComparison.OrdinalIgnoreCase))) { return; @@ -636,7 +649,13 @@ namespace Barotrauma { if (newAffliction.Prefab == affliction.Prefab) { - float newStrength = Math.Min(affliction.Prefab.MaxStrength, affliction.Strength + (newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(affliction.Prefab.Identifier)))); + float newStrength = newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(affliction.Prefab.Identifier)); + if (allowStacking) + { + // Add the existing strength + newStrength += affliction.Strength; + } + newStrength = Math.Min(affliction.Prefab.MaxStrength, newStrength); if (affliction == stunAffliction) { Character.SetStun(newStrength, true, true); } affliction.Strength = newStrength; affliction.Source = newAffliction.Source; @@ -664,7 +683,6 @@ namespace Barotrauma } } - partial void UpdateProjSpecific(float deltaTime); partial void UpdateLimbAfflictionOverlays(); @@ -673,6 +691,8 @@ namespace Barotrauma { UpdateOxygen(deltaTime); + StunTimer = Stun > 0 ? StunTimer + deltaTime : 0; + for (int i = 0; i < limbHealths.Count; i++) { for (int j = limbHealths[i].Afflictions.Count - 1; j >= 0; j--) @@ -686,12 +706,16 @@ namespace Barotrauma 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); + Limb targetLimb = Character.AnimController.Limbs.LastOrDefault(l => !l.IsSevered && !l.Hidden && l.HealthIndex == i); + if (targetLimb == null) + { + targetLimb = Character.AnimController.MainLimb; + } affliction.Update(this, targetLimb, deltaTime); affliction.DamagePerSecondTimer += deltaTime; - if (affliction is AfflictionBleeding) + if (affliction is AfflictionBleeding bleeding) { - UpdateBleedingProjSpecific((AfflictionBleeding)affliction, targetLimb, deltaTime); + UpdateBleedingProjSpecific(bleeding, targetLimb, deltaTime); } Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier()); } @@ -788,6 +812,13 @@ namespace Barotrauma Vitality -= vitalityDecrease; affliction.CalculateDamagePerSecond(vitalityDecrease); } + +#if CLIENT + if (IsUnconscious) + { + HintManager.OnCharacterUnconscious(Character); + } +#endif } private void Kill() @@ -877,6 +908,7 @@ namespace Barotrauma float minSuitability = -10, maxSuitability = 10; foreach (Affliction affliction in GetAllAfflictions()) { + if (affliction.Strength < affliction.Prefab.TreatmentThreshold) { continue; } foreach (KeyValuePair treatment in affliction.Prefab.TreatmentSuitability) { if (!treatmentSuitability.ContainsKey(treatment.Key)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index bcfeaa2fe..c95a4feb2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs @@ -20,6 +20,15 @@ namespace Barotrauma [Serialize(1f, false)] public float HealthMultiplier { get; protected set; } + [Serialize(1f, false)] + public float HealthMultiplierInMultiplayer { get; protected set; } + + [Serialize(1f, false)] + public float AimSpeed { get; protected set; } + + [Serialize(1f, false)] + public float AimAccuracy { get; protected set; } + private readonly HashSet moduleFlags = new HashSet(); [Serialize("", true, "What outpost module tags does the NPC prefer to spawn in.")] @@ -67,6 +76,9 @@ namespace Barotrauma [Serialize(AIObjectiveIdle.BehaviorType.Passive, false)] public AIObjectiveIdle.BehaviorType Behavior { get; protected set; } + [Serialize(float.PositiveInfinity, false)] + public float ReportRange { get; protected set; } + public List PreferredOutpostModuleTypes { get; protected set; } public string OriginalName { get { return Identifier; } } @@ -105,16 +117,54 @@ namespace Barotrauma return Job != null && Job != "any" ? JobPrefab.Get(Job) : JobPrefab.Random(randSync); } - public void GiveItems(Character character, Submarine submarine, Rand.RandSync randSync = Rand.RandSync.Unsynced) + public void InitializeCharacter(Character npc, ISpatialEntity positionToStayIn = null) + { + npc.CharacterHealth.MaxVitality *= HealthMultiplier; + if (GameMain.NetworkMember != null) + { + npc.CharacterHealth.MaxVitality *= HealthMultiplierInMultiplayer; + } + var humanAI = npc.AIController as HumanAIController; + if (humanAI != null) + { + var idleObjective = humanAI.ObjectiveManager.GetObjective(); + if (positionToStayIn != null && Behavior == AIObjectiveIdle.BehaviorType.StayInHull) + { + idleObjective.TargetHull = AIObjectiveGoTo.GetTargetHull(positionToStayIn); + idleObjective.Behavior = AIObjectiveIdle.BehaviorType.StayInHull; + } + else + { + idleObjective.Behavior = Behavior; + foreach (string moduleType in PreferredOutpostModuleTypes) + { + idleObjective.PreferredOutpostModuleTypes.Add(moduleType); + } + } + humanAI.ReportRange = ReportRange; + humanAI.AimSpeed = AimSpeed; + humanAI.AimAccuracy = AimAccuracy; + } + if (CampaignInteractionType != CampaignMode.InteractionType.None) + { + (GameMain.GameSession.GameMode as CampaignMode)?.AssignNPCMenuInteraction(npc, CampaignInteractionType); + if (positionToStayIn != null && humanAI != null) + { + humanAI.ObjectiveManager.SetForcedOrder(new AIObjectiveGoTo(positionToStayIn, npc, humanAI.ObjectiveManager, repeat: true, getDivingGearIfNeeded: false, closeEnough: 200)); + } + } + } + + public void GiveItems(Character character, Submarine submarine, Rand.RandSync randSync = Rand.RandSync.Unsynced, bool createNetworkEvents = true) { var spawnItems = ToolBox.SelectWeightedRandom(ItemSets.Keys.ToList(), ItemSets.Values.ToList(), randSync); foreach (XElement itemElement in spawnItems.GetChildElements("item")) { - InitializeItems(character, itemElement, submarine); + InitializeItems(character, itemElement, submarine, createNetworkEvents: createNetworkEvents); } } - private void InitializeItems(Character character, XElement itemElement, Submarine submarine, Item parentItem = null) + private void InitializeItems(Character character, XElement itemElement, Submarine submarine, Item parentItem = null, bool createNetworkEvents = true) { ItemPrefab itemPrefab; string itemIdentifier = itemElement.GetAttributeString("identifier", ""); @@ -126,7 +176,7 @@ namespace Barotrauma } Item item = new Item(itemPrefab, character.Position, null); #if SERVER - if (GameMain.Server != null && Entity.Spawner != null) + if (GameMain.Server != null && Entity.Spawner != null && createNetworkEvents) { if (GameMain.Server.EntityEventManager.UniqueEvents.Any(ev => ev.Entity == item)) { @@ -187,7 +237,7 @@ namespace Barotrauma } foreach (XElement childItemElement in itemElement.Elements()) { - InitializeItems(character, childItemElement, submarine, item); + InitializeItems(character, childItemElement, submarine, item, createNetworkEvents); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs index a51136f8c..21b7f6cb2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs @@ -187,16 +187,18 @@ namespace Barotrauma } } - if (item.Prefab.Identifier == "idcard" && spawnPoint != null) + if (item.Prefab.Identifier == "idcard") { - foreach (string s in spawnPoint.IdCardTags) + if (spawnPoint != null) { - item.AddTag(s); + foreach (string s in spawnPoint.IdCardTags) + { + item.AddTag(s); + if (!string.IsNullOrWhiteSpace(spawnPoint.IdCardDesc)) { item.Description = spawnPoint.IdCardDesc; } + } } item.AddTag("name:" + character.Name); item.AddTag("job:" + Name); - if (!string.IsNullOrWhiteSpace(spawnPoint.IdCardDesc)) - item.Description = spawnPoint.IdCardDesc; IdCard idCardComponent = item.GetComponent(); if (idCardComponent != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index 1f2757e3a..c1e9394a4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -203,7 +203,7 @@ namespace Barotrauma partial class Limb : ISerializableEntity, ISpatialEntity { //how long it takes for severed limbs to fade out - public float SeveredFadeOutTime => Params.SeveredFadeOutTime; + public float SeveredFadeOutTime { get; private set; } = 10; public readonly Character character; /// @@ -308,6 +308,12 @@ namespace Barotrauma set { if (isSevered == value) { return; } + if (value == true) + { + // If any of the connected limbs have a longer fade out time, use that + var connectedLimbs = GetConnectedLimbs(); + SeveredFadeOutTime = Math.Max(Params.SeveredFadeOutTime, connectedLimbs.Any() ? connectedLimbs.Max(l => l.SeveredFadeOutTime) : 0); + } isSevered = value; if (isSevered) { @@ -726,6 +732,10 @@ namespace Barotrauma { newAffliction = affliction.CreateMultiplied(finalDamageModifier); } + else + { + newAffliction.SetStrength(affliction.NonClampedStrength); + } if (applyAffliction) { @@ -861,6 +871,23 @@ namespace Barotrauma float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(simPos, attackSimPos)); bool wasRunning = attack.IsRunning; attack.UpdateAttackTimer(deltaTime, character); + if (attack.Blink) + { + if (attack.ForceOnLimbIndices != null && attack.ForceOnLimbIndices.Any()) + { + foreach (int limbIndex in attack.ForceOnLimbIndices) + { + if (limbIndex < 0 || limbIndex >= character.AnimController.Limbs.Length) { continue; } + Limb limb = character.AnimController.Limbs[limbIndex]; + if (limb.IsSevered) { continue; } + limb.Blink(); + } + } + else + { + Blink(); + } + } bool wasHit = false; Body structureBody = null; @@ -871,11 +898,11 @@ namespace Barotrauma case HitDetection.Distance: if (dist < attack.DamageRange) { - structureBody = Submarine.PickBody(simPos, attackSimPos, collisionCategory: Physics.CollisionWall | Physics.CollisionLevel, allowInsideFixture: true); - if (structureBody?.UserData as string == "ruinroom") + structureBody = Submarine.PickBody(simPos, attackSimPos, collisionCategory: Physics.CollisionWall | Physics.CollisionLevel, allowInsideFixture: true, customPredicate: + (Fixture f) => { - structureBody = null; - } + return f?.Body?.UserData as string != "ruinroom"; + }); if (damageTarget is Item i && i.GetComponent() != null) { // If the attack is aimed to an item and hits an item, it's successful. @@ -1098,12 +1125,26 @@ namespace Barotrauma foreach (StatusEffect statusEffect in statusEffects) { if (statusEffect.type != actionType) { continue; } + if (statusEffect.type == ActionType.OnDamaged) + { + if (statusEffect.AllowedAfflictions != null && (character.LastDamage.Afflictions == null || character.LastDamage.Afflictions.None(a => statusEffect.AllowedAfflictions.Contains(a.Prefab.AfflictionType) || statusEffect.AllowedAfflictions.Contains(a.Prefab.Identifier)))) + { + continue; + } + if (statusEffect.OnlyPlayerTriggered) + { + if (character.LastAttacker == null || !character.LastAttacker.IsPlayer) + { + continue; + } + } + } if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) || statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { targets.Clear(); statusEffect.GetNearbyTargets(WorldPosition, targets); - statusEffect.Apply(ActionType.OnActive, deltaTime, character, targets); + statusEffect.Apply(actionType, deltaTime, character, targets); } else { @@ -1111,7 +1152,40 @@ namespace Barotrauma { statusEffect.Apply(actionType, deltaTime, character, character, WorldPosition); } - statusEffect.Apply(actionType, deltaTime, character, this, WorldPosition); + else if (statusEffect.targetLimbs != null) + { + foreach (var limbType in statusEffect.targetLimbs) + { + if (statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs)) + { + // Target all matching limbs + foreach (var limb in ragdoll.Limbs) + { + if (limb.IsSevered) { continue; } + if (limb.type == limbType) + { + statusEffect.Apply(actionType, deltaTime, character, limb); + } + } + } + else if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb)) + { + // Target just the first matching limb + Limb limb = ragdoll.GetLimb(limbType); + statusEffect.Apply(actionType, deltaTime, character, limb); + } + else if (statusEffect.HasTargetType(StatusEffect.TargetType.LastLimb)) + { + // Target just the last matching limb + Limb limb = ragdoll.Limbs.LastOrDefault(l => l.type == limbType && !l.IsSevered && !l.Hidden); + statusEffect.Apply(actionType, deltaTime, character, limb); + } + } + } + else + { + statusEffect.Apply(actionType, deltaTime, character, this, WorldPosition); + } } } } @@ -1121,7 +1195,12 @@ namespace Barotrauma private float TotalBlinkDurationOut => Params.BlinkDurationOut + Params.BlinkHoldTime; - public void Blink(float deltaTime, float referenceRotation) + public void Blink() + { + blinkTimer = -TotalBlinkDurationOut; + } + + public void UpdateBlink(float deltaTime, float referenceRotation) { if (blinkTimer > -TotalBlinkDurationOut) { @@ -1155,6 +1234,26 @@ namespace Barotrauma } } + public IEnumerable GetConnectedJoints() => ragdoll.LimbJoints.Where(j => !j.IsSevered && (j.LimbA == this || j.LimbB == this)); + + public IEnumerable GetConnectedLimbs() + { + var connectedJoints = GetConnectedJoints(); + var connectedLimbs = new HashSet(); + foreach (Limb limb in ragdoll.Limbs) + { + var otherJoints = limb.GetConnectedJoints(); + foreach (LimbJoint connectedJoint in connectedJoints) + { + if (otherJoints.Contains(connectedJoint)) + { + connectedLimbs.Add(limb); + } + } + } + return connectedLimbs; + } + public void Remove() { body?.Remove(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs index 10f6c0215..34c017ebe 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs @@ -54,7 +54,7 @@ namespace Barotrauma abstract class SwimParams : AnimationParams { - [Serialize(25.0f, true, description: "Turning speed (or rather a force applied on the main collider to make it turn). Note that you can set a limb-specific steering forces too (additional)."), Editable(MinValueFloat = 0, MaxValueFloat = 500, ValueStep = 1)] + [Serialize(25.0f, true, description: "Turning speed (or rather a force applied on the main collider to make it turn). Note that you can set a limb-specific steering forces too (additional)."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] public float SteerTorque { get; set; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs index fe8a2da9b..07c3bf980 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs @@ -173,13 +173,13 @@ namespace Barotrauma [Editable, Serialize(true, true, description: "Should the character face towards the direction it's heading.")] public bool RotateTowardsMovement { get; set; } - [Serialize(25.0f, true, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] + [Serialize(25.0f, true, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 2000, ValueStep = 1)] public float TorsoTorque { get; set; } - [Serialize(25.0f, true, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] + [Serialize(25.0f, true, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 2000, ValueStep = 1)] public float HeadTorque { get; set; } - [Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] + [Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 2000, ValueStep = 1)] public float TailTorque { get; set; } [Serialize(1f, true, description: "Multiplier applied based on the angle difference between the tail and the main limb. Increasing the value prevents snake-like characters from getting tangled on themselves. Default = 1 (no boost)"), Editable(MinValueFloat = 1, MaxValueFloat = 100)] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index f2f59d231..d8aa2419a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -49,6 +49,9 @@ namespace Barotrauma [Serialize(false, false), Editable] public bool CanSpeak { get; set; } + [Serialize(false, true), Editable] + public bool UseBossHealthBar { get; private set; } + [Serialize(100f, true, description: "How much noise the character makes when moving?"), Editable(minValue: 0f, maxValue: 100000f)] public float Noise { get; set; } @@ -64,6 +67,9 @@ namespace Barotrauma [Serialize("waterblood", true), Editable] public string BleedParticleWater { get; private set; } + [Serialize(1f, true), Editable] + public float BleedParticleMultiplier { get; private set; } + [Serialize(10f, true, description: "How effectively/easily the character eats other characters. Affects the forces, the amount of particles, and the time required before the target is eaten away"), Editable(MinValueFloat = 1, MaxValueFloat = 1000, ValueStep = 1)] public float EatingSpeed { get; set; } @@ -76,6 +82,12 @@ namespace Barotrauma [Serialize(0f, true), Editable] public float SonarDisruption { get; set; } + [Serialize(0f, true), Editable] + public float DistantSonarRange { get; set; } + + [Serialize(25000f, true, "If the character is farther than this (in pixels) from the sub and the players, it will be disabled. The halved value is used for triggering simple physics where the ragdoll is disabled and only the main collider is updated."), Editable(MinValueFloat = 10000f, MaxValueFloat = 100000f)] + public float DisableDistance { get; set; } + public readonly string File; public XDocument VariantFile { get; private set; } @@ -118,10 +130,11 @@ namespace Barotrauma // TODO: Make recursive? In practice we don't have to go deeper than this, but the implementation would be a lot cleaner with recursion. foreach (XElement subSubElement in subElement.Elements()) { - matchingParams = matchingParams.SubParams.FirstOrDefault(p => p.Name.Equals(subSubElement.Name.ToString(), StringComparison.OrdinalIgnoreCase)); - if (matchingParams != null) + if (subSubElement.Name.ToString().Equals("item", StringComparison.OrdinalIgnoreCase)) { continue; } + var matchingSubParams = matchingParams.SubParams.FirstOrDefault(p => p.Name.Equals(subSubElement.Name.ToString(), StringComparison.OrdinalIgnoreCase)); + if (matchingSubParams != null) { - TryLoadOverride(matchingParams, subSubElement, matchingParams.SerializableProperties); + TryLoadOverride(matchingSubParams, subSubElement, matchingSubParams.SerializableProperties); } } } @@ -423,10 +436,10 @@ namespace Barotrauma [Serialize(false, true)] public bool UseHealthWindow { get; set; } - [Serialize(0f, true, description: "How easily the character heals from the bleeding wounds. Default 0 (no extra healing)."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] + [Serialize(0f, true, description: "How easily the character heals from the bleeding wounds. Default 0 (no extra healing)."), Editable(MinValueFloat = 0, MaxValueFloat = 100, DecimalCount = 2)] public float BleedingReduction { get; private set; } - [Serialize(0f, true, description: "How easily the character heals from the burn wounds. Default 0 (no extra healing)."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] + [Serialize(0f, true, description: "How easily the character heals from the burn wounds. Default 0 (no extra healing)."), Editable(MinValueFloat = 0, MaxValueFloat = 100, DecimalCount = 2)] public float BurnReduction { get; private set; } [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] @@ -522,21 +535,36 @@ namespace Barotrauma [Serialize(20f, true, description: "How long the creature flees before returning to normal state. When the creature sees the target or is being chased, it will always flee, if it's in the flee state."), Editable(minValue: 0f, maxValue: 100f)] public float MinFleeTime { get; private set; } - [Serialize(false, true, description: "Does the character try to break inside the sub?"), Editable()] + [Serialize(false, true, description: "Does the character try to break inside the sub?"), Editable] public bool AggressiveBoarding { get; private set; } - [Serialize(true, true, description: "Enforce aggressive behavior if the creature is spawned as a target of a monster mission."), Editable()] + [Serialize(true, true, description: "Enforce aggressive behavior if the creature is spawned as a target of a monster mission."), Editable] public bool EnforceAggressiveBehaviorForMissions { get; private set; } - [Serialize(true, true, description: "Should the character target or ignore walls when it's outside the submarine."), Editable()] + [Serialize(true, true, description: "Should the character target or ignore walls when it's outside the submarine."), Editable] public bool TargetOuterWalls { get; private set; } - [Serialize(false, true, description: "If enabled, the character chooses randomly from the available attacks. The priority is used as a weight for weighted random."), Editable()] + [Serialize(false, true, description: "If enabled, the character chooses randomly from the available attacks. The priority is used as a weight for weighted random."), Editable] public bool RandomAttack { get; private set; } - [Serialize(false, true, description:"Can the character open doors and hatches without a proper id card? Only applies on humanoids.")] + [Serialize(false, true, description:"Can the character open doors and hatches without a proper id card? Only applies on humanoids."), Editable] public bool Infiltrate { get; private set; } + [Serialize(true, true, "Is the creature allowed to navigate from and into the depths of the abyss? When enabled, the creatures will try to avoid the depths."), Editable] + public bool AvoidAbyss { get; set; } + + [Serialize(false, true, "Does the creature try to keep in the abyss? Has effect only when AvoidAbyss is false."), Editable] + public bool StayInAbyss { get; set; } + + [Serialize(0f, true, description: ""), Editable] + public float StartAggression { get; private set; } + + [Serialize(100f, true, description: ""), Editable] + public float MaxAggression { get; private set; } + + [Serialize(0f, true, description: ""), Editable] + public float AggressionCumulation { get; private set; } + public IEnumerable Targets => targets; protected readonly List targets = new List(); @@ -639,9 +667,19 @@ namespace Barotrauma [Serialize(false, true, description: "Should the target be ignored while the creature is outside. Doesn't matter where the target is."), Editable] public bool IgnoreOutside { get; set; } - [Serialize(false, true)] + [Serialize(false, true, description: "Should the target be ignored if it's inside a different submarine than us? Normally only some targets are ignored when they are not inside the same sub."), Editable] + public bool IgnoreIfNotInSameSub { get; set; } + + [Serialize(false, true), Editable] public bool IgnoreIncapacitated { get; set; } + [Serialize(0f, true, description: "How much damage the protected target should take from an attacker before the creature starts defending it."), Editable] + public float DamageThreshold { get; private set; } + + [Serialize(AttackPattern.Straight, true), Editable] + public AttackPattern AttackPattern { get; set; } + + #region Sweep [Serialize(0f, true, description: "Use to define a distance at which the creature starts the sweeping movement."), Editable(MinValueFloat = 0, MaxValueFloat = 10000, ValueStep = 1, DecimalCount = 0)] public float SweepDistance { get; private set; } @@ -650,9 +688,21 @@ namespace Barotrauma [Serialize(1f, true, description: "How quickly the sweep direction changes. Uses the sine wave pattern."), Editable(MinValueFloat = 0, MaxValueFloat = 10, ValueStep = 0.1f, DecimalCount = 2)] public float SweepSpeed { get; private set; } + #endregion - [Serialize(0f, true, description: "How much damage the protected target should take from an attacker before the creature starts defending it.")] - public float Threshold { get; private set; } + #region Circle + [Serialize(5000f, true), Editable(MinValueFloat = 0f, MaxValueFloat = 20000f)] + public float CircleStartDistance { get; private set; } + + [Serialize(1f, true), Editable(MinValueFloat = 0.5f, MaxValueFloat = 2f)] + public float CircleRotationSpeed { get; private set; } + + [Serialize(5f, true), Editable(MinValueFloat = 1f, MaxValueFloat = 10f)] + public float CircleStrikeDistanceMultiplier { get; private set; } + + [Serialize(0f, true), Editable(MinValueFloat = 0f, MaxValueFloat = 50f)] + public float CircleMaxRandomOffset { get; private set; } + #endregion public TargetParams(XElement element, CharacterParams character) : base(element, character) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs index 67880787e..b3f6806dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs @@ -599,7 +599,7 @@ namespace Barotrauma [Serialize(0f, true, description: "Width of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] public float Width { get; set; } - [Serialize(10f, true, description: "The more the density the heavier the limb is."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] + [Serialize(10f, true, description: "The more the density the heavier the limb is."), Editable(MinValueFloat = 0, MaxValueFloat = 100, DecimalCount = 2)] public float Density { get; set; } [Serialize(false, true), Editable] diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index ac93c08d3..36daaf126 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -79,6 +79,11 @@ namespace Barotrauma OnExecute(args); } + + public override int GetHashCode() + { + return names[0].GetHashCode(); + } } private static readonly Queue queuedMessages = new Queue(); @@ -339,7 +344,7 @@ namespace Barotrauma return new string[][] { GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(), - commands.Select(c => c.names[0]).ToArray() + commands.Select(c => c.names[0]).Union(new string[]{ "All" }).ToArray() }; })); @@ -351,7 +356,7 @@ namespace Barotrauma return new string[][] { GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(), - new string[0] + commands.Select(c => c.names[0]).Union(new string[]{ "All" }).ToArray() }; })); @@ -604,7 +609,7 @@ namespace Barotrauma commands.Add(new Command("giveaffliction", "giveaffliction [affliction name] [affliction strength] [character name]: Add an affliction to a character. If the name parameter is omitted, the affliction is added to the controlled character.", (string[] args) => { - if (args.Length < 2) return; + if (args.Length < 2) { return; } AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(a => a.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase) || @@ -621,9 +626,19 @@ namespace Barotrauma return; } - Character targetCharacter = (args.Length <= 2) ? Character.Controlled : FindMatchingCharacter(args.Skip(2).ToArray()); + bool relativeStrength = false; + if (args.Length > 2) + { + bool.TryParse(args[2], out relativeStrength); + } + + Character targetCharacter = (relativeStrength || args.Length <= 2) ? Character.Controlled : FindMatchingCharacter(args.Skip(2).ToArray()); if (targetCharacter != null) { + if (relativeStrength) + { + afflictionStrength *= targetCharacter.MaxVitality / afflictionPrefab.MaxStrength; + } targetCharacter.CharacterHealth.ApplyAffliction(targetCharacter.AnimController.MainLimb, afflictionPrefab.Instantiate(afflictionStrength)); } }, @@ -707,9 +722,10 @@ namespace Barotrauma commands.Add(new Command("freecamera|freecam", "freecam: Detach the camera from the controlled character.", (string[] args) => { +#if CLIENT + if (Screen.Selected == GameMain.SubEditorScreen) { return; } Character.Controlled = null; GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; -#if CLIENT GameMain.Client?.SendConsoleCommand("freecam"); #endif }, isCheat: true)); @@ -728,19 +744,19 @@ namespace Barotrauma List eventPrefabs = EventSet.GetAllEventPrefabs().Where(prefab => !string.IsNullOrWhiteSpace(prefab.Identifier)).ToList(); if (GameMain.GameSession?.EventManager != null && args.Length > 0) { - EventPrefab newEvent = eventPrefabs.Find(prefab => string.Equals(prefab.Identifier, args[0], StringComparison.InvariantCultureIgnoreCase)); + EventPrefab eventPrefab = eventPrefabs.Find(prefab => string.Equals(prefab.Identifier, args[0], StringComparison.InvariantCultureIgnoreCase)); - if (newEvent != null) + if (eventPrefab != null) { - var @event = newEvent.CreateInstance(); + var newEvent = eventPrefab.CreateInstance(); if (newEvent == null) { NewMessage($"Could not initialize event {args[0]} because level did not meet requirements"); return; } - GameMain.GameSession.EventManager.ActiveEvents.Add(@event); - @event.Init(true); - NewMessage($"Initialized event {newEvent.Identifier}", Color.Aqua); + GameMain.GameSession.EventManager.ActiveEvents.Add(newEvent); + newEvent.Init(true); + NewMessage($"Initialized event {eventPrefab.Identifier}", Color.Aqua); return; } @@ -996,7 +1012,7 @@ namespace Barotrauma } else { - ThrowError("Could not set location reputation ({args[0]} is not a valid reputation value)."); + ThrowError($"Could not set location reputation ({args[0]} is not a valid reputation value)."); } } else @@ -1004,6 +1020,41 @@ namespace Barotrauma ThrowError("Could not set location reputation (no active campaign)."); } }, null, true)); + + commands.Add(new Command("setreputation", "setreputation [faction] [value]: Set the reputation of a cation to the specified value.", (string[] args) => + { + if (args.Length < 2) + { + ThrowError("Insufficient arguments (expected 2)"); + return; + } + + if (GameMain.GameSession?.GameMode is CampaignMode campaign) + { + if (campaign.Factions.FirstOrDefault(f => f.Prefab.Identifier.Equals(args[0], StringComparison.OrdinalIgnoreCase)) is { } faction) + { + if (float.TryParse(args[1], NumberStyles.Any, CultureInfo.InvariantCulture, out float reputation)) + { + faction.Reputation.Value = reputation; + } + else + { + ThrowError($"Could not set faction reputation ({args[1]} is not a valid reputation value)."); + } + } + else + { + ThrowError($"Could not set faction reputation (faction {args[0]} not found)."); + } + } + else + { + ThrowError("Could not set faction reputation (no active campaign)."); + } + }, () => + { + return new[] { FactionPrefab.Prefabs.Select(f => f.Identifier).ToArray() }; + }, true)); commands.Add(new Command("fixitems", "fixitems: Repairs all items and restores them to full condition.", (string[] args) => { @@ -1127,6 +1178,56 @@ namespace Barotrauma UpgradePrefab.Prefabs.Select(c => c.Identifier).Distinct().ToArray() }; }, true)); + + commands.Add(new Command("maxupgrades", "maxupgrades [category] [prefab]: Maxes out all upgrades or only specific one if given arguments.", args => + { + UpgradeManager upgradeManager = GameMain.GameSession?.Campaign?.UpgradeManager; + if (upgradeManager == null) + { + ThrowError("This command can only be used in campaign."); + return; + } + + string categoryIdentifier = null; + string prefabIdentifier = null; + + switch (args.Length) + { + case 1: + categoryIdentifier = args[0]; + break; + case 2: + categoryIdentifier = args[0]; + prefabIdentifier = args[1]; + break; + } + + foreach (UpgradeCategory category in UpgradeCategory.Categories) + { + if (!string.IsNullOrWhiteSpace(categoryIdentifier) && !category.Identifier.Equals(categoryIdentifier, StringComparison.OrdinalIgnoreCase)) { continue; } + foreach (UpgradePrefab prefab in UpgradePrefab.Prefabs) + { + if (!prefab.UpgradeCategories.Contains(category)) { continue; } + if (!string.IsNullOrWhiteSpace(prefabIdentifier) && !prefab.Identifier.Equals(prefabIdentifier, StringComparison.OrdinalIgnoreCase)) { continue; } + + int targetLevel = prefab.MaxLevel - upgradeManager.GetRealUpgradeLevel(prefab, category); + for (int i = 0; i < targetLevel; i++) + { + upgradeManager.PurchaseUpgrade(prefab, category, force: true); + } + NewMessage($"Upgraded {category.Identifier}.{prefab.Identifier} by {targetLevel} levels.", Color.DarkGreen); + } + } + + NewMessage($"Start a new round to apply the upgrades.", Color.Lime); + }, () => + { + return new[] + { + UpgradeCategory.Categories.Select(c => c.Identifier).Distinct().ToArray(), + UpgradePrefab.Prefabs.Select(c => c.Identifier).Distinct().ToArray() + }; + }, true)); commands.Add(new Command("power", "power: Immediately powers up the submarine's nuclear reactor.", (string[] args) => { @@ -1173,11 +1274,14 @@ namespace Barotrauma c.SetAllDamage(200.0f, 0.0f, 0.0f); } } - foreach (Hull hull in Hull.hullList) { hull.BallastFlora?.Kill(); } + foreach (Submarine sub in Submarine.Loaded) + { + sub.WreckAI?.Kill(); + } }, null, isCheat: true)); commands.Add(new Command("setclientcharacter", "setclientcharacter [client name] [character name]: Gives the client control of the specified character.", null, @@ -1692,6 +1796,8 @@ namespace Barotrauma return null; } + // Use same sorting as DebugConsole.ListCharacterNames() above + matchingCharacters = matchingCharacters.OrderBy(c => c.IsDead).ThenByDescending(c => c.IsHuman).ToList(); if (characterIndex == -1) { if (matchingCharacters.Count > 1) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalManager.cs index b107eac25..277efee1c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalManager.cs @@ -110,13 +110,14 @@ namespace Barotrauma public Decal CreateDecal(string decalName, float scale, Vector2 worldPosition, Hull hull, int? spriteIndex = null) { - if (!Prefabs.ContainsKey(decalName.ToLowerInvariant())) + string lowerCaseDecalName = decalName.ToLowerInvariant(); + if (!Prefabs.ContainsKey(lowerCaseDecalName)) { DebugConsole.ThrowError("Decal prefab " + decalName + " not found!"); return null; } - DecalPrefab prefab = Prefabs[decalName]; + DecalPrefab prefab = Prefabs[lowerCaseDecalName]; return new Decal(prefab, scale, worldPosition, hull, spriteIndex); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs index b01df332d..af68ef218 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs @@ -109,7 +109,7 @@ namespace Barotrauma state = 1; break; case 1: - if (!Submarine.MainSub.AtEndPosition && !Submarine.MainSub.AtStartPosition) return; + if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) return; Finished(); state = 2; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckAfflictionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckAfflictionAction.cs index 658c70d5c..b5ff3c16a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckAfflictionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckAfflictionAction.cs @@ -27,25 +27,24 @@ namespace Barotrauma if (string.IsNullOrWhiteSpace(Identifier) || string.IsNullOrWhiteSpace(TargetTag)) { return false; } List targets = ParentEvent.GetTargets(TargetTag).OfType().ToList(); - if (!(targets.FirstOrDefault() is { } target)) { return false; } - - if (TargetLimb == LimbType.None) + foreach (var target in targets) { - Affliction? affliction = target.CharacterHealth?.GetAffliction(Identifier, AllowLimbAfflictions); - return affliction != null; + if (target.CharacterHealth == null) { continue; } + if (TargetLimb == LimbType.None) + { + if (target.CharacterHealth.GetAffliction(Identifier, AllowLimbAfflictions) != null) { return true; } + } + IEnumerable afflictions = target.CharacterHealth.GetAllAfflictions().Where(affliction => + { + LimbType? limbType = target.CharacterHealth.GetAfflictionLimb(affliction)?.type; + if (limbType == null) { return false; } + + return limbType == TargetLimb || true; + }); + + if (afflictions.Any(a => a.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase))) { return true; } } - - if (target.CharacterHealth == null) { return false; } - - IEnumerable afflictions = target.CharacterHealth.GetAllAfflictions().Where(affliction => - { - LimbType? limbType = target.CharacterHealth.GetAfflictionLimb(affliction)?.type; - if (limbType == null) { return false; } - - return limbType == TargetLimb || true; - }); - - return afflictions.Any(a => a.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase)); + return false; } public override string ToDebugString() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs index 1b77d9040..3e12045f1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs @@ -61,7 +61,6 @@ namespace Barotrauma private Character speaker; - private OrderInfo? prevSpeakerOrder; private AIObjective prevIdleObjective, prevGotoObjective; public List Options { get; private set; } @@ -180,6 +179,7 @@ namespace Barotrauma { if (speaker == null) { return; } speaker.CampaignInteractionType = CampaignMode.InteractionType.None; + speaker.ActiveConversation = this; speaker.SetCustomInteract(null, null); #if SERVER GameMain.NetworkMember.CreateEntityEvent(speaker, new object[] { NetEntityEvent.Type.AssignCampaignInteraction }); @@ -187,16 +187,10 @@ namespace Barotrauma var humanAI = speaker.AIController as HumanAIController; if (humanAI != null && !speaker.IsDead && !speaker.Removed) { - if (prevSpeakerOrder != null) - { - humanAI.SetOrder(prevSpeakerOrder.Value.Order, prevSpeakerOrder.Value.OrderOption, orderGiver: null, speak: false); - } - else - { - humanAI.SetOrder(null, string.Empty, orderGiver: null, speak: false); - } + humanAI.ClearForcedOrder(); if (prevIdleObjective != null) { humanAI.ObjectiveManager.AddObjective(prevIdleObjective); } if (prevGotoObjective != null) { humanAI.ObjectiveManager.AddObjective(prevGotoObjective); } + humanAI.ObjectiveManager.SortObjectives(); } } @@ -221,24 +215,24 @@ namespace Barotrauma #if CLIENT Character.DisableControls = true; #endif - if (ShouldInterrupt()) + if (ShouldInterrupt()) { ResetSpeaker(); - interrupt = true; + interrupt = true; } - return; + return; } if (!string.IsNullOrEmpty(SpeakerTag)) { - if (speaker != null && !speaker.Removed && speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk) { return; } + if (speaker != null && !speaker.Removed && speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk && speaker.ActiveConversation?.ParentEvent != this.ParentEvent) { return; } speaker = ParentEvent.GetTargets(SpeakerTag).FirstOrDefault(e => e is Character) as Character; if (speaker == null || speaker.Removed) - { - return; + { + return; } //some conversation already assigned to the speaker, wait for it to be removed - if (speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk) + if (speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk && speaker.ActiveConversation?.ParentEvent != this.ParentEvent) { return; } @@ -249,6 +243,7 @@ namespace Barotrauma else { speaker.CampaignInteractionType = CampaignMode.InteractionType.Talk; + speaker.ActiveConversation = this; #if CLIENT speaker.SetCustomInteract( TryStartConversation, @@ -324,16 +319,11 @@ namespace Barotrauma if (speaker?.AIController is HumanAIController humanAI) { - prevSpeakerOrder = null; - if (humanAI.CurrentOrder != null) - { - prevSpeakerOrder = new OrderInfo(humanAI.CurrentOrder, humanAI.CurrentOrderOption); - } prevIdleObjective = humanAI.ObjectiveManager.GetObjective(); prevGotoObjective = humanAI.ObjectiveManager.GetObjective(); - humanAI.SetOrder( - Order.PrefabList.Find(o => o.Identifier.Equals("wait", StringComparison.OrdinalIgnoreCase)), - option: string.Empty, orderGiver: null, speak: false); + humanAI.SetForcedOrder( + Order.PrefabList.Find(o => o.Identifier.Equals("wait", StringComparison.OrdinalIgnoreCase)), + option: string.Empty, orderGiver: null); if (targets.Any()) { Entity closestTarget = null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs index f2c00b386..9fc395d0a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs @@ -18,14 +18,13 @@ namespace Barotrauma public MissionAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { - //TODO: use event identifier in the error messages if (string.IsNullOrEmpty(MissionIdentifier) && string.IsNullOrEmpty(MissionTag)) { - DebugConsole.ThrowError($"Error in event \"{"event identifier goes here"}\": neither MissionIdentifier or MissionTag has been configured."); + DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": neither MissionIdentifier or MissionTag has been configured."); } if (!string.IsNullOrEmpty(MissionIdentifier) && !string.IsNullOrEmpty(MissionTag)) { - DebugConsole.ThrowError($"Error in event \"{"event identifier goes here"}\": both MissionIdentifier or MissionTag have been configured. The tag will be ignored."); + DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": both MissionIdentifier or MissionTag have been configured. The tag will be ignored."); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs index 35e6a5a62..66b9f360a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -117,30 +117,10 @@ namespace Barotrauma foreach (Item item in newCharacter.Inventory.AllItems) { item.SpawnedInOutpost = true; + item.AllowStealing = false; } } - newCharacter.CharacterHealth.MaxVitality *= humanPrefab.HealthMultiplier; - var humanAI = newCharacter.AIController as HumanAIController; - if (humanAI != null) - { - var idleObjective = humanAI.ObjectiveManager.GetObjective(); - if (idleObjective != null) - { - idleObjective.Behavior = humanPrefab.Behavior; - foreach (string moduleType in humanPrefab.PreferredOutpostModuleTypes) - { - idleObjective.PreferredOutpostModuleTypes.Add(moduleType); - } - } - } - if (humanPrefab.CampaignInteractionType != CampaignMode.InteractionType.None) - { - (GameMain.GameSession.GameMode as CampaignMode)?.AssignNPCMenuInteraction(newCharacter, humanPrefab.CampaignInteractionType); - if (spawnPos != null && humanAI != null) - { - humanAI.ObjectiveManager.SetOrder(new AIObjectiveGoTo(spawnPos, newCharacter, humanAI.ObjectiveManager, repeat: true, getDivingGearIfNeeded: false, closeEnough: 200)); - } - } + humanPrefab.InitializeCharacter(newCharacter, spawnPos); if (!string.IsNullOrEmpty(TargetTag) && newCharacter != null) { ParentEvent.AddTarget(TargetTag, newCharacter); @@ -261,7 +241,7 @@ namespace Barotrauma return GetSpawnPos(SpawnLocation, spawnPointType, targetModuleTags, SpawnPointTag.ToEnumerable()); } - public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable moduleFlags = null, IEnumerable spawnpointTags = null) + public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable moduleFlags = null, IEnumerable spawnpointTags = null, bool asFarAsPossibleFromAirlock = false) { List potentialSpawnPoints = spawnLocation switch { @@ -275,6 +255,7 @@ namespace Barotrauma potentialSpawnPoints = potentialSpawnPoints.FindAll(wp => wp.ConnectedDoor == null && wp.Ladders == null && !wp.isObstructed); + var airlockSpawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull?.OutpostModuleTags?.Contains("airlock") ?? false).ToList(); if (moduleFlags != null && moduleFlags.Any()) { List spawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull?.OutpostModuleTags?.Any(moduleFlags.Contains) ?? false).ToList(); @@ -303,7 +284,7 @@ namespace Barotrauma IEnumerable validSpawnPoints; if (spawnPointType.HasValue) { - validSpawnPoints = potentialSpawnPoints.FindAll(wp => wp.SpawnType == spawnPointType.Value); + validSpawnPoints = potentialSpawnPoints.FindAll(wp => spawnPointType.Value.HasFlag(wp.SpawnType)); } else { @@ -312,7 +293,6 @@ namespace Barotrauma } //don't spawn in an airlock module if there are other options - var airlockSpawnPoints = validSpawnPoints.Where(wp => wp.CurrentHull?.OutpostModuleTags?.Contains("airlock") ?? false); if (airlockSpawnPoints.Count() < validSpawnPoints.Count()) { validSpawnPoints = validSpawnPoints.Except(airlockSpawnPoints); @@ -324,6 +304,12 @@ namespace Barotrauma return potentialSpawnPoints.GetRandom(); } + //avoid using waypoints if there's any actual spawnpoints available + if (validSpawnPoints.Any(wp => wp.SpawnType != SpawnType.Path)) + { + validSpawnPoints = validSpawnPoints.Where(wp => wp.SpawnType != SpawnType.Path); + } + //if not trying to spawn at a tagged spawnpoint, favor spawnpoints without tags if (spawnpointTags == null || !spawnpointTags.Any()) { @@ -334,7 +320,25 @@ namespace Barotrauma } } - return validSpawnPoints.GetRandom(); + if (asFarAsPossibleFromAirlock && airlockSpawnPoints.Any()) + { + WayPoint furthestPoint = validSpawnPoints.First(); + float furthestDist = 0.0f; + foreach (WayPoint waypoint in validSpawnPoints) + { + float dist = Vector2.DistanceSquared(waypoint.WorldPosition, airlockSpawnPoints.First().WorldPosition); + if (dist > furthestDist) + { + furthestDist = dist; + furthestPoint = waypoint; + } + } + return furthestPoint; + } + else + { + return validSpawnPoints.GetRandom(); + } } public override string ToDebugString() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs index 3a2c82772..ef22c7638 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs @@ -6,12 +6,17 @@ namespace Barotrauma { class TagAction : EventAction { + public enum SubType { Any= 0, Player = 1, Outpost = 2, Wreck = 4, BeaconStation = 8 } + [Serialize("", true)] public string Criteria { get; set; } [Serialize("", true)] public string Tag { get; set; } + [Serialize(SubType.Any, true)] + public SubType SubmarineType { get; set; } + [Serialize(true, true)] public bool IgnoreIncapacitatedCharacters { get; set; } @@ -40,15 +45,15 @@ namespace Barotrauma } } - private void TagBots() + private void TagBots(bool playerCrewOnly) { if (IgnoreIncapacitatedCharacters) { - ParentEvent.AddTargetPredicate(Tag, e => e is Character c && c.IsBot && !c.IsIncapacitated); + ParentEvent.AddTargetPredicate(Tag, e => e is Character c && c.IsBot && !c.IsIncapacitated && (!playerCrewOnly || c.TeamID == CharacterTeamType.Team1)); } else { - ParentEvent.AddTargetPredicate(Tag, e => e is Character c && c.IsBot); + ParentEvent.AddTargetPredicate(Tag, e => e is Character c && c.IsBot && (!playerCrewOnly || c.TeamID == CharacterTeamType.Team1)); } } @@ -57,23 +62,44 @@ namespace Barotrauma #if CLIENT GameMain.GameSession.CrewManager.GetCharacters().ForEach(c => ParentEvent.AddTarget(Tag, c)); #else - TagPlayers(); TagBots(); //TODO: this seems like it would tag more than it should, fix + TagPlayers(); + TagBots(playerCrewOnly: true); #endif } private void TagStructuresByIdentifier(string identifier) { - ParentEvent.AddTargetPredicate(Tag, e => e is Structure s && s.Prefab.Identifier.Equals(identifier, StringComparison.InvariantCultureIgnoreCase)); + ParentEvent.AddTargetPredicate(Tag, e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.Prefab.Identifier.Equals(identifier, StringComparison.InvariantCultureIgnoreCase)); } private void TagItemsByIdentifier(string identifier) { - ParentEvent.AddTargetPredicate(Tag, e => e is Item it && it.Prefab.Identifier.Equals(identifier, StringComparison.InvariantCultureIgnoreCase)); + ParentEvent.AddTargetPredicate(Tag, e => e is Item it && SubmarineTypeMatches(it.Submarine) && it.Prefab.Identifier.Equals(identifier, StringComparison.InvariantCultureIgnoreCase)); } private void TagItemsByTag(string tag) { - ParentEvent.AddTargetPredicate(Tag, e => e is Item it && it.HasTag(tag)); + ParentEvent.AddTargetPredicate(Tag, e => e is Item it && SubmarineTypeMatches(it.Submarine) && it.HasTag(tag)); + } + + private bool SubmarineTypeMatches(Submarine sub) + { + if (SubmarineType == SubType.Any) { return true; } + if (sub == null) { return false; } + switch (sub.Info.Type) + { + case Barotrauma.SubmarineType.Player: + return SubmarineType.HasFlag(SubType.Player); + case Barotrauma.SubmarineType.Outpost: + case Barotrauma.SubmarineType.OutpostModule: + return SubmarineType.HasFlag(SubType.Outpost); + case Barotrauma.SubmarineType.Wreck: + return SubmarineType.HasFlag(SubType.Wreck); + case Barotrauma.SubmarineType.BeaconStation: + return SubmarineType.HasFlag(SubType.BeaconStation); + default: + return false; + } } public override void Update(float deltaTime) @@ -91,7 +117,7 @@ namespace Barotrauma TagPlayers(); break; case "bot": - TagBots(); + TagBots(playerCrewOnly: false); break; case "crew": TagCrew(); @@ -113,7 +139,7 @@ namespace Barotrauma public override string ToDebugString() { - return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(TagAction)} -> (Criteria: {Criteria.ColorizeObject()}, Tag: {Tag.ColorizeObject()})"; + return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(TagAction)} -> (Criteria: {Criteria.ColorizeObject()}, Tag: {Tag.ColorizeObject()}, Sub: {SubmarineType.ColorizeObject()})"; } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UnlockPathAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UnlockPathAction.cs new file mode 100644 index 000000000..51ef8e0cf --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UnlockPathAction.cs @@ -0,0 +1,66 @@ +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + class UnlockPathAction : EventAction + { + public UnlockPathAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + + private bool isFinished = false; + + public override bool IsFinished(ref string goTo) + { + return isFinished; + } + + public override void Reset() + { + isFinished = false; + } + + public override void Update(float deltaTime) + { + if (isFinished) { return; } + if (GameMain.GameSession?.Map?.CurrentLocation?.Connections != null) + { + foreach (LocationConnection connection in GameMain.GameSession?.Map?.CurrentLocation?.Connections) + { + if (!connection.Locked) { continue; } + connection.Locked = false; +#if SERVER + NotifyUnlock(connection); +#else + new GUIMessageBox(string.Empty, TextManager.Get("pathunlockedgeneric"), + new string[0], type: GUIMessageBox.Type.InGame, iconStyle: "UnlockPathIcon", relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128)); +#endif + } + } + + isFinished = true; + } + + public override string ToDebugString() + { + return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(UnlockPathAction)}"; + } + +#if SERVER + private void NotifyUnlock(LocationConnection connection) + { + foreach (Client client in GameMain.Server.ConnectedClients) + { + IWriteMessage outmsg = new WriteOnlyMessage(); + outmsg.Write((byte)ServerPacketHeader.EVENTACTION); + outmsg.Write((byte)EventManager.NetworkEventType.UNLOCKPATH); + outmsg.Write((UInt16)GameMain.GameSession.Map.Connections.IndexOf(connection)); + GameMain.Server.ServerPeer.Send(outmsg, client.Connection, DeliveryMethod.Reliable); + } + } +#endif + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index e02fb93a1..32b7481af 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; using Barotrauma.Extensions; +using NLog; namespace Barotrauma { @@ -13,7 +14,8 @@ namespace Barotrauma { CONVERSATION, STATUSEFFECT, - MISSION + MISSION, + UNLOCKPATH } const float IntensityUpdateInterval = 5.0f; @@ -93,6 +95,8 @@ namespace Barotrauma public void StartRound(Level level) { + this.level = level; + if (isClient) { return; } pendingEventSets.Clear(); @@ -107,24 +111,52 @@ namespace Barotrauma totalPathLength = steeringPath.TotalLength; } - this.level = level; SelectSettings(); + int seed = 0; + if (level != null) + { + seed = ToolBox.StringToInt(level.Seed); + foreach (var previousEvent in level.LevelData.EventHistory) + { + seed ^= ToolBox.StringToInt(previousEvent.Identifier); + } + } + MTRandom rand = new MTRandom(seed); + var initialEventSet = SelectRandomEvents(EventSet.List); if (initialEventSet != null) { pendingEventSets.Add(initialEventSet); - int seed = ToolBox.StringToInt(level.Seed); - foreach (var previousEvent in level.LevelData.EventHistory) - { - seed ^= ToolBox.StringToInt(previousEvent.Identifier); - } - MTRandom rand = new MTRandom(seed); CreateEvents(initialEventSet, rand); } if (level?.LevelData?.Type == LevelData.LevelType.Outpost) { + //if the outpost is connected to a locked connection, create an event to unlock it + if (level.StartLocation?.Connections.Any(c => c.Locked && level.StartLocation.MapPosition.X < c.OtherLocation(level.StartLocation).MapPosition.X) ?? false) + { + var unlockPathPrefabs = EventSet.PrefabList.FindAll(e => e.UnlockPathEvent); + var unlockPathPrefabsForBiome = unlockPathPrefabs.FindAll(e => + string.IsNullOrEmpty(e.BiomeIdentifier) || + e.BiomeIdentifier.Equals(level.LevelData.Biome.Identifier, StringComparison.OrdinalIgnoreCase)); + + var unlockPathEventPrefab = unlockPathPrefabsForBiome.Any() ? + ToolBox.SelectWeightedRandom(unlockPathPrefabsForBiome, unlockPathPrefabsForBiome.Select(b => b.Commonness).ToList(), rand) : + ToolBox.SelectWeightedRandom(unlockPathPrefabs, unlockPathPrefabs.Select(b => b.Commonness).ToList(), rand); + if (unlockPathEventPrefab != null) + { + var newEvent = unlockPathEventPrefab.CreateInstance(); + newEvent.Init(true); + ActiveEvents.Add(newEvent); + } + else + { + //if no event that unlocks the path can be found, unlock it automatically + level.StartLocation.Connections.ForEach(c => c.Locked = false); + } + } + level.LevelData.EventHistory.AddRange(selectedEvents.Values.SelectMany(v => v).Select(e => e.Prefab).Where(e => !level.LevelData.EventHistory.Contains(e))); if (level.LevelData.EventHistory.Count > MaxEventHistory) { @@ -134,11 +166,14 @@ namespace Barotrauma void AddChildEvents(EventSet eventSet) { if (eventSet == null) { return; } - foreach (EventPrefab ep in eventSet.EventPrefabs.Select(e => e.First)) + if (eventSet.OncePerOutpost) { - if (!level.LevelData.NonRepeatableEvents.Contains(ep)) + foreach (EventPrefab ep in eventSet.EventPrefabs.Select(e => e.First)) { - level.LevelData.NonRepeatableEvents.Add(ep); + if (!level.LevelData.NonRepeatableEvents.Contains(ep)) + { + level.LevelData.NonRepeatableEvents.Add(ep); + } } } foreach (EventSet childSet in eventSet.ChildSets) @@ -286,14 +321,23 @@ namespace Barotrauma } } RagdollParams ragdollParams; - if (humanoid) + try { - ragdollParams = RagdollParams.GetRagdollParams(speciesName); + if (humanoid) + { + ragdollParams = RagdollParams.GetRagdollParams(characterPrefab.VariantOf ?? speciesName); + } + else + { + ragdollParams = RagdollParams.GetRagdollParams(characterPrefab.VariantOf ?? speciesName); + } } - else + catch (Exception e) { - ragdollParams = RagdollParams.GetRagdollParams(speciesName); + DebugConsole.ThrowError($"Failed to preload a ragdoll file for the character \"{characterPrefab.Name}\"", e); + continue; } + if (ragdollParams != null) { HashSet texturePaths = new HashSet @@ -341,6 +385,8 @@ namespace Barotrauma private void CreateEvents(EventSet eventSet, Random rand) { if (level == null) { return; } + if (level.LevelData.HasHuntingGrounds && eventSet.DisableInHuntingGrounds) { return; } + int applyCount = 1; List> spawnPosFilter = new List>(); if (eventSet.PerRuin) @@ -361,22 +407,27 @@ namespace Barotrauma } else if (eventSet.PerWreck) { - var wrecks = Submarine.Loaded.Where(s => s.Info.IsWreck && (s.WreckAI == null || !s.WreckAI.IsAlive)); + var wrecks = Submarine.Loaded.Where(s => s.Info.IsWreck && (s.WreckAI == null || !s.WreckAI.IsAlive)); applyCount = wrecks.Count(); foreach (var wreck in wrecks) { spawnPosFilter.Add((Level.InterestingPosition pos) => { return pos.Submarine == wreck; }); } } + + var suitablePrefabs = eventSet.EventPrefabs.FindAll(e => + string.IsNullOrEmpty(e.First.BiomeIdentifier) || + e.First.BiomeIdentifier.Equals(Level.Loaded.LevelData?.Biome?.Identifier, StringComparison.OrdinalIgnoreCase)); for (int i = 0; i < applyCount; i++) { if (eventSet.ChooseRandom) { - if (eventSet.EventPrefabs.Count > 0) + if (suitablePrefabs.Count > 0) { - List> unusedEvents = new List>(eventSet.EventPrefabs); + List> unusedEvents = new List>(suitablePrefabs); for (int j = 0; j < eventSet.EventCount; j++) { + if (unusedEvents.All(e => CalculateCommonness(e) <= 0.0f)) { break; } var eventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, unusedEvents.Select(e => CalculateCommonness(e)).ToList(), rand); if (eventPrefab != null) { @@ -402,7 +453,7 @@ namespace Barotrauma } else { - foreach (Pair eventPrefab in eventSet.EventPrefabs) + foreach (Pair eventPrefab in suitablePrefabs) { var newEvent = eventPrefab.First.CreateInstance(); if (newEvent == null) { continue; } @@ -429,11 +480,19 @@ namespace Barotrauma MTRandom rand = new MTRandom(ToolBox.StringToInt(level.Seed)); var allowedEventSets = - eventSets.Where(es => level.Difficulty >= es.MinLevelDifficulty && level.Difficulty <= es.MaxLevelDifficulty && level.LevelData.Type == es.LevelType); + eventSets.Where(es => + level.Difficulty >= es.MinLevelDifficulty && level.Difficulty <= es.MaxLevelDifficulty && + level.LevelData.Type == es.LevelType && + (string.IsNullOrEmpty(es.BiomeIdentifier) || es.BiomeIdentifier.Equals(level.LevelData.Biome.Identifier, StringComparison.OrdinalIgnoreCase))); + + Location location = (GameMain.GameSession?.GameMode as CampaignMode)?.Map?.CurrentLocation ?? level?.StartLocation; + LocationType locationType = location?.GetLocationType(); - if (GameMain.GameSession?.GameMode is CampaignMode campaign && campaign.Map?.CurrentLocation?.Type != null) + if (location != null) { - allowedEventSets = allowedEventSets.Where(set => set.LocationTypeIdentifiers == null || set.LocationTypeIdentifiers.Any(identifier => string.Equals(identifier, campaign.Map.CurrentLocation.Type.Identifier, StringComparison.OrdinalIgnoreCase))); + allowedEventSets = allowedEventSets.Where(set => + set.LocationTypeIdentifiers == null || + set.LocationTypeIdentifiers.Any(identifier => string.Equals(identifier, locationType.Identifier, StringComparison.OrdinalIgnoreCase))); } float totalCommonness = allowedEventSets.Sum(e => e.GetCommonness(level)); @@ -454,8 +513,8 @@ namespace Barotrauma private bool CanStartEventSet(EventSet eventSet) { ISpatialEntity refEntity = GetRefEntity(); - float distFromStart = Vector2.Distance(refEntity.WorldPosition, level.StartPosition); - float distFromEnd = Vector2.Distance(refEntity.WorldPosition, level.EndPosition); + float distFromStart = (float)Math.Sqrt(MathUtils.LineSegmentToPointDistanceSquared(level.StartExitPosition.ToPoint(), level.StartPosition.ToPoint(), refEntity.WorldPosition.ToPoint())); + float distFromEnd = (float)Math.Sqrt(MathUtils.LineSegmentToPointDistanceSquared(level.EndExitPosition.ToPoint(), level.EndPosition.ToPoint(), refEntity.WorldPosition.ToPoint())); //don't create new events if within 50 meters of the start/end of the level if (!eventSet.AllowAtStart) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs index 7727330e4..d605651cc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs @@ -7,19 +7,22 @@ namespace Barotrauma class EventPrefab { public readonly XElement ConfigElement; - public readonly Type EventType; - public readonly string MusicType; + public readonly Type EventType; public readonly float SpawnProbability; public readonly bool TriggerEventCooldown; public float Commonness; public string Identifier; + public string BiomeIdentifier; + + public bool UnlockPathEvent; + public string UnlockPathTooltip; + public int UnlockPathReputation; + public string UnlockPathFaction; public EventPrefab(XElement element) { ConfigElement = element; - MusicType = element.GetAttributeString("musictype", "default"); - try { EventType = Type.GetType("Barotrauma." + ConfigElement.Name, true, true); @@ -34,9 +37,15 @@ namespace Barotrauma } Identifier = ConfigElement.GetAttributeString("identifier", string.Empty); + BiomeIdentifier = ConfigElement.GetAttributeString("biome", string.Empty); Commonness = element.GetAttributeFloat("commonness", 1.0f); SpawnProbability = Math.Clamp(element.GetAttributeFloat("spawnprobability", 1.0f), 0, 1); TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true); + + UnlockPathEvent = element.GetAttributeBool("unlockpathevent", false); + UnlockPathTooltip = element.GetAttributeString("unlockpathtooltip", "lockedpathtooltip"); + UnlockPathReputation = element.GetAttributeInt("unlockpathreputation", 0); + UnlockPathFaction = element.GetAttributeString("unlockpathfaction", ""); } public Event CreateInstance() @@ -57,5 +66,10 @@ namespace Barotrauma return (Event)instance; } + + public override string ToString() + { + return $"EventPrefab ({Identifier})"; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs index 050b131e5..16170a3b0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs @@ -65,6 +65,8 @@ namespace Barotrauma //0-100 public readonly float MinLevelDifficulty, MaxLevelDifficulty; + public readonly string BiomeIdentifier; + public readonly LevelData.LevelType LevelType; public readonly string[] LocationTypeIdentifiers; @@ -84,6 +86,7 @@ namespace Barotrauma public readonly bool IgnoreCoolDown; public readonly bool PerRuin, PerCave, PerWreck; + public readonly bool DisableInHuntingGrounds; public readonly bool OncePerOutpost; @@ -111,6 +114,7 @@ namespace Barotrauma EventPrefabs = new List>(); ChildSets = new List(); + BiomeIdentifier = element.GetAttributeString("biome", string.Empty); MinLevelDifficulty = element.GetAttributeFloat("minleveldifficulty", 0); MaxLevelDifficulty = Math.Max(element.GetAttributeFloat("maxleveldifficulty", 100), MinLevelDifficulty); @@ -139,9 +143,10 @@ namespace Barotrauma PerRuin = element.GetAttributeBool("perruin", false); PerCave = element.GetAttributeBool("percave", false); PerWreck = element.GetAttributeBool("perwreck", false); + DisableInHuntingGrounds = element.GetAttributeBool("disableinhuntinggrounds", false); IgnoreCoolDown = element.GetAttributeBool("ignorecooldown", parentSet?.IgnoreCoolDown ?? (PerRuin || PerCave || PerWreck)); DelayWhenCrewAway = element.GetAttributeBool("delaywhencrewaway", !PerRuin && !PerCave && !PerWreck); - OncePerOutpost = element.GetAttributeBool("perwreck", false); + OncePerOutpost = element.GetAttributeBool("onceperoutpost", false); TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true); Commonness[""] = 1.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs new file mode 100644 index 000000000..0fa581532 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs @@ -0,0 +1,257 @@ +using Barotrauma.Extensions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + partial class AbandonedOutpostMission : Mission + { + private readonly XElement characterConfig; + + protected readonly List characters = new List(); + private readonly Dictionary> characterItems = new Dictionary>(); + protected readonly HashSet requireKill = new HashSet(); + protected readonly HashSet requireRescue = new HashSet(); + + protected const int HostagesKilledState = 5; + + private readonly string hostagesKilledMessage; + + private const float EndDelay = 5.0f; + private float endTimer; + + public override bool AllowRespawn => false; + + public override bool AllowUndocking + { + get + { + if (GameMain.GameSession.GameMode is CampaignMode) { return true; } + return state > 0; + } + } + + protected bool wasDocked; + + public AbandonedOutpostMission(MissionPrefab prefab, Location[] locations) : + base(prefab, locations) + { + characterConfig = prefab.ConfigElement.Element("Characters"); + + string msgTag = prefab.ConfigElement.GetAttributeString("hostageskilledmessage", ""); + hostagesKilledMessage = TextManager.Get(msgTag, returnNull: true) ?? msgTag; + } + + protected override void StartMissionSpecific(Level level) + { + failed = false; + endTimer = 0.0f; + characters.Clear(); + characterItems.Clear(); + requireKill.Clear(); + requireRescue.Clear(); + + var submarine = Submarine.Loaded.Find(s => s.Info.Type == SubmarineType.Outpost) ?? Submarine.MainSub; + if (!IsClient) + { + InitCharacters(submarine); + } + + wasDocked = Submarine.MainSub.DockedTo.Contains(Level.Loaded.StartOutpost); + } + + private void InitCharacters(Submarine submarine) + { + characters.Clear(); + characterItems.Clear(); + + if (characterConfig == null) { return; } + + foreach (XElement element in characterConfig.Elements()) + { + if (GameMain.NetworkMember == null && element.GetAttributeBool("multiplayeronly", false)) { continue; } + + int defaultCount = element.GetAttributeInt("count", -1); + if (defaultCount < 0) + { + defaultCount = element.GetAttributeInt("amount", 1); + } + int min = Math.Min(element.GetAttributeInt("min", defaultCount), 255); + int max = Math.Min(Math.Max(min, element.GetAttributeInt("max", defaultCount)), 255); + int count = Rand.Range(min, max + 1); + + if (element.Attribute("identifier") != null && element.Attribute("from") != null) + { + string characterIdentifier = element.GetAttributeString("identifier", ""); + string characterFrom = element.GetAttributeString("from", ""); + HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier); + if (humanPrefab == null) + { + DebugConsole.ThrowError("Couldn't spawn a character for abandoned outpost mission: character prefab \"" + characterIdentifier + "\" not found"); + continue; + } + for (int i = 0; i < count; i++) + { + LoadHuman(humanPrefab, element, submarine); + } + } + else + { + string speciesName = element.GetAttributeString("character", element.GetAttributeString("identifier", "")); + var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName); + if (characterPrefab == null) + { + DebugConsole.ThrowError("Couldn't spawn a character for abandoned outpost mission: character prefab \"" + speciesName + "\" not found"); + continue; + } + for (int i = 0; i < count; i++) + { + LoadMonster(characterPrefab, element, submarine); + } + } + } + } + + private void LoadHuman(HumanPrefab humanPrefab, XElement element, Submarine submarine) + { + string[] moduleFlags = element.GetAttributeStringArray("moduleflags", null); + string[] spawnPointTags = element.GetAttributeStringArray("spawnpointtags", null); + ISpatialEntity spawnPos = SpawnAction.GetSpawnPos( + SpawnAction.SpawnLocationType.Outpost, SpawnType.Human, + moduleFlags ?? humanPrefab.GetModuleFlags(), + spawnPointTags ?? humanPrefab.GetSpawnPointTags(), + element.GetAttributeBool("asfaraspossible", false)); + if (spawnPos == null) + { + spawnPos = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandom(); + } + + var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: humanPrefab.GetJobPrefab(Rand.RandSync.Server), randSync: Rand.RandSync.Server); + Character spawnedCharacter = Character.Create(characterInfo.SpeciesName, spawnPos.WorldPosition, ToolBox.RandomSeed(8), characterInfo, createNetworkEvent: false); + if (element.GetAttributeBool("requirerescue", false)) + { + requireRescue.Add(spawnedCharacter); + spawnedCharacter.TeamID = CharacterTeamType.FriendlyNPC; +#if CLIENT + GameMain.GameSession.CrewManager.AddCharacterToCrewList(spawnedCharacter); +#endif + } + else + { + spawnedCharacter.TeamID = CharacterTeamType.None; + } + humanPrefab.InitializeCharacter(spawnedCharacter, spawnPos); + humanPrefab.GiveItems(spawnedCharacter, Submarine.MainSub, Rand.RandSync.Server, createNetworkEvents: false); + if (spawnPos is WayPoint wp) + { + spawnedCharacter.GiveIdCardTags(wp); + } + if (element.GetAttributeBool("requirekill", false)) + { + requireKill.Add(spawnedCharacter); + } + characters.Add(spawnedCharacter); + characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true)); + } + + private void LoadMonster(CharacterPrefab monsterPrefab, XElement element, Submarine submarine) + { + string[] moduleFlags = element.GetAttributeStringArray("moduleflags", null); + string[] spawnPointTags = element.GetAttributeStringArray("spawnpointtags", null); + ISpatialEntity spawnPos = SpawnAction.GetSpawnPos(SpawnAction.SpawnLocationType.Outpost, SpawnType.Enemy, moduleFlags, spawnPointTags, element.GetAttributeBool("asfaraspossible", false)); + if (spawnPos == null) + { + spawnPos = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandom(); + } + Character spawnedCharacter = Character.Create(monsterPrefab.Identifier, spawnPos.WorldPosition, ToolBox.RandomSeed(8), createNetworkEvent: false); + characters.Add(spawnedCharacter); + if (element.GetAttributeBool("requirekill", false)) + { + requireKill.Add(spawnedCharacter); + } + if (spawnedCharacter.Inventory != null) + { + characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true)); + } + if (submarine != null && spawnedCharacter.AIController is EnemyAIController enemyAi) + { + enemyAi.UnattackableSubmarines.Add(submarine); + enemyAi.UnattackableSubmarines.Add(Submarine.MainSub); + foreach (Submarine sub in Submarine.MainSub.DockedTo) + { + enemyAi.UnattackableSubmarines.Add(sub); + } + } + } + + + public override void Update(float deltaTime) + { + if (State != HostagesKilledState) + { + if (requireRescue.Any(r => r.Removed || r.IsDead)) + { + State = HostagesKilledState; + return; + } + } + else + { + endTimer += deltaTime; + if (endTimer > EndDelay) + { +#if SERVER + if (!(GameMain.GameSession.GameMode is CampaignMode) && GameMain.Server != null) + { + GameMain.Server.EndGame(); + } +#endif + } + } + + switch (state) + { + case 0: + + if (requireKill.All(c => c.Removed || c.IsDead) && + requireRescue.All(c => c.Submarine?.Info.Type == SubmarineType.Player)) + { + State = 1; + } + break; +#if SERVER + case 1: + if (!(GameMain.GameSession.GameMode is CampaignMode) && GameMain.Server != null) + { + if (!Submarine.MainSub.AtStartExit || (wasDocked && !Submarine.MainSub.DockedTo.Contains(Level.Loaded.StartOutpost))) + { + GameMain.Server.EndGame(); + State = 2; + } + } + break; +#endif + } + + } + + public override void End() + { + completed = State > 0 && State != HostagesKilledState; + if (completed) + { + if (Prefab.LocationTypeChangeOnCompleted != null) + { + ChangeLocationType(Prefab.LocationTypeChangeOnCompleted); + } + GiveReward(); + } + else + { + failed = requireRescue.Any(r => r.Removed || r.IsDead); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs index c337b56a6..b3a5365f1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs @@ -11,7 +11,6 @@ namespace Barotrauma private bool swarmSpawned; private readonly string monsterSpeciesName; private Point monsterCountRange; - private Level level; private readonly string sonarLabel; public BeaconMission(MissionPrefab prefab, Location[] locations) : base(prefab, locations) @@ -54,11 +53,6 @@ namespace Barotrauma } } - public override void Start(Level level) - { - this.level = level; - } - public override void Update(float deltaTime) { if (IsClient) { return; } @@ -113,8 +107,15 @@ namespace Barotrauma completed = level.CheckBeaconActive(); if (completed) { - ChangeLocationType("None", "Explored"); + if (Prefab.LocationTypeChangeOnCompleted != null) + { + ChangeLocationType(Prefab.LocationTypeChangeOnCompleted); + } GiveReward(); + if (level?.LevelData != null) + { + level.LevelData.IsBeaconActive = true; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs index b7fa8a6a9..2e1419d80 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs @@ -96,7 +96,8 @@ namespace Barotrauma var item = new Item(itemPrefab, position, cargoRoom.Submarine) { - SpawnedInOutpost = true + SpawnedInOutpost = true, + AllowStealing = false }; item.FindHull(); items.Add(item); @@ -118,7 +119,7 @@ namespace Barotrauma } } - public override void Start(Level level) + protected override void StartMissionSpecific(Level level) { items.Clear(); parentInventoryIDs.Clear(); @@ -131,13 +132,17 @@ namespace Barotrauma public override void End() { - if (Submarine.MainSub != null && Submarine.MainSub.AtEndPosition) + if (Submarine.MainSub != null && Submarine.MainSub.AtEndExit) { int deliveredItemCount = items.Count(i => i.CurrentHull != null && !i.Removed && i.Condition > 0.0f); if (deliveredItemCount >= requiredDeliveryAmount) { GiveReward(); completed = true; + if (Prefab.LocationTypeChangeOnCompleted != null) + { + ChangeLocationType(Prefab.LocationTypeChangeOnCompleted); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs index 0362e10de..054436bfd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using System.Collections.Generic; namespace Barotrauma @@ -89,8 +90,8 @@ namespace Barotrauma Winner != CharacterTeamType.None && Winner == character.TeamID; } - - public override void Start(Level level) + + protected override void StartMissionSpecific(Level level) { if (GameMain.NetworkMember == null) { @@ -99,23 +100,23 @@ namespace Barotrauma } subs = new Submarine[] { Submarine.MainSubs[0], Submarine.MainSubs[1] }; - subs[0].TeamID = CharacterTeamType.Team1; subs[1].TeamID = CharacterTeamType.Team2; - subs[0].NeutralizeBallast(); subs[1].NeutralizeBallast(); + + subs[0].NeutralizeBallast(); + subs[0].TeamID = CharacterTeamType.Team1; + subs[0].DockedTo.ForEach(s => s.TeamID = CharacterTeamType.Team1); + + subs[1].NeutralizeBallast(); + subs[1].TeamID = CharacterTeamType.Team2; + subs[1].DockedTo.ForEach(s => s.TeamID = CharacterTeamType.Team2); subs[1].SetPosition(subs[1].FindSpawnPos(Level.Loaded.EndPosition)); subs[1].FlipX(); crews = new List[] { new List(), new List() }; - - foreach (Submarine submarine in Submarine.Loaded) - { - //hide all subs from sonar to make sneak attacks possible - submarine.ShowSonarMarker = false; - } } public override void End() { - if (GameMain.NetworkMember == null) return; + if (GameMain.NetworkMember == null) { return; } if (Winner != CharacterTeamType.None) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs index 1aeb97ac3..ef15b10df 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs @@ -44,7 +44,7 @@ namespace Barotrauma } } - public override void Start(Level level) + protected override void StartMissionSpecific(Level level) { if (SpawnedResources.Any()) { @@ -125,7 +125,7 @@ namespace Barotrauma State = 1; break; case 1: - if (!Submarine.MainSub.AtEndPosition && !Submarine.MainSub.AtStartPosition) { return; } + if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) { return; } State = 2; break; } @@ -135,6 +135,10 @@ namespace Barotrauma { if (EnoughHaveBeenCollected()) { + if (Prefab.LocationTypeChangeOnCompleted != null) + { + ChangeLocationType(Prefab.LocationTypeChangeOnCompleted); + } GiveReward(); completed = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index bcf370b21..fa3d252da 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -1,9 +1,8 @@ -using Barotrauma.Networking; -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; -using System.Reflection; namespace Barotrauma { @@ -11,8 +10,11 @@ namespace Barotrauma { public readonly MissionPrefab Prefab; protected bool completed, failed; + + protected Level level; + protected int state; - public int State + public virtual int State { get { return state; } protected set @@ -21,7 +23,7 @@ namespace Barotrauma { state = value; #if SERVER - GameMain.Server?.UpdateMissionState(state); + GameMain.Server?.UpdateMissionState(this, state); #endif ShowMessage(State); } @@ -38,25 +40,30 @@ namespace Barotrauma get { return Prefab.Name; } } - private string successMessage; + private readonly string successMessage; public virtual string SuccessMessage { get { return successMessage; } - private set { successMessage = value; } + //private set { successMessage = value; } } - private string failureMessage; + private readonly string failureMessage; public virtual string FailureMessage { get { return failureMessage; } - private set { failureMessage = value; } + //private set { failureMessage = value; } } protected string description; public virtual string Description { get { return description; } - private set { description = value; } + //private set { description = value; } + } + + public virtual bool AllowUndocking + { + get { return true; } } public int Reward @@ -100,6 +107,11 @@ namespace Barotrauma } public readonly Location[] Locations; + + public int? Difficulty + { + get { return Prefab.Difficulty; } + } public Mission(MissionPrefab prefab, Location[] locations) { @@ -109,7 +121,7 @@ namespace Barotrauma description = prefab.Description; successMessage = prefab.SuccessMessage; - FailureMessage = prefab.FailureMessage; + failureMessage = prefab.FailureMessage; Headers = new List(prefab.Headers); Messages = new List(prefab.Messages); @@ -117,20 +129,22 @@ namespace Barotrauma for (int n = 0; n < 2; n++) { - if (description != null) description = description.Replace("[location" + (n + 1) + "]", locations[n].Name); - if (successMessage != null) successMessage = successMessage.Replace("[location" + (n + 1) + "]", locations[n].Name); - if (failureMessage != null) failureMessage = failureMessage.Replace("[location" + (n + 1) + "]", locations[n].Name); + string locationName = $"‖color:gui.orange‖{locations[n].Name}‖end‖"; + if (description != null) { description = description.Replace("[location" + (n + 1) + "]", locationName); } + if (successMessage != null) { successMessage = successMessage.Replace("[location" + (n + 1) + "]", locationName); } + if (failureMessage != null) { failureMessage = failureMessage.Replace("[location" + (n + 1) + "]", locationName); } for (int m = 0; m < Messages.Count; m++) { - Messages[m] = Messages[m].Replace("[location" + (n + 1) + "]", locations[n].Name); + Messages[m] = Messages[m].Replace("[location" + (n + 1) + "]", locationName); } } - if (description != null) description = description.Replace("[reward]", Reward.ToString("N0")); - if (successMessage != null) successMessage = successMessage.Replace("[reward]", Reward.ToString("N0")); - if (failureMessage != null) failureMessage = failureMessage.Replace("[reward]", Reward.ToString("N0")); + string rewardText = $"‖color:gui.orange‖{string.Format(CultureInfo.InvariantCulture, "{0:N0}", Reward)}‖end‖"; + if (description != null) { description = description.Replace("[reward]", rewardText); } + if (successMessage != null) { successMessage = successMessage.Replace("[reward]", rewardText); } + if (failureMessage != null) { failureMessage = failureMessage.Replace("[reward]", rewardText); } for (int m = 0; m < Messages.Count; m++) { - Messages[m] = Messages[m].Replace("[reward]", Reward.ToString("N0")); + Messages[m] = Messages[m].Replace("[reward]", rewardText); } } public static Mission LoadRandom(Location[] locations, string seed, bool requireCorrectLocationType, MissionType missionType, bool isSinglePlayer = false) @@ -175,7 +189,23 @@ namespace Barotrauma return null; } - public virtual void Start(Level level) { } + public void Start(Level level) + { +#if CLIENT + shownMessages.Clear(); +#endif + foreach (string categoryToShow in Prefab.UnhideEntitySubCategories) + { + foreach (MapEntity entityToShow in MapEntity.mapEntityList.Where(me => me.prefab?.HasSubCategory(categoryToShow) ?? false)) + { + entityToShow.HiddenInGame = false; + } + } + this.level = level; + StartMissionSpecific(level); + } + + protected virtual void StartMissionSpecific(Level level) { } public virtual void Update(float deltaTime) { } @@ -192,7 +222,10 @@ namespace Barotrauma public virtual void End() { completed = true; - + if (Prefab.LocationTypeChangeOnCompleted != null) + { + ChangeLocationType(Prefab.LocationTypeChangeOnCompleted); + } GiveReward(); } @@ -224,22 +257,32 @@ namespace Barotrauma } } - protected void ChangeLocationType(string from, string to) + protected void ChangeLocationType(LocationTypeChange change) { + if (change == null) { throw new ArgumentException(); } if (GameMain.GameSession.GameMode is CampaignMode && !IsClient) { int srcIndex = -1; for (int i = 0; i < Locations.Length; i++) { - if (Locations[i].Type.Identifier.Equals(from, StringComparison.OrdinalIgnoreCase)) + if (Locations[i].Type.Identifier.Equals(change.CurrentType, StringComparison.OrdinalIgnoreCase)) { srcIndex = i; break; } } if (srcIndex == -1) { return; } - var upgradeLocation = Locations[srcIndex]; - upgradeLocation.ChangeType(LocationType.List.Find(lt => lt.Identifier.Equals(to, StringComparison.OrdinalIgnoreCase))); + var location = Locations[srcIndex]; + + if (change.RequiredDurationRange.X > 0) + { + location.PendingLocationTypeChange = (change, Rand.Range(change.RequiredDurationRange.X, change.RequiredDurationRange.Y), Prefab); + } + else + { + location.ChangeType(LocationType.List.Find(lt => lt.Identifier.Equals(change.ChangeToType, StringComparison.OrdinalIgnoreCase))); + location.LocationTypeChangeCooldown = change.CooldownAfterChange; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs index 63cb3de8b..274f041cf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -18,7 +18,10 @@ namespace Barotrauma Nest = 0x10, Mineral = 0x20, Combat = 0x40, - All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat + OutpostDestroy = 0x80, + OutpostRescue = 0x100, + + All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | OutpostDestroy | OutpostRescue } partial class MissionPrefab @@ -33,6 +36,8 @@ namespace Barotrauma { MissionType.Beacon, typeof(BeaconMission) }, { MissionType.Nest, typeof(NestMission) }, { MissionType.Mineral, typeof(MineralMission) }, + { MissionType.OutpostDestroy, typeof(OutpostDestroyMission) }, + { MissionType.OutpostRescue, typeof(AbandonedOutpostMission) }, }; public static readonly Dictionary PvPMissionClasses = new Dictionary() { @@ -67,14 +72,34 @@ namespace Barotrauma public readonly List> DataRewards = new List>(); public readonly int Commonness; + public readonly int? Difficulty; + public const int MinDifficulty = 1, MaxDifficulty = 4; public readonly int Reward; public readonly List Headers; public readonly List Messages; - //the mission can only be received when travelling from Pair.First to Pair.Second - public readonly List> AllowedLocationTypes; + public readonly bool AllowRetry; + + public readonly bool IsSideObjective; + + /// + /// The mission can only be received when travelling from Pair.First to Pair.Second + /// + public readonly List> AllowedConnectionTypes; + + /// + /// The mission can only be received in these location types + /// + public readonly List AllowedLocationTypes = new List(); + + /// + /// Show entities belonging to these sub categories when the mission starts + /// + public readonly List UnhideEntitySubCategories = new List(); + + public LocationTypeChange LocationTypeChangeOnCompleted; public readonly XElement ConfigElement; @@ -130,8 +155,14 @@ namespace Barotrauma Name = TextManager.Get("MissionName." + TextIdentifier, true) ?? element.GetAttributeString("name", ""); Description = TextManager.Get("MissionDescription." + TextIdentifier, true) ?? element.GetAttributeString("description", ""); Reward = element.GetAttributeInt("reward", 1); - + AllowRetry = element.GetAttributeBool("allowretry", false); + IsSideObjective = element.GetAttributeBool("sideobjective", false); Commonness = element.GetAttributeInt("commonness", 1); + if (element.GetAttribute("difficulty") != null) + { + int difficulty = element.GetAttributeInt("difficulty", MinDifficulty); + Difficulty = Math.Clamp(difficulty, MinDifficulty, MaxDifficulty); + } SuccessMessage = TextManager.Get("MissionSuccess." + TextIdentifier, true) ?? element.GetAttributeString("successmessage", "Mission completed successfully"); FailureMessage = TextManager.Get("MissionFailure." + TextIdentifier, true) ?? ""; @@ -144,7 +175,10 @@ namespace Barotrauma FailureMessage = element.GetAttributeString("failuremessage", ""); } - SonarLabel = TextManager.Get("MissionSonarLabel." + TextIdentifier, true) ?? element.GetAttributeString("sonarlabel", ""); + SonarLabel = + TextManager.Get("MissionSonarLabel." + TextIdentifier, true) ?? + TextManager.Get("MissionSonarLabel." + element.GetAttributeString("sonarlabel", ""), true) ?? + element.GetAttributeString("sonarlabel", ""); SonarIconIdentifier = element.GetAttributeString("sonaricon", ""); MultiplayerOnly = element.GetAttributeBool("multiplayeronly", false); @@ -152,9 +186,11 @@ namespace Barotrauma AchievementIdentifier = element.GetAttributeString("achievementidentifier", ""); + UnhideEntitySubCategories = element.GetAttributeStringArray("unhideentitysubcategories", new string[0]).ToList(); + Headers = new List(); Messages = new List(); - AllowedLocationTypes = new List>(); + AllowedConnectionTypes = new List>(); for (int i = 0; i < 100; i++) { @@ -183,9 +219,20 @@ namespace Barotrauma messageIndex++; break; case "locationtype": - AllowedLocationTypes.Add(new Pair( - subElement.GetAttributeString("from", ""), - subElement.GetAttributeString("to", ""))); + case "connectiontype": + if (subElement.Attribute("identifier") != null) + { + AllowedLocationTypes.Add(subElement.GetAttributeString("identifier", "")); + } + else + { + AllowedConnectionTypes.Add(new Pair( + subElement.GetAttributeString("from", ""), + subElement.GetAttributeString("to", ""))); + } + break; + case "locationtypechange": + LocationTypeChangeOnCompleted = new LocationTypeChange(subElement.GetAttributeString("from", ""), subElement, requireChangeMessages: false, defaultProbability: 1.0f); break; case "reputation": case "reputationreward": @@ -257,19 +304,32 @@ namespace Barotrauma public bool IsAllowed(Location from, Location to) { - foreach (Pair allowedLocationType in AllowedLocationTypes) + if (from == to) { - if (allowedLocationType.First.Equals("any", StringComparison.OrdinalIgnoreCase) || - allowedLocationType.First.Equals(from.Type.Identifier, StringComparison.OrdinalIgnoreCase)) + return + AllowedLocationTypes.Any(lt => lt.Equals("any", StringComparison.OrdinalIgnoreCase)) || + AllowedLocationTypes.Any(lt => lt.Equals(from.Type.Identifier, StringComparison.OrdinalIgnoreCase)); + } + + foreach (Pair allowedConnectionType in AllowedConnectionTypes) + { + if (allowedConnectionType.First.Equals("any", StringComparison.OrdinalIgnoreCase) || + allowedConnectionType.First.Equals(from.Type.Identifier, StringComparison.OrdinalIgnoreCase)) { - if (allowedLocationType.Second.Equals("any", StringComparison.OrdinalIgnoreCase) || - allowedLocationType.Second.Equals(to.Type.Identifier, StringComparison.OrdinalIgnoreCase)) + if (allowedConnectionType.Second.Equals("any", StringComparison.OrdinalIgnoreCase) || + allowedConnectionType.Second.Equals(to.Type.Identifier, StringComparison.OrdinalIgnoreCase)) { return true; } } } + if (Type == MissionType.Beacon) + { + var connection = from.Connections.Find(c => c.Locations.Contains(from) && c.Locations.Contains(to)); + if (connection?.LevelData == null || !connection.LevelData.HasBeaconStation || connection.LevelData.IsBeaconActive) { return false; } + } + return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs index e92abdbdb..bf83c9e61 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs @@ -8,7 +8,7 @@ namespace Barotrauma partial class MonsterMission : Mission { //string = filename, point = min,max - private readonly HashSet> monsterPrefabs = new HashSet>(); + private readonly HashSet<(CharacterPrefab character, Point amountRange)> monsterPrefabs = new HashSet<(CharacterPrefab character, Point amountRange)>(); private readonly List monsters = new List(); private readonly List sonarPositions = new List(); @@ -16,6 +16,7 @@ namespace Barotrauma private readonly float maxSonarMarkerDistance = 10000.0f; + private readonly Level.PositionType spawnPosType; public override IEnumerable SonarPositions { @@ -42,7 +43,7 @@ namespace Barotrauma if (characterPrefab != null) { int monsterCount = Math.Min(prefab.ConfigElement.GetAttributeInt("monstercount", 1), 255); - monsterPrefabs.Add(new Tuple(characterPrefab, new Point(monsterCount))); + monsterPrefabs.Add((characterPrefab, new Point(monsterCount))); } else { @@ -52,6 +53,13 @@ namespace Barotrauma maxSonarMarkerDistance = prefab.ConfigElement.GetAttributeFloat("maxsonarmarkerdistance", 10000.0f); + var spawnPosTypeStr = prefab.ConfigElement.GetAttributeString("spawntype", ""); + if (string.IsNullOrWhiteSpace(spawnPosTypeStr) || + !Enum.TryParse(spawnPosTypeStr, true, out spawnPosType)) + { + spawnPosType = Level.PositionType.MainPath | Level.PositionType.SidePath; + } + foreach (var monsterElement in prefab.ConfigElement.GetChildElements("monster")) { speciesName = monsterElement.GetAttributeString("character", string.Empty); @@ -65,7 +73,7 @@ namespace Barotrauma var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName); if (characterPrefab != null) { - monsterPrefabs.Add(new Tuple(characterPrefab, new Point(min, max))); + monsterPrefabs.Add((characterPrefab, new Point(min, max))); } else { @@ -75,14 +83,14 @@ namespace Barotrauma if (monsterPrefabs.Any()) { - var characterParams = new CharacterParams(monsterPrefabs.First().Item1.FilePath); + var characterParams = new CharacterParams(monsterPrefabs.First().character.FilePath); description = description.Replace("[monster]", TextManager.Get("character." + characterParams.SpeciesTranslationOverride, returnNull: true) ?? TextManager.Get("character." + characterParams.SpeciesName)); } } - - public override void Start(Level level) + + protected override void StartMissionSpecific(Level level) { if (monsters.Count > 0) { @@ -106,13 +114,13 @@ namespace Barotrauma if (!IsClient) { - Level.Loaded.TryGetInterestingPosition(true, Level.PositionType.MainPath | Level.PositionType.SidePath, Level.Loaded.Size.X * 0.3f, out Vector2 spawnPos); - foreach (var monster in monsterPrefabs) + Level.Loaded.TryGetInterestingPosition(true, spawnPosType, Level.Loaded.Size.X * 0.3f, out Vector2 spawnPos); + foreach (var (character, amountRange) in monsterPrefabs) { - int amount = Rand.Range(monster.Item2.X, monster.Item2.Y + 1); + int amount = Rand.Range(amountRange.X, amountRange.Y + 1); for (int i = 0; i < amount; i++) { - monsters.Add(Character.Create(monster.Item1.Identifier, spawnPos, ToolBox.RandomSeed(8), createNetworkEvent: false)); + monsters.Add(Character.Create(character.Identifier, spawnPos, ToolBox.RandomSeed(8), createNetworkEvent: false)); } } @@ -213,9 +221,17 @@ namespace Barotrauma tempSonarPositions.Clear(); monsters.Clear(); if (State < 1) { return; } - + + if (Prefab.LocationTypeChangeOnCompleted != null) + { + ChangeLocationType(Prefab.LocationTypeChangeOnCompleted); + } GiveReward(); completed = true; + if (level?.LevelData != null && Prefab.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase) || t.Equals("huntinggroundsnoreward", StringComparison.OrdinalIgnoreCase))) + { + level.LevelData.HasHuntingGrounds = false; + } } public bool IsEliminated(Character enemy) => diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs index 6a8117551..b97ade2ee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs @@ -90,7 +90,7 @@ namespace Barotrauma } - public override void Start(Level level) + protected override void StartMissionSpecific(Level level) { if (items.Any()) { @@ -270,7 +270,7 @@ namespace Barotrauma break; case 1: - if (!Submarine.MainSub.AtEndPosition && !Submarine.MainSub.AtStartPosition) { return; } + if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) { return; } State = 2; break; } @@ -309,7 +309,10 @@ namespace Barotrauma completed = true; if (completed) { - ChangeLocationType("None", "Explored"); + if (Prefab.LocationTypeChangeOnCompleted != null) + { + ChangeLocationType(Prefab.LocationTypeChangeOnCompleted); + } } } foreach (Item item in items) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/OutpostDestroyMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/OutpostDestroyMission.cs new file mode 100644 index 000000000..029db236f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/OutpostDestroyMission.cs @@ -0,0 +1,165 @@ +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + partial class OutpostDestroyMission : AbandonedOutpostMission + { + private readonly string itemTag; + private readonly XElement itemConfig; + private readonly List items = new List(); + + public override IEnumerable SonarPositions + { + get + { + if (State > 0) + { + return Enumerable.Empty(); + } + else + { + return Targets.Select(t => t.WorldPosition); + } + } + } + + private IEnumerable Targets + { + get + { + if (State > 0) + { + return Enumerable.Empty(); + } + else + { + if (items.Any()) + { + return items.Where(it => !it.Removed && it.Condition > 0.0f).Cast().Concat(requireKill.Where(c => !c.Removed && !c.IsDead)).Concat(requireRescue); + } + else + { + return requireKill.Concat(requireRescue); + } + } + } + } + + public OutpostDestroyMission(MissionPrefab prefab, Location[] locations) : + base(prefab, locations) + { + itemConfig = prefab.ConfigElement.Element("Items"); + itemTag = prefab.ConfigElement.GetAttributeString("targetitem", ""); + } + + protected override void StartMissionSpecific(Level level) + { + items.Clear(); +#if SERVER + spawnedItems.Clear(); +#endif + if (!string.IsNullOrEmpty(itemTag)) + { + var itemsToDestroy = Item.ItemList.FindAll(it => it.Submarine?.Info.Type != SubmarineType.Player && it.HasTag(itemTag)); + if (!itemsToDestroy.Any()) + { + DebugConsole.ThrowError($"Error in mission \"{Prefab.Identifier}\". Could not find an item with the tag \"{itemTag}\"."); + } + else + { + items.AddRange(itemsToDestroy); + } + } + if (itemConfig != null && !IsClient) + { + foreach (XElement element in itemConfig.Elements()) + { + string itemIdentifier = element.GetAttributeString("identifier", ""); + if (!(MapEntityPrefab.Find(null, itemIdentifier) is ItemPrefab itemPrefab)) + { + DebugConsole.ThrowError("Couldn't spawn item for outpost destroy mission: item prefab \"" + itemIdentifier + "\" not found"); + continue; + } + + string[] moduleFlags = element.GetAttributeStringArray("moduleflags", null); + string[] spawnPointTags = element.GetAttributeStringArray("spawnpointtags", null); + ISpatialEntity spawnPoint = SpawnAction.GetSpawnPos( + SpawnAction.SpawnLocationType.Outpost, SpawnType.Human | SpawnType.Enemy, + moduleFlags, spawnPointTags, element.GetAttributeBool("asfaraspossible", false)); + if (spawnPoint == null) + { + var submarine = Submarine.Loaded.Find(s => s.Info.Type == SubmarineType.Outpost) ?? Submarine.MainSub; + spawnPoint = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandom(); + } + Vector2 spawnPos = spawnPoint.WorldPosition; + if (spawnPoint is WayPoint wp && wp.CurrentHull != null) + { + spawnPos = new Vector2( + MathHelper.Clamp(wp.WorldPosition.X + Rand.Range(-200, 200), wp.CurrentHull.WorldRect.X, wp.CurrentHull.WorldRect.Right), + wp.CurrentHull.WorldRect.Y - wp.CurrentHull.Rect.Height + 16.0f); + } + var item = new Item(itemPrefab, spawnPos, null); + items.Add(item); +#if SERVER + spawnedItems.Add(item); +#endif + } + } + + base.StartMissionSpecific(level); + } + + public override void Update(float deltaTime) + { + if (requireRescue.Any(r => r.Removed || r.IsDead)) + { +#if SERVER + if (!(GameMain.GameSession.GameMode is CampaignMode) && GameMain.Server != null) + { + GameMain.Server.EndGame(); + } +#endif + return; + } + + switch (state) + { + case 0: + if (items.Any()) + { + if (items.All(it => it.Removed || it.Condition <= 0.0f) && + requireKill.All(c => c.Removed || c.IsDead) && + requireRescue.All(c => c.Submarine?.Info.Type == SubmarineType.Player)) + { + State = 1; + } + } + else + { + if (requireKill.All(c => c.Removed || c.IsDead) && + requireRescue.All(c => c.Submarine?.Info.Type == SubmarineType.Player)) + { + State = 1; + } + } + break; +#if SERVER + case 1: + if (!(GameMain.GameSession.GameMode is CampaignMode) && GameMain.Server != null) + { + if (!Submarine.MainSub.AtStartExit || (wasDocked && !Submarine.MainSub.DockedTo.Contains(Level.Loaded.StartOutpost))) + { + GameMain.Server.EndGame(); + State = 2; + } + } + break; +#endif + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index 8d90652d7..4738a3a66 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -102,7 +102,7 @@ namespace Barotrauma } } - public override void Start(Level level) + protected override void StartMissionSpecific(Level level) { #if SERVER originalInventoryID = Entity.NullEntityID; @@ -239,7 +239,7 @@ namespace Barotrauma State = 1; break; case 1: - if (!Submarine.MainSub.AtEndPosition && !Submarine.MainSub.AtStartPosition) { return; } + if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) { return; } State = 2; break; } @@ -248,11 +248,16 @@ namespace Barotrauma public override void End() { var root = item.GetRootContainer() ?? item; - if (root.CurrentHull?.Submarine == null || (!root.CurrentHull.Submarine.AtEndPosition && !root.CurrentHull.Submarine.AtStartPosition) || item.Removed) + if (root.CurrentHull?.Submarine == null || (!root.CurrentHull.Submarine.AtEndExit && !root.CurrentHull.Submarine.AtStartExit) || item.Removed) { return; } + if (Prefab.LocationTypeChangeOnCompleted != null) + { + ChangeLocationType(Prefab.LocationTypeChangeOnCompleted); + } + item?.Remove(); item = null; GiveReward(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index 1414a2f7b..e7bf21f0e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -16,16 +16,16 @@ namespace Barotrauma private readonly float scatter; private readonly float offset; - private readonly bool spawnDeep; - private Vector2? spawnPos; - private readonly bool disallowed; + private bool disallowed; private readonly Level.PositionType spawnPosType; private bool spawnPending; + private int maxAmountPerLevel = int.MaxValue; + public List Monsters => monsters; public Vector2? SpawnPos => spawnPos; public bool SpawnPending => spawnPending; @@ -72,15 +72,21 @@ namespace Barotrauma minAmount = prefab.ConfigElement.GetAttributeInt("minamount", defaultAmount); maxAmount = Math.Max(prefab.ConfigElement.GetAttributeInt("maxamount", 1), minAmount); - var spawnPosTypeStr = prefab.ConfigElement.GetAttributeString("spawntype", ""); + maxAmountPerLevel = prefab.ConfigElement.GetAttributeInt("maxamountperlevel", int.MaxValue); + var spawnPosTypeStr = prefab.ConfigElement.GetAttributeString("spawntype", ""); if (string.IsNullOrWhiteSpace(spawnPosTypeStr) || !Enum.TryParse(spawnPosTypeStr, true, out spawnPosType)) { spawnPosType = Level.PositionType.MainPath; } - spawnDeep = prefab.ConfigElement.GetAttributeBool("spawndeep", false); + //backwards compatibility + if (prefab.ConfigElement.GetAttributeBool("spawndeep", false)) + { + spawnPosType = Level.PositionType.Abyss; + } + offset = prefab.ConfigElement.GetAttributeFloat("offset", 0); scatter = Math.Clamp(prefab.ConfigElement.GetAttributeFloat("scatter", 1000), 0, 3000); @@ -163,15 +169,6 @@ namespace Barotrauma { removals.Add(position); } - if (spawnDeep) - { - for (int i = 0; i < availablePositions.Count; i++) - { - var pos = availablePositions[i].Position; - pos = new Point(pos.X, pos.Y - Level.Loaded.Size.Y); - availablePositions[i] = new Level.InterestingPosition(pos, availablePositions[i].PositionType); - } - } if (position.Position.Y < Level.Loaded.GetBottomPosition(position.Position.X).Y) { removals.Add(position); @@ -196,7 +193,7 @@ namespace Barotrauma var availablePositions = GetAvailableSpawnPositions(); var chosenPosition = new Level.InterestingPosition(Point.Zero, Level.PositionType.MainPath, isValid: false); bool isSubOrWreck = spawnPosType == Level.PositionType.Ruin || spawnPosType == Level.PositionType.Wreck; - if (affectSubImmediately && !isSubOrWreck) + if (affectSubImmediately && !isSubOrWreck && spawnPosType != Level.PositionType.Abyss) { if (availablePositions.None()) { @@ -218,7 +215,7 @@ namespace Barotrauma float dist = Vector2.DistanceSquared(pos, refSub.WorldPosition); foreach (Submarine sub in Submarine.Loaded) { - if (sub.Info.Type != SubmarineType.Player) { continue; } + if (sub.Info.Type != SubmarineType.Player && sub != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle) { continue; } float minDistToSub = GetMinDistanceToSub(sub); if (dist < minDistToSub * minDistToSub) { continue; } @@ -276,6 +273,7 @@ namespace Barotrauma { for (int i = 1; i < Submarine.MainSubs.Length; i++) { + if (Submarine.MainSubs[i] == null) { continue; } availablePositions.RemoveAll(p => Vector2.DistanceSquared(Submarine.MainSubs[i].WorldPosition, p.Position.ToVector2()) < minDistance * minDistance); } } @@ -301,6 +299,13 @@ namespace Barotrauma System.Diagnostics.Debug.Assert(spawnPoint.ParentRuin == chosenPosition.Ruin); spawnPos = spawnPoint.WorldPosition; } + else + { + //no suitable position found, disable the event + spawnPos = null; + Finished(); + return; + } } else if ((chosenPosition.PositionType == Level.PositionType.MainPath || chosenPosition.PositionType == Level.PositionType.SidePath) && offset > 0) @@ -351,6 +356,15 @@ namespace Barotrauma if (spawnPos == null) { + if (maxAmountPerLevel < int.MaxValue) + { + if (Character.CharacterList.Count(c => c.SpeciesName == speciesName) >= maxAmountPerLevel) + { + disallowed = true; + return; + } + } + FindSpawnPosition(affectSubImmediately: true); //the event gets marked as finished if a spawn point is not found if (isFinished) { return; } @@ -361,7 +375,7 @@ namespace Barotrauma if (spawnPending) { //wait until there are no submarines at the spawnpos - if (spawnPosType == Level.PositionType.MainPath) + if (spawnPosType == Level.PositionType.MainPath || spawnPosType == Level.PositionType.SidePath || spawnPosType == Level.PositionType.Abyss) { foreach (Submarine submarine in Submarine.Loaded) { @@ -400,6 +414,19 @@ namespace Barotrauma if (!someoneNearby) { return; } } + + if (spawnPosType == Level.PositionType.Abyss || spawnPosType == Level.PositionType.AbyssCave) + { + foreach (Submarine submarine in Submarine.Loaded) + { + if (submarine.Info.Type != SubmarineType.Player) { continue; } + if (submarine.WorldPosition.Y > 0) + { + return; + } + } + } + spawnPending = false; //+1 because Range returns an integer less than the max value @@ -431,7 +458,16 @@ namespace Barotrauma } } - monsters.Add(Character.Create(speciesName, pos, seed, characterInfo: null, isRemotePlayer: false, hasAi: true, createNetworkEvent: true)); + Character createdCharacter = Character.Create(speciesName, pos, seed, characterInfo: null, isRemotePlayer: false, hasAi: true, createNetworkEvent: true); + if (GameMain.GameSession.IsCurrentLocationRadiated()) + { + AfflictionPrefab radiationPrefab = AfflictionPrefab.RadiationSickness; + Affliction affliction = new Affliction(radiationPrefab, radiationPrefab.MaxStrength); + createdCharacter?.CharacterHealth.ApplyAffliction(null, affliction); + // TODO test multiplayer + createdCharacter?.Kill(CauseOfDeathType.Affliction, affliction, log: false); + } + monsters.Add(createdCharacter); if (monsters.Count == amount) { @@ -440,7 +476,7 @@ namespace Barotrauma //otherwise it'll make the spawned characters act as a swarm SwarmBehavior.CreateSwarm(monsters.Cast()); } - }, Rand.Range(0f, amount / 2)); + }, Rand.Range(0f, amount / 2f)); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs index c638f057d..779136e9c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs @@ -13,7 +13,8 @@ namespace Barotrauma private int prevEntityCount; private int prevPlayerCount, prevBotCount; - private string[] requiredDestinationTypes; + private readonly string[] requiredDestinationTypes; + public readonly bool RequireBeaconStation; public int CurrentActionIndex { get; private set; } public List Actions { get; } = new List(); @@ -21,7 +22,7 @@ namespace Barotrauma public override string ToString() { - return "ScriptedEvent (" + prefab.EventType.ToString() +")"; + return $"ScriptedEvent ({prefab.Identifier})"; } public ScriptedEvent(EventPrefab prefab) : base(prefab) @@ -43,6 +44,7 @@ namespace Barotrauma } requiredDestinationTypes = prefab.ConfigElement.GetAttributeStringArray("requireddestinationtypes", null); + RequireBeaconStation = prefab.ConfigElement.GetAttributeBool("requirebeaconstation", false); } public void AddTarget(string tag, Entity target) @@ -208,9 +210,16 @@ namespace Barotrauma { if (requiredDestinationTypes == null) { return true; } var currLocation = GameMain.GameSession?.Campaign?.Map.CurrentLocation; - if (currLocation == null) { return true; } - var locations = currLocation?.Connections?.Select(c => c.Locations.First(l => l != currLocation)); - return locations.Any(l => requiredDestinationTypes.Any(t => l.Type.Identifier.Equals(t, StringComparison.OrdinalIgnoreCase))); + if (currLocation?.Connections == null) { return true; } + foreach (LocationConnection c in currLocation.Connections) + { + if (RequireBeaconStation && !c.LevelData.HasBeaconStation) { continue; } + if (requiredDestinationTypes.Any(t => c.OtherLocation(currLocation).Type.Identifier.Equals(t, StringComparison.OrdinalIgnoreCase))) + { + return true; + } + } + return false; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index 5453f987a..f2e62e40d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -205,6 +205,7 @@ namespace Barotrauma var item = new Item(itemPrefab, validContainer.Key.Item.Position, validContainer.Key.Item.Submarine) { SpawnedInOutpost = validContainer.Key.Item.SpawnedInOutpost, + AllowStealing = validContainer.Key.Item.AllowStealing, OriginalModuleIndex = validContainer.Key.Item.OriginalModuleIndex, OriginalContainerID = validContainer.Key.Item.ID }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index e8cb4f33a..cb4ec388a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -172,15 +172,15 @@ namespace Barotrauma public void CreatePurchasedItems() { - CreateItems(PurchasedItems); + CreateItems(PurchasedItems, Submarine.MainSub); OnPurchasedItemsChanged?.Invoke(); } - public static void CreateItems(List itemsToSpawn) + public static void CreateItems(List itemsToSpawn, Submarine sub) { if (itemsToSpawn.Count == 0) { return; } - WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, Submarine.MainSub); + WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, sub); if (wp == null) { DebugConsole.ThrowError("The submarine must have a waypoint marked as Cargo for bought items to be placed correctly!"); @@ -188,25 +188,27 @@ namespace Barotrauma } Hull cargoRoom = Hull.FindHull(wp.WorldPosition); - if (cargoRoom == null) { DebugConsole.ThrowError("A waypoint marked as Cargo must be placed inside a room!"); return; } -#if CLIENT - new GUIMessageBox("", TextManager.GetWithVariable("CargoSpawnNotification", "[roomname]", cargoRoom.DisplayName, true), new string[0], type: GUIMessageBox.Type.InGame, iconStyle: "StoreShoppingCrateIcon"); -#else - foreach (Client client in GameMain.Server.ConnectedClients) + if (sub == Submarine.MainSub) { - ChatMessage msg = ChatMessage.Create("", - TextManager.ContainsTag(cargoRoom.RoomName) ? $"CargoSpawnNotification~[roomname]=§{cargoRoom.RoomName}" : $"CargoSpawnNotification~[roomname]={cargoRoom.RoomName}", - ChatMessageType.ServerMessageBoxInGame, null); - msg.IconStyle = "StoreShoppingCrateIcon"; - GameMain.Server.SendDirectChatMessage(msg, client); - } +#if CLIENT + new GUIMessageBox("", TextManager.GetWithVariable("CargoSpawnNotification", "[roomname]", cargoRoom.DisplayName, true), new string[0], type: GUIMessageBox.Type.InGame, iconStyle: "StoreShoppingCrateIcon"); +#else + foreach (Client client in GameMain.Server.ConnectedClients) + { + ChatMessage msg = ChatMessage.Create("", + TextManager.ContainsTag(cargoRoom.RoomName) ? $"CargoSpawnNotification~[roomname]=§{cargoRoom.RoomName}" : $"CargoSpawnNotification~[roomname]={cargoRoom.RoomName}", + ChatMessageType.ServerMessageBoxInGame, null); + msg.IconStyle = "StoreShoppingCrateIcon"; + GameMain.Server.SendDirectChatMessage(msg, client); + } #endif + } List availableContainers = new List(); ItemPrefab containerPrefab = null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index 9defaf5b9..0c9cf2706 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -71,10 +71,13 @@ namespace Barotrauma else if (!isUnignoreOrder) { ActiveOrders.Add(new Pair(order, fadeOutTime)); +#if CLIENT + HintManager.OnActiveOrderAdded(order); +#endif return true; } - bool MatchesTarget(Entity existingTarget, Entity newTarget) + static bool MatchesTarget(Entity existingTarget, Entity newTarget) { if (existingTarget == newTarget) { return true; } if (existingTarget is Hull existingHullTarget && newTarget is Hull newHullTarget) @@ -145,7 +148,13 @@ namespace Barotrauma } #if CLIENT AddCharacterToCrewList(character); - AddCurrentOrderIcon(character, character.CurrentOrder, character.CurrentOrderOption); + if (character.CurrentOrders != null) + { + foreach (var order in character.CurrentOrders) + { + AddCurrentOrderIcon(character, order); + } + } #endif if (character.AIController is HumanAIController humanAI) { @@ -175,7 +184,7 @@ namespace Barotrauma List spawnWaypoints = null; List mainSubWaypoints = WayPoint.SelectCrewSpawnPoints(characterInfos, Submarine.MainSub).ToList(); - if (Level.IsLoadedOutpost) + if (Level.IsLoadedOutpost && Submarine.Loaded.Any(s => s.Info.Type == SubmarineType.Outpost && (s.Info.OutpostGenerationParams?.SpawnCrewInsideOutpost ?? false))) { spawnWaypoints = WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Human && @@ -236,6 +245,21 @@ namespace Barotrauma conversationTimer = IsSinglePlayer ? Rand.Range(5.0f, 10.0f) : Rand.Range(45.0f, 60.0f); } + public void RenameCharacter(CharacterInfo characterInfo, string newName) + { + int identifier = characterInfo.GetIdentifierUsingOriginalName(); + var match = characterInfos.FirstOrDefault(ci => ci.GetIdentifierUsingOriginalName() == identifier); + if (match == null) + { + DebugConsole.ThrowError($"Tried to rename an invalid crew member ({identifier})"); + return; + } + match.Rename(newName); + RenameCharacterProjSpecific(match); + } + + partial void RenameCharacterProjSpecific(CharacterInfo characterInfo); + public void FireCharacter(CharacterInfo characterInfo) { RemoveCharacterInfo(characterInfo); @@ -247,7 +271,8 @@ namespace Barotrauma { if (order.Second.HasValue) { order.Second -= deltaTime; } } - ActiveOrders.RemoveAll(o => o.Second.HasValue && o.Second <= 0.0f); + ActiveOrders.RemoveAll(o => (o.Second.HasValue && o.Second <= 0.0f) || + (o.First.TargetEntity != null && o.First.TargetEntity.Removed)); UpdateConversations(deltaTime); UpdateProjectSpecific(deltaTime); @@ -270,6 +295,7 @@ namespace Barotrauma private void UpdateConversations(float deltaTime) { + if (GameMain.GameSession?.GameMode?.Preset == GameModePreset.TestMode) { return; } if (GameMain.NetworkMember != null && GameMain.NetworkMember.ServerSettings.DisableBotConversations) { return; } conversationTimer -= deltaTime; @@ -287,7 +313,7 @@ namespace Barotrauma { foreach (Character npc in Character.CharacterList) { - if (npc.TeamID != CharacterTeamType.FriendlyNPC || npc.CurrentHull == null || npc.IsIncapacitated) { continue; } + if ((npc.TeamID != CharacterTeamType.FriendlyNPC && npc.TeamID != CharacterTeamType.None) || npc.CurrentHull == null || npc.IsIncapacitated) { continue; } if (npc.AIController is HumanAIController humanAI && (humanAI.ObjectiveManager.IsCurrentObjective() || humanAI.ObjectiveManager.IsCurrentObjective())) { continue; @@ -298,19 +324,35 @@ namespace Barotrauma { List availableSpeakers = new List() { npc, player }; List dialogFlags = new List() { "OutpostNPC", "EnterOutpost" }; - if (GameMain.GameSession?.GameMode is CampaignMode campaignMode && campaignMode.Map?.CurrentLocation?.Reputation != null) + if (GameMain.GameSession?.GameMode is CampaignMode campaignMode) { - float normalizedReputation = MathUtils.InverseLerp( - campaignMode.Map.CurrentLocation.Reputation.MinReputation, - campaignMode.Map.CurrentLocation.Reputation.MaxReputation, - campaignMode.Map.CurrentLocation.Reputation.Value); - if (normalizedReputation < 0.2f) + if (campaignMode.Map?.CurrentLocation?.Type?.Identifier.Equals("abandoned", StringComparison.OrdinalIgnoreCase) ?? false) { - dialogFlags.Add("LowReputation"); + if (npc.TeamID == CharacterTeamType.None) + { + dialogFlags.Remove("OutpostNPC"); + dialogFlags.Add("Bandit"); + } + else if (npc.TeamID == CharacterTeamType.FriendlyNPC) + { + dialogFlags.Remove("OutpostNPC"); + dialogFlags.Add("Hostage"); + } } - else if (normalizedReputation > 0.8f) + else if (campaignMode.Map?.CurrentLocation?.Reputation != null) { - dialogFlags.Add("HighReputation"); + float normalizedReputation = MathUtils.InverseLerp( + campaignMode.Map.CurrentLocation.Reputation.MinReputation, + campaignMode.Map.CurrentLocation.Reputation.MaxReputation, + campaignMode.Map.CurrentLocation.Reputation.Value); + if (normalizedReputation < 0.2f) + { + dialogFlags.Add("LowReputation"); + } + else if (normalizedReputation > 0.8f) + { + dialogFlags.Add("HighReputation"); + } } } pendingConversationLines.AddRange(NPCConversation.CreateRandom(availableSpeakers, dialogFlags)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs index 55977e124..ba5a9912b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs @@ -144,7 +144,7 @@ namespace Barotrauma new XAttribute("value", valueStr), new XAttribute("type", value?.GetType()))); } -#if DEBUG || UNSTABLE +#if DEBUG DebugConsole.Log(element.ToString()); #endif modeElement.Add(element); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs index ed3995e75..9944fd2bc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs @@ -1,10 +1,11 @@ -using System; +using Microsoft.Xna.Framework; +using System; namespace Barotrauma { class Reputation { - public const float HostileThreshold = 0.1f; + public const float HostileThreshold = 0.2f; public const float ReputationLossPerNPCDamage = 0.1f; public const float ReputationLossPerStolenItemPrice = 0.01f; public const float ReputationLossPerWallDamage = 0.1f; @@ -52,5 +53,71 @@ namespace Barotrauma MaxReputation = maxReputation; InitialReputation = initialReputation; } + + public string GetReputationName() + { + return GetReputationName(NormalizedValue); + } + + public static string GetReputationName(float normalizedValue) + { + if (normalizedValue < HostileThreshold) + { + return TextManager.Get("reputationverylow"); + } + else if (normalizedValue < 0.4f) + { + return TextManager.Get("reputationlow"); + } + else if (normalizedValue < 0.6f) + { + return TextManager.Get("reputationneutral"); + } + else if (normalizedValue < 0.8f) + { + return TextManager.Get("reputationhigh"); + } + return TextManager.Get("reputationveryhigh"); + } + +#if CLIENT + public static Color GetReputationColor(float normalizedValue) + { + if (normalizedValue < HostileThreshold) + { + return GUI.Style.ColorReputationVeryLow; + } + else if (normalizedValue < 0.4f) + { + return GUI.Style.ColorReputationLow; + } + else if (normalizedValue < 0.6f) + { + return GUI.Style.ColorReputationNeutral; + } + else if (normalizedValue < 0.8f) + { + return GUI.Style.ColorReputationHigh; + } + return GUI.Style.ColorReputationVeryHigh; + } + public string GetFormattedReputationText(bool addColorTags = false) + { + return GetFormattedReputationText(NormalizedValue, Value, addColorTags); + } + + public static string GetFormattedReputationText(float normalizedValue, float value, bool addColorTags = false) + { + string reputationName = GetReputationName(normalizedValue); + string formattedReputation = TextManager.GetWithVariables("reputationformat", + new string[] { "[reputationname]", "[reputationvalue]" }, + new string[] { reputationName, ((int)Math.Round(value)).ToString() }); + if (addColorTags) + { + formattedReputation = $"‖color:{XMLExtensions.ColorToString(GetReputationColor(normalizedValue))}‖{formattedReputation}‖end‖"; + } + return formattedReputation; + } +#endif } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index 8b32f255d..c5a02bcde 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -5,9 +5,40 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using Barotrauma.Networking; +using Barotrauma.Extensions; namespace Barotrauma { + internal struct CampaignSettings + { + public static CampaignSettings Empty = new CampaignSettings(); + + // Anything that uses this field I wasn't sure if actually needed the proper campaign settings to be passed down + public static CampaignSettings Unsure = Empty; + public bool RadiationEnabled { get; set; } + + public CampaignSettings(IReadMessage inc) + { + RadiationEnabled = inc.ReadBoolean(); + } + + public CampaignSettings(XElement element) + { + RadiationEnabled = element.GetAttributeBool(nameof(RadiationEnabled).ToLower(), true); + } + + public void Serialize(IWriteMessage msg) + { + msg.Write(RadiationEnabled); + } + + public XElement Save() + { + return new XElement(nameof(CampaignSettings), new XAttribute(nameof(RadiationEnabled).ToLower(), RadiationEnabled)); + } + } + abstract partial class CampaignMode : GameMode { const int MaxMoney = int.MaxValue / 2; //about 1 billion @@ -31,6 +62,10 @@ namespace Barotrauma protected XElement petsElement; + public CampaignSettings Settings; + + private List extraMissions = new List(); + public enum TransitionType { None, @@ -74,11 +109,22 @@ namespace Barotrauma get { return map; } } - public override Mission Mission + public override IEnumerable Missions { get { - return Map.CurrentLocation?.SelectedMission; + if (Map.CurrentLocation?.SelectedMission != null) + { + if (Map.CurrentLocation.SelectedMission.Locations[0] == Map.CurrentLocation.SelectedMission.Locations[1] || + Map.CurrentLocation.SelectedMission.Locations.Contains(Map.SelectedLocation)) + { + yield return Map.CurrentLocation.SelectedMission; + } + } + foreach (Mission mission in extraMissions) + { + yield return mission; + } } } @@ -106,28 +152,26 @@ namespace Barotrauma /// The location that's displayed as the "current one" in the map screen. Normally the current outpost or the location at the start of the level, /// but when selecting the next destination at the end of the level at an uninhabited location we use the location at the end /// - public Location CurrentDisplayLocation + public Location GetCurrentDisplayLocation() { - get + if (Level.Loaded?.EndLocation != null && !Level.Loaded.Generating && + Level.Loaded.Type == LevelData.LevelType.LocationConnection && + GetAvailableTransition(out _, out _) == TransitionType.ProgressToNextEmptyLocation) { - if (Level.Loaded?.EndLocation != null && !Level.Loaded.Generating && - Level.Loaded.Type == LevelData.LevelType.LocationConnection && - GetAvailableTransition(out _, out _) == TransitionType.ProgressToNextEmptyLocation) - { - return Level.Loaded.EndLocation; - } - return Level.Loaded?.StartLocation ?? Map.CurrentLocation; + return Level.Loaded.EndLocation; } + return Level.Loaded?.StartLocation ?? Map.CurrentLocation; } public List GetSubsToLeaveBehind(Submarine leavingSub) { //leave subs behind if they're not docked to the leaving sub and not at the same exit - return Submarine.Loaded.FindAll(s => - s != leavingSub && - !leavingSub.DockedTo.Contains(s) && - s.Info.Type == SubmarineType.Player && - (s.AtEndPosition != leavingSub.AtEndPosition || s.AtStartPosition != leavingSub.AtStartPosition)); + return Submarine.Loaded.FindAll(sub => + sub != leavingSub && + !leavingSub.DockedTo.Contains(sub) && + sub.Info.Type == SubmarineType.Player && + sub != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle && + (sub.AtEndExit != leavingSub.AtEndExit || sub.AtStartExit != leavingSub.AtStartExit)); } public override void Start() @@ -135,7 +179,9 @@ namespace Barotrauma base.Start(); dialogLastSpoken.Clear(); characterOutOfBoundsTimer.Clear(); - +#if CLIENT + prevCampaignUIAutoOpenType = TransitionType.None; +#endif if (PurchasedHullRepairs) { foreach (Structure wall in Structure.WallList) @@ -185,6 +231,67 @@ namespace Barotrauma /// public event Action BeforeLevelLoading; + + public override void AddExtraMissions(LevelData levelData) + { + extraMissions.Clear(); + + var currentLocation = Map.CurrentLocation; + if (levelData.Type == LevelData.LevelType.Outpost) + { + //if there's an available mission that takes place in the outpost, select it + var availableMissionsInLocation = currentLocation.AvailableMissions.Where(m => m.Locations[0] == currentLocation && m.Locations[1] == currentLocation); + if (availableMissionsInLocation.Any()) + { + currentLocation.SelectedMission = availableMissionsInLocation.FirstOrDefault(); + } + else + { + currentLocation.SelectedMission = null; + } + } + else + { + //if we had selected a mission that takes place in the outpost, deselect it when leaving the outpost + if (currentLocation.SelectedMission?.Locations[0] == currentLocation && + currentLocation.SelectedMission?.Locations[1] == currentLocation) + { + currentLocation.SelectedMission = null; + } + + if (levelData.HasBeaconStation && !levelData.IsBeaconActive) + { + var beaconMissionPrefabs = MissionPrefab.List.FindAll(m => m.Tags.Any(t => t.Equals("beaconnoreward", StringComparison.OrdinalIgnoreCase))); + if (beaconMissionPrefabs.Any()) + { + Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed)); + var beaconMissionPrefab = beaconMissionPrefabs.GetRandom(rand); + if (!Missions.Any(m => m.Prefab.Type == beaconMissionPrefab.Type)) + { + extraMissions.Add(beaconMissionPrefab.Instantiate(Map.SelectedConnection.Locations)); + } + } + } + if (levelData.HasHuntingGrounds) + { + var huntingGroundsMissionPrefabs = MissionPrefab.List.FindAll(m => m.Tags.Any(t => t.Equals("huntinggroundsnoreward", StringComparison.OrdinalIgnoreCase))); + if (!huntingGroundsMissionPrefabs.Any()) + { + DebugConsole.AddWarning("Could not find a hunting grounds mission for the level. No mission with the tag \"huntinggroundsnoreward\" found."); + } + else + { + Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed)); + var huntingGroundsMissionPrefab = huntingGroundsMissionPrefabs.GetRandom(rand); + if (!Missions.Any(m => m.Prefab.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase)))) + { + extraMissions.Add(huntingGroundsMissionPrefab.Instantiate(Map.SelectedConnection.Locations)); + } + } + } + } + } + public void LoadNewLevel() { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) @@ -215,8 +322,8 @@ namespace Barotrauma "(current location: " + (map.CurrentLocation?.Name ?? "null") + ", " + "selected location: " + (map.SelectedLocation?.Name ?? "null") + ", " + "leaving sub: " + (leavingSub?.Info?.Name ?? "null") + ", " + - "at start: " + (leavingSub?.AtStartPosition.ToString() ?? "null") + ", " + - "at end: " + (leavingSub?.AtEndPosition.ToString() ?? "null") + ")\n" + + "at start: " + (leavingSub?.AtStartExit.ToString() ?? "null") + ", " + + "at end: " + (leavingSub?.AtEndExit.ToString() ?? "null") + ")\n" + Environment.StackTrace.CleanupStackTrace()); return; } @@ -227,8 +334,8 @@ namespace Barotrauma "current location: " + (map.CurrentLocation?.Name ?? "null") + ", " + "selected location: " + (map.SelectedLocation?.Name ?? "null") + ", " + "leaving sub: " + (leavingSub?.Info?.Name ?? "null") + ", " + - "at start: " + (leavingSub?.AtStartPosition.ToString() ?? "null") + ", " + - "at end: " + (leavingSub?.AtEndPosition.ToString() ?? "null") + ")\n" + + "at start: " + (leavingSub?.AtStartExit.ToString() ?? "null") + ", " + + "at end: " + (leavingSub?.AtEndExit.ToString() ?? "null") + ")\n" + Environment.StackTrace.CleanupStackTrace()); return; } @@ -239,8 +346,8 @@ namespace Barotrauma " (current location: " + (map.CurrentLocation?.Name ?? "null") + ", " + "selected location: " + (map.SelectedLocation?.Name ?? "null") + ", " + "leaving sub: " + (leavingSub?.Info?.Name ?? "null") + ", " + - "at start: " + (leavingSub?.AtStartPosition.ToString() ?? "null") + ", " + - "at end: " + (leavingSub?.AtEndPosition.ToString() ?? "null") + ", " + + "at start: " + (leavingSub?.AtStartExit.ToString() ?? "null") + ", " + + "at end: " + (leavingSub?.AtEndExit.ToString() ?? "null") + ", " + "transition type: " + availableTransition + ")"); IsFirstRound = false; @@ -277,7 +384,7 @@ namespace Barotrauma //currently travelling from location to another if (Level.Loaded.Type == LevelData.LevelType.LocationConnection) { - if (leavingSub.AtEndPosition) + if (leavingSub.AtEndExit) { if (Map.EndLocation != null && map.SelectedLocation == Map.EndLocation && @@ -303,15 +410,15 @@ namespace Barotrauma return TransitionType.ProgressToNextEmptyLocation; } } - else if (leavingSub.AtStartPosition) + else if (leavingSub.AtStartExit) { if (map.CurrentLocation.Type.HasOutpost && Level.Loaded.StartOutpost != null) { nextLevel = map.CurrentLocation.LevelData; return TransitionType.ReturnToPreviousLocation; } - else if (map.SelectedLocation != null && map.SelectedLocation != map.CurrentLocation && !map.CurrentLocation.Type.HasOutpost && - (Level.Loaded.LevelData != map.SelectedConnection.LevelData)) + else if (map.SelectedLocation != null && map.SelectedLocation != map.CurrentLocation && !map.CurrentLocation.Type.HasOutpost && + map.SelectedConnection != null && Level.Loaded.LevelData != map.SelectedConnection.LevelData) { nextLevel = map.SelectedConnection.LevelData; return TransitionType.LeaveLocation; @@ -358,12 +465,15 @@ namespace Barotrauma leavingSubAtStart ??= Submarine.MainSub; leavingSubAtEnd ??= Submarine.MainSub; } - int playersInSubAtStart = leavingSubAtStart == null ? 0 : + int playersInSubAtStart = leavingSubAtStart == null || !leavingSubAtStart.AtStartExit ? 0 : leavingPlayers.Count(c => c.Submarine == leavingSubAtStart || leavingSubAtStart.DockedTo.Contains(c.Submarine) || (Level.Loaded.StartOutpost != null && c.Submarine == Level.Loaded.StartOutpost)); - int playersInSubAtEnd = leavingSubAtEnd == null ? 0 : + int playersInSubAtEnd = leavingSubAtEnd == null || !leavingSubAtEnd.AtEndExit ? 0 : leavingPlayers.Count(c => c.Submarine == leavingSubAtEnd || leavingSubAtEnd.DockedTo.Contains(c.Submarine) || (Level.Loaded.EndOutpost != null && c.Submarine == Level.Loaded.EndOutpost)); - if (playersInSubAtStart == 0 && playersInSubAtEnd == 0) { return null; } + if (playersInSubAtStart == 0 && playersInSubAtEnd == 0) + { + return null; + } return playersInSubAtStart > playersInSubAtEnd ? leavingSubAtStart : leavingSubAtEnd; @@ -371,7 +481,7 @@ namespace Barotrauma { if (Level.Loaded.StartOutpost == null) { - Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartPosition, ignoreOutposts: true); + Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID); return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub; } else @@ -380,13 +490,14 @@ namespace Barotrauma if (Level.Loaded.StartOutpost.DockedTo.Any()) { var dockedSub = Level.Loaded.StartOutpost.DockedTo.FirstOrDefault(); + if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle) { return null; } return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub; } //nothing docked, check if there's a sub close enough to the outpost and someone inside the outpost if (Level.Loaded.Type == LevelData.LevelType.LocationConnection && !leavingPlayers.Any(s => s.Submarine == Level.Loaded.StartOutpost)) { return null; } - Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartOutpost.WorldPosition, ignoreOutposts: true); - if (closestSub == null || !closestSub.AtStartPosition) { return null; } + Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID); + if (closestSub == null || !closestSub.AtStartExit) { return null; } return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub; } } @@ -398,7 +509,7 @@ namespace Barotrauma if (Level.Loaded.EndOutpost == null) { - Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndPosition, ignoreOutposts: true); + Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID); return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub; } else @@ -407,13 +518,14 @@ namespace Barotrauma if (Level.Loaded.EndOutpost.DockedTo.Any()) { var dockedSub = Level.Loaded.EndOutpost.DockedTo.FirstOrDefault(); + if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle) { return null; } return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub; } //nothing docked, check if there's a sub close enough to the outpost and someone inside the outpost if (Level.Loaded.Type == LevelData.LevelType.LocationConnection && !leavingPlayers.Any(s => s.Submarine == Level.Loaded.EndOutpost)) { return null; } - Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndOutpost.WorldPosition, ignoreOutposts: true); - if (closestSub == null || !closestSub.AtEndPosition) { return null; } + Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID); + if (closestSub == null || !closestSub.AtEndExit) { return null; } return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub; } } @@ -425,16 +537,19 @@ namespace Barotrauma foreach (Item item in Item.ItemList) { if (!item.SpawnedInOutpost || item.OriginalModuleIndex < 0) { continue; } - if ((!(item.GetRootInventoryOwner()?.Submarine?.Info?.IsOutpost ?? false)) || item.Submarine == null || !item.Submarine.Info.IsOutpost) + var owner = item.GetRootInventoryOwner(); + if ((!(owner?.Submarine?.Info?.IsOutpost ?? false)) || (owner is Character character && character.TeamID == CharacterTeamType.Team1) || item.Submarine == null || !item.Submarine.Info.IsOutpost) { takenItems.Add(item); } } - map.CurrentLocation.RegisterTakenItems(takenItems); - - map.CurrentLocation.AddToStock(CargoManager.SoldItems); - CargoManager.ClearSoldItemsProjSpecific(); - map.CurrentLocation.RemoveFromStock(CargoManager.PurchasedItems); + if (map != null && CargoManager != null) + { + map.CurrentLocation.RegisterTakenItems(takenItems); + map.CurrentLocation.AddToStock(CargoManager.SoldItems); + CargoManager.ClearSoldItemsProjSpecific(); + map.CurrentLocation.RemoveFromStock(CargoManager.PurchasedItems); + } if (GameMain.NetworkMember == null) { CargoManager.ClearItemsInBuyCrate(); @@ -444,11 +559,11 @@ namespace Barotrauma { if (GameMain.NetworkMember.IsServer) { - CargoManager.ClearItemsInBuyCrate(); + CargoManager?.ClearItemsInBuyCrate(); } else if (GameMain.NetworkMember.IsClient) { - CargoManager.ClearItemsInSellCrate(); + CargoManager?.ClearItemsInSellCrate(); } } @@ -480,7 +595,7 @@ namespace Barotrauma { CrewManager.RemoveCharacterInfo(ci); } - ci?.ResetCurrentOrder(); + ci?.ClearCurrentOrders(); } foreach (DockingPort port in DockingPort.List) @@ -502,14 +617,30 @@ namespace Barotrauma { connection.Difficulty = MathHelper.Lerp(connection.Difficulty, 100.0f, 0.25f); connection.LevelData.Difficulty = connection.Difficulty; + connection.LevelData.IsBeaconActive = false; + connection.LevelData.HasHuntingGrounds = connection.LevelData.OriginallyHadHuntingGrounds; } foreach (Location location in Map.Locations) { + if (location.Type != location.OriginalType) + { + location.ChangeType(location.OriginalType); + location.PendingLocationTypeChange = null; + } location.CreateStore(force: true); location.ClearMissions(); + location.Discovered = false; } Map.SetLocation(Map.Locations.IndexOf(Map.StartLocation)); Map.SelectLocation(-1); + if (Map.Radiation != null) + { + Map.Radiation.Amount = Map.Radiation.Params.StartingRadiation; + } + foreach (Location location in Map.Locations) + { + location.TurnsInRadiation = 0; + } EndCampaignProjSpecific(); if (CampaignMetadata != null) @@ -523,14 +654,12 @@ namespace Barotrauma public bool TryHireCharacter(Location location, CharacterInfo characterInfo) { + if (characterInfo == null) { return false; } if (Money < characterInfo.Salary) { return false; } - characterInfo.IsNewHire = true; - location.RemoveHireableCharacter(characterInfo); CrewManager.AddCharacterInfo(characterInfo); Money -= characterInfo.Salary; - return true; } @@ -552,18 +681,14 @@ namespace Barotrauma HumanAIController humanAI = npc.AIController as HumanAIController; if (humanAI == null) { yield return CoroutineStatus.Failure; } - OrderInfo? prevSpeakerOrder = null; - if (humanAI.CurrentOrder != null) - { - prevSpeakerOrder = new OrderInfo(humanAI.CurrentOrder, humanAI.CurrentOrderOption); - } var waitOrder = Order.PrefabList.Find(o => o.Identifier.Equals("wait", StringComparison.OrdinalIgnoreCase)); - humanAI.SetOrder(waitOrder, option: string.Empty, orderGiver: null, speak: false); + humanAI.SetForcedOrder(waitOrder, string.Empty, null); + var waitObjective = humanAI.ObjectiveManager.ForcedOrder; humanAI.FaceTarget(interactor); while (!npc.Removed && !interactor.Removed && Vector2.DistanceSquared(npc.WorldPosition, interactor.WorldPosition) < 300.0f * 300.0f && - humanAI.CurrentOrder == waitOrder && + humanAI.ObjectiveManager.ForcedOrder == waitObjective && humanAI.AllowCampaignInteraction() && !interactor.IsIncapacitated) { @@ -574,17 +699,7 @@ namespace Barotrauma ShowCampaignUI = false; #endif - if (humanAI.CurrentOrder == waitOrder) - { - if (prevSpeakerOrder != null) - { - humanAI.SetOrder(prevSpeakerOrder.Value.Order, prevSpeakerOrder.Value.OrderOption, orderGiver: null, speak: false); - } - else - { - humanAI.SetOrder(null, string.Empty, orderGiver: null, speak: false); - } - } + humanAI.ClearForcedOrder(); yield return CoroutineStatus.Success; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CoOpMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CoOpMode.cs index 8df39d58e..62a3021ee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CoOpMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CoOpMode.cs @@ -1,10 +1,11 @@ using System; +using System.Collections.Generic; namespace Barotrauma { class CoOpMode : MissionMode { - public CoOpMode(GameModePreset preset, MissionPrefab missionPrefab) : base(preset, ValidateMissionPrefab(missionPrefab, MissionPrefab.CoOpMissionClasses)) { } + public CoOpMode(GameModePreset preset, IEnumerable missionPrefabs) : base(preset, ValidateMissionPrefabs(missionPrefabs, MissionPrefab.CoOpMissionClasses)) { } public CoOpMode(GameModePreset preset, MissionType missionType, string seed) : base(preset, ValidateMissionType(missionType, MissionPrefab.CoOpMissionClasses), seed) { } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameMode.cs index 6faaf1b13..1061e9754 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameMode.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; namespace Barotrauma { @@ -16,9 +17,9 @@ namespace Barotrauma get { return GameMain.GameSession?.CrewManager; } } - public virtual Mission Mission + public virtual IEnumerable Missions { - get { return null; } + get { return Enumerable.Empty(); } } public bool IsSinglePlayer @@ -54,6 +55,8 @@ namespace Barotrauma } public virtual void ShowStartMessage() { } + + public virtual void AddExtraMissions(LevelData levelData) { } public virtual void AddToGUIUpdateList() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs index 39193b8a2..8caf39c4f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs @@ -5,37 +5,43 @@ namespace Barotrauma { abstract partial class MissionMode : GameMode { - private readonly Mission mission; + private readonly List missions = new List(); - public override Mission Mission + public override IEnumerable Missions { get { - return mission; + return missions; } } - public MissionMode(GameModePreset preset, MissionPrefab missionPrefab) + public MissionMode(GameModePreset preset, IEnumerable missionPrefabs) : base(preset) { Location[] locations = { GameMain.GameSession.StartLocation, GameMain.GameSession.EndLocation }; - mission = missionPrefab.Instantiate(locations); + foreach (MissionPrefab missionPrefab in missionPrefabs) + { + missions.Add(missionPrefab.Instantiate(locations)); + } } public MissionMode(GameModePreset preset, MissionType missionType, string seed) : base(preset) { Location[] locations = { GameMain.GameSession.StartLocation, GameMain.GameSession.EndLocation }; - mission = Mission.LoadRandom(locations, seed, false, missionType); + missions.Add(Mission.LoadRandom(locations, seed, false, missionType)); } - protected static MissionPrefab ValidateMissionPrefab(MissionPrefab missionPrefab, Dictionary missionClasses) + protected static IEnumerable ValidateMissionPrefabs(IEnumerable missionPrefabs, Dictionary missionClasses) { - if (ValidateMissionType(missionPrefab.Type, missionClasses) != missionPrefab.Type) + foreach (MissionPrefab missionPrefab in missionPrefabs) { - throw new InvalidOperationException("Cannot start gamemode with mission type " + missionPrefab.Type); + if (ValidateMissionType(missionPrefab.Type, missionClasses) != missionPrefab.Type) + { + throw new InvalidOperationException("Cannot start gamemode with mission type " + missionPrefab.Type); + } } - return missionPrefab; + return missionPrefabs; } protected static MissionType ValidateMissionType(MissionType missionType, Dictionary missionClasses) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index 608668d29..6bbfa5cfb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -59,13 +59,14 @@ namespace Barotrauma InitCampaignData(); } - public static MultiPlayerCampaign StartNew(string mapSeed, SubmarineInfo selectedSub) + public static MultiPlayerCampaign StartNew(string mapSeed, SubmarineInfo selectedSub, CampaignSettings settings) { MultiPlayerCampaign campaign = new MultiPlayerCampaign(); //only the server generates the map, the clients load it from a save file if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { - campaign.map = new Map(campaign, mapSeed); + campaign.map = new Map(campaign, mapSeed, settings); + campaign.Settings = settings; } campaign.InitProjSpecific(); return campaign; @@ -128,11 +129,14 @@ namespace Barotrauma { switch (subElement.Name.ToString().ToLowerInvariant()) { + case "campaignsettings": + Settings = new CampaignSettings(subElement); + break; case "map": if (map == null) { //map not created yet, loading this campaign for the first time - map = Map.Load(this, subElement); + map = Map.Load(this, subElement, Settings); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/PvPMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/PvPMode.cs index 9a7af996c..1ac387dee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/PvPMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/PvPMode.cs @@ -1,13 +1,11 @@ using Barotrauma.Networking; -using System; using System.Collections.Generic; -using System.Linq; namespace Barotrauma { class PvPMode : MissionMode { - public PvPMode(GameModePreset preset, MissionPrefab missionPrefab) : base(preset, ValidateMissionPrefab(missionPrefab, MissionPrefab.PvPMissionClasses)) { } + public PvPMode(GameModePreset preset, IEnumerable missionPrefabs) : base(preset, ValidateMissionPrefabs(missionPrefabs, MissionPrefab.PvPMissionClasses)) { } public PvPMode(GameModePreset preset, MissionType missionType, string seed) : base(preset, ValidateMissionType(missionType, MissionPrefab.PvPMissionClasses), seed) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index e24154981..3c8b2bd10 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -22,7 +22,8 @@ namespace Barotrauma public double RoundStartTime; - public Mission Mission { get; private set; } + private readonly List missions = new List(); + public IEnumerable Missions { get { return missions; } } public CharacterTeamType? WinningTeam; @@ -102,29 +103,28 @@ namespace Barotrauma /// /// Start a new GameSession. Will be saved to the specified save path (if playing a game mode that can be saved). /// - public GameSession(SubmarineInfo submarineInfo, string savePath, GameModePreset gameModePreset, string seed = null, MissionType missionType = MissionType.None) + public GameSession(SubmarineInfo submarineInfo, string savePath, GameModePreset gameModePreset, CampaignSettings settings, string seed = null, MissionType missionType = MissionType.None) : this(submarineInfo) { this.SavePath = savePath; CrewManager = new CrewManager(gameModePreset != null && gameModePreset.IsSinglePlayer); - GameMode = InstantiateGameMode(gameModePreset, seed, submarineInfo, missionType: missionType); + GameMode = InstantiateGameMode(gameModePreset, seed, submarineInfo, settings, missionType: missionType); } /// /// Start a new GameSession with a specific pre-selected mission. /// - public GameSession(SubmarineInfo submarineInfo, GameModePreset gameModePreset, string seed = null, MissionPrefab missionPrefab = null) + public GameSession(SubmarineInfo submarineInfo, GameModePreset gameModePreset, string seed = null, IEnumerable missionPrefabs = null) : this(submarineInfo) { CrewManager = new CrewManager(gameModePreset != null && gameModePreset.IsSinglePlayer); - GameMode = InstantiateGameMode(gameModePreset, seed, submarineInfo, missionPrefab: missionPrefab); + GameMode = InstantiateGameMode(gameModePreset, seed, submarineInfo, CampaignSettings.Empty, missionPrefabs: missionPrefabs); } /// /// Load a game session from the specified XML document. The session will be saved to the specified path. /// - public GameSession(SubmarineInfo submarineInfo, List ownedSubmarines, XDocument doc, string saveFile) - : this(submarineInfo, ownedSubmarines) + public GameSession(SubmarineInfo submarineInfo, List ownedSubmarines, XDocument doc, string saveFile) : this(submarineInfo, ownedSubmarines) { this.SavePath = saveFile; GameMain.GameSession = this; @@ -158,23 +158,23 @@ namespace Barotrauma } } - private GameMode InstantiateGameMode(GameModePreset gameModePreset, string seed, SubmarineInfo selectedSub, MissionPrefab missionPrefab = null, MissionType missionType = MissionType.None) + private GameMode InstantiateGameMode(GameModePreset gameModePreset, string seed, SubmarineInfo selectedSub, CampaignSettings settings, IEnumerable missionPrefabs = null, MissionType missionType = MissionType.None) { if (gameModePreset.GameModeType == typeof(CoOpMode)) { - return missionPrefab != null ? - new CoOpMode(gameModePreset, missionPrefab) : + return missionPrefabs != null ? + new CoOpMode(gameModePreset, missionPrefabs) : new CoOpMode(gameModePreset, missionType, seed ?? ToolBox.RandomSeed(8)); } else if (gameModePreset.GameModeType == typeof(PvPMode)) { - return missionPrefab != null ? - new PvPMode(gameModePreset, missionPrefab) : + return missionPrefabs != null ? + new PvPMode(gameModePreset, missionPrefabs) : new PvPMode(gameModePreset, missionType, seed ?? ToolBox.RandomSeed(8)); } else if (gameModePreset.GameModeType == typeof(MultiPlayerCampaign)) { - var campaign = MultiPlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub); + var campaign = MultiPlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub, settings); if (campaign != null && selectedSub != null) { campaign.Money = Math.Max(MultiPlayerCampaign.MinimumInitialMoney, campaign.Money - selectedSub.Price); @@ -184,7 +184,7 @@ namespace Barotrauma #if CLIENT else if (gameModePreset.GameModeType == typeof(SinglePlayerCampaign)) { - var campaign = SinglePlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub); + var campaign = SinglePlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub, settings); if (campaign != null && selectedSub != null) { campaign.Money = Math.Max(SinglePlayerCampaign.MinimumInitialMoney, campaign.Money - selectedSub.Price); @@ -210,7 +210,7 @@ namespace Barotrauma } } - private void CreateDummyLocations() + private void CreateDummyLocations(LocationType? forceLocationType = null) { dummyLocations = new Location[2]; @@ -227,7 +227,7 @@ namespace Barotrauma MTRandom rand = new MTRandom(ToolBox.StringToInt(seed)); for (int i = 0; i < 2; i++) { - dummyLocations[i] = Location.CreateRandom(new Vector2((float)rand.NextDouble() * 10000.0f, (float)rand.NextDouble() * 10000.0f), null, rand, requireOutpost: true); + dummyLocations[i] = Location.CreateRandom(new Vector2((float)rand.NextDouble() * 10000.0f, (float)rand.NextDouble() * 10000.0f), null, rand, requireOutpost: true, forceLocationType: forceLocationType); } } @@ -282,9 +282,38 @@ namespace Barotrauma (OwnedSubmarines != null && OwnedSubmarines.Any(os => os.Name == query.Name)); } + public bool IsCurrentLocationRadiated() + { + if (Map?.CurrentLocation == null || Campaign == null) { return false; } + + bool isRadiated = Map.CurrentLocation.IsRadiated(); + + if (Level.Loaded?.EndLocation is { } endLocation) + { + isRadiated |= endLocation.IsRadiated(); + } + + return isRadiated; + } + public void StartRound(string levelSeed, float? difficulty = null) { - StartRound(LevelData.CreateRandom(levelSeed, difficulty)); + LevelData randomLevel = null; + foreach (Mission mission in Missions.Union(GameMode.Missions)) + { + MissionPrefab missionPrefab = mission.Prefab; + if (missionPrefab != null && + missionPrefab.AllowedLocationTypes.Any() && + !missionPrefab.AllowedConnectionTypes.Any()) + { + LocationType locationType = LocationType.List.FirstOrDefault(lt => missionPrefab.AllowedLocationTypes.Any(m => m.Equals(lt.Identifier, StringComparison.OrdinalIgnoreCase))); + CreateDummyLocations(locationType); + randomLevel = LevelData.CreateRandom(levelSeed, difficulty, requireOutpost: true); + break; + } + } + randomLevel ??= LevelData.CreateRandom(levelSeed, difficulty); + StartRound(randomLevel); } public void StartRound(LevelData levelData, bool mirrorLevel = false, SubmarineInfo startOutpost = null, SubmarineInfo endOutpost = null) @@ -307,12 +336,6 @@ namespace Barotrauma LevelData = levelData; - if (GameMode is CampaignMode campaignMode && GameMode.Mission != null && - LevelData != null && LevelData.Type == LevelData.LevelType.Outpost) - { - campaignMode.Map.CurrentLocation.SelectedMission = null; - } - Submarine.Unload(); Submarine = Submarine.MainSub = new Submarine(SubmarineInfo); foreach (Submarine sub in Submarine.GetConnectedSubs()) @@ -332,6 +355,19 @@ namespace Barotrauma Submarine.MainSubs[1] = new Submarine(SubmarineInfo, true); } + if (GameMain.NetworkMember?.ServerSettings?.LockAllDefaultWires ?? false) + { + foreach (Item item in Item.ItemList) + { + if (item.Submarine == Submarine.MainSubs[0] || + (Submarine.MainSubs[1] != null && item.Submarine == Submarine.MainSubs[1])) + { + Wire wire = item.GetComponent(); + if (wire != null && !wire.NoAutoLock && wire.Connections.Any(c => c != null)) { wire.Locked = true; } + } + } + } + Level level = null; if (levelData != null) { @@ -340,11 +376,6 @@ namespace Barotrauma InitializeLevel(level); - GameAnalyticsManager.AddDesignEvent("Submarine:" + Submarine.Info.Name); - GameAnalyticsManager.AddDesignEvent("Level", ToolBox.StringToInt(levelData?.Seed ?? "[NO_LEVEL]")); - GameAnalyticsManager.AddProgressionEvent(GameAnalyticsSDK.Net.EGAProgressionStatus.Start, - GameMode.Preset.Identifier, (Mission == null ? "None" : Mission.GetType().ToString())); - #if CLIENT if (GameMode is CampaignMode) { SteamAchievementManager.OnBiomeDiscovered(levelData.Biome); } @@ -354,7 +385,7 @@ namespace Barotrauma existingRoundSummary.ContinueButton.Visible = true; } - RoundSummary = new RoundSummary(Submarine.Info, GameMode, Mission, StartLocation, EndLocation); + RoundSummary = new RoundSummary(Submarine.Info, GameMode, Missions, StartLocation, EndLocation); if (!(GameMode is TutorialMode) && !(GameMode is TestGameMode)) { @@ -363,7 +394,16 @@ namespace Barotrauma { GUI.AddMessage(levelData.Biome.DisplayName, Color.Lerp(Color.CadetBlue, Color.DarkRed, levelData.Difficulty / 100.0f), 5.0f, playSound: false); GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Destination"), EndLocation.Name), Color.CadetBlue, playSound: false); - GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Mission"), (Mission == null ? TextManager.Get("None") : Mission.Name)), Color.CadetBlue, playSound: false); + if (missions.Count > 1) + { + string joinedMissionNames = string.Join(", ", missions.Select(m => m.Name)); + GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Mission"), joinedMissionNames), Color.CadetBlue, playSound: false); + } + else + { + var mission = missions.FirstOrDefault(); + GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Mission"), mission?.Name ?? TextManager.Get("None")), Color.CadetBlue, playSound: false); + } } else { @@ -372,6 +412,8 @@ namespace Barotrauma } GUI.PreventPauseMenuToggle = false; + + HintManager.OnRoundStarted(); #endif } @@ -383,6 +425,7 @@ namespace Barotrauma #if CLIENT GameMain.LightManager.LosEnabled = GameMain.Client == null || GameMain.Client.CharacterInfo != null; + if (GameMain.LightManager.LosEnabled) { GameMain.LightManager.LosAlpha = 1f; } if (GameMain.Client == null) GameMain.LightManager.LosMode = GameMain.Config.LosMode; #endif LevelData = level?.LevelData; @@ -400,16 +443,18 @@ namespace Barotrauma Entity.Spawner = new EntitySpawner(); - if (GameMode.Mission != null) { Mission = GameMode.Mission; } - if (GameMode != null) { GameMode.Start(); } - if (GameMode.Mission != null) + missions.Clear(); + GameMode.AddExtraMissions(LevelData); + missions.AddRange(GameMode.Missions); + GameMode.Start(); + foreach (Mission mission in missions) { int prevEntityCount = Entity.GetEntities().Count(); - Mission.Start(Level.Loaded); + mission.Start(Level.Loaded); if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Entity.GetEntities().Count() != prevEntityCount) { DebugConsole.ThrowError( - "Entity count has changed after starting a mission as a client. " + + $"Entity count has changed after starting a mission ({mission.Prefab.Identifier}) as a client. " + "The clients should not instantiate entities themselves when starting the mission," + " but instead the server should inform the client of the spawned entities using Mission.ServerWriteInitial."); } @@ -433,13 +478,6 @@ namespace Barotrauma } if (GameMode is MultiPlayerCampaign mpCampaign) { - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) - { - mpCampaign.CargoManager.CreatePurchasedItems(); -#if SERVER - mpCampaign.SendCrewState(false, null); -#endif - } mpCampaign.UpgradeManager.ApplyUpgrades(); mpCampaign.UpgradeManager.SanityCheckUpgrades(Submarine); } @@ -514,7 +552,7 @@ namespace Barotrauma { Submarine.SetPosition(spawnPos); myPort.Dock(outPostPort); - myPort.Lock(true); + myPort.Lock(isNetworkMessage: true, applyEffects: false); } else { @@ -531,7 +569,7 @@ namespace Barotrauma } else { - Submarine.SetPosition(Submarine.FindSpawnPos(level.StartPosition, verticalMoveDir: 1)); + Submarine.SetPosition(Submarine.FindSpawnPos(level.StartPosition)); Submarine.NeutralizeBallast(); Submarine.EnableMaintainPosition(); } @@ -553,21 +591,33 @@ namespace Barotrauma { EventManager?.Update(deltaTime); GameMode?.Update(deltaTime); - Mission?.Update(deltaTime); - + //backwards for loop because the missions may get completed and removed from the list in Update() + for (int i = missions.Count - 1; i >= 0; i--) + { + missions[i].Update(deltaTime); + } UpdateProjSpecific(deltaTime); } + public Mission GetMission(int index) + { + if (index < 0 || index >= missions.Count) { return null; } + return missions[index]; + } + + public int GetMissionIndex(Mission mission) + { + return missions.IndexOf(mission); + } + partial void UpdateProjSpecific(float deltaTime); public void EndRound(string endMessage, List traitorResults = null, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None) { - if (Mission != null) { Mission.End(); } - GameAnalyticsManager.AddProgressionEvent( - (Mission == null || Mission.Completed) ? GameAnalyticsSDK.Net.EGAProgressionStatus.Complete : GameAnalyticsSDK.Net.EGAProgressionStatus.Fail, - GameMode.Preset.Identifier, - Mission == null ? "None" : Mission.GetType().ToString()); - + foreach (Mission mission in missions) + { + mission.End(); + } #if CLIENT if (GUI.PauseMenuOpen) { @@ -593,8 +643,12 @@ namespace Barotrauma GameMode?.End(transitionType); EventManager?.EndRound(); StatusEffect.StopAll(); - Mission = null; + missions.Clear(); IsRunning = false; + +#if CLIENT + HintManager.OnRoundEnded(); +#endif } public void KillCharacter(Character character) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/HireManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/HireManager.cs index 1d6f6529d..59b6e4bc5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/HireManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/HireManager.cs @@ -1,5 +1,5 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; +using System.Linq; namespace Barotrauma { @@ -39,5 +39,12 @@ namespace Barotrauma AvailableCharacters.ForEach(c => c.Remove()); AvailableCharacters.Clear(); } + + public void RenameCharacter(CharacterInfo characterInfo, string newName) + { + if (characterInfo == null || string.IsNullOrEmpty(newName)) { return; } + AvailableCharacters.FirstOrDefault(ci => ci == characterInfo)?.Rename(newName); + PendingHires.FirstOrDefault(ci => ci == characterInfo)?.Rename(newName); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs index 182a5ef40..fae47582a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs @@ -104,7 +104,8 @@ namespace Barotrauma /// /// /// - public void PurchaseUpgrade(UpgradePrefab prefab, UpgradeCategory category) + /// + public void PurchaseUpgrade(UpgradePrefab prefab, UpgradeCategory category, bool force = false) { if (!CanUpgradeSub()) { @@ -136,6 +137,11 @@ namespace Barotrauma }); } + if (force) + { + price = 0; + } + if (Campaign.Money > price) { if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) @@ -154,7 +160,7 @@ namespace Barotrauma PurchasedUpgrade? upgrade = FindMatchingUpgrade(prefab, category); #if CLIENT - DebugLog($"CLIENT: Purchased level {GetUpgradeLevel(prefab, category) + 1} {category.Name}.{prefab.Name} for ${price}", GUI.Style.Orange); + DebugLog($"CLIENT: Purchased level {GetUpgradeLevel(prefab, category) + 1} {category.Name}.{prefab.Name} for {price}", GUI.Style.Orange); #endif if (upgrade == null) @@ -689,7 +695,7 @@ namespace Barotrauma public static void DebugLog(string msg, Color? color = null) { -#if UNSTABLE || DEBUG +#if DEBUG DebugConsole.NewMessage(msg, color ?? Color.GreenYellow); #else DebugConsole.Log(msg); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs index 99ce67c26..4d69e9155 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs @@ -171,7 +171,7 @@ namespace Barotrauma /// /// How many corpses there can be in a sub before they start to get despawned /// - public int CorpsesPerSubDespawnThreshold { get; set; } = 5; + public int CorpsesPerSubDespawnThreshold { get; set; } = 10; private string overrideSaveFolder, overrideMultiplayerSaveFolder; @@ -301,10 +301,14 @@ namespace Barotrauma public volatile bool WaitingForAutoUpdate; + public bool DisableInGameHints { get; set; } + #if DEBUG public bool AutomaticQuickStartEnabled { get; set; } public bool AutomaticCampaignLoadEnabled { get; set; } public bool TextManagerDebugModeEnabled { get; set; } + + public bool ModBreakerMode { get; set; } #endif private System.IO.FileSystemWatcher modsFolderWatcher; @@ -735,6 +739,10 @@ namespace Barotrauma private bool textScaleDirty; public List CompletedTutorialNames { get; private set; } + /// + /// Identifiers of hints the player has chosen not to see again + /// + public HashSet IgnoredHints { get; private set; } = new HashSet(); public HashSet EncounteredCreatures { get; private set; } = new HashSet(); public HashSet KilledCreatures { get; private set; } = new HashSet(); @@ -1151,6 +1159,12 @@ namespace Barotrauma CompletedTutorialNames.Add(element.GetAttributeString("name", "")); } } + + if (doc.Root.Element("ignoredhints") is XElement ignoredHintsElement) + { + IgnoredHints = new HashSet(ignoredHintsElement.GetAttributeStringArray("identifiers", new string[0], convertToLowerInvariant: true)); + } + XElement encounters = doc.Root.Element("encountered"); if (encounters != null) { @@ -1172,7 +1186,7 @@ namespace Barotrauma #endregion #region Save PlayerConfig - public void SaveNewPlayerConfig() + public bool SaveNewPlayerConfig() { XDocument doc = new XDocument(); UnsavedSettings = false; @@ -1211,11 +1225,13 @@ namespace Barotrauma new XAttribute("tutorialskipwarning", ShowTutorialSkipWarning), new XAttribute("corpsedespawndelay", CorpseDespawnDelay), new XAttribute("corpsespersubdespawnthreshold", CorpsesPerSubDespawnThreshold), - new XAttribute("usedualmodesockets", UseDualModeSockets) + new XAttribute("usedualmodesockets", UseDualModeSockets), + new XAttribute("disableingamehints", DisableInGameHints) #if DEBUG , new XAttribute("automaticquickstartenabled", AutomaticQuickStartEnabled) , new XAttribute("automaticcampaignloadenabled", AutomaticCampaignLoadEnabled) , new XAttribute("textmanagerdebugmodeenabled", TextManagerDebugModeEnabled) + , new XAttribute("modbreakermode", ModBreakerMode) #endif ); @@ -1403,6 +1419,8 @@ namespace Barotrauma } doc.Root.Add(tutorialElement); + doc.Root.Add(new XElement("ignoredhints", new XAttribute("identifiers", string.Join(",", IgnoredHints).Trim().ToLowerInvariant()))); + doc.Root.Add(new XElement("encountered", new XAttribute("creatures", string.Join(",", EncounteredCreatures).Trim().ToLowerInvariant()))); doc.Root.Add(new XElement("killed", new XAttribute("creatures", string.Join(",", KilledCreatures).Trim().ToLowerInvariant()))); @@ -1426,7 +1444,10 @@ namespace Barotrauma 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.CleanupStackTrace()); + return false; } + + return true; } #endregion @@ -1460,10 +1481,12 @@ namespace Barotrauma EditorDisclaimerShown = doc.Root.GetAttributeBool("editordisclaimershown", EditorDisclaimerShown); ShowTutorialSkipWarning = doc.Root.GetAttributeBool("tutorialskipwarning", true); UseDualModeSockets = doc.Root.GetAttributeBool("usedualmodesockets", true); + DisableInGameHints = doc.Root.GetAttributeBool("disableingamehints", DisableInGameHints); #if DEBUG AutomaticQuickStartEnabled = doc.Root.GetAttributeBool("automaticquickstartenabled", AutomaticQuickStartEnabled); AutomaticCampaignLoadEnabled = doc.Root.GetAttributeBool("automaticcampaignloadenabled", AutomaticCampaignLoadEnabled); TextManagerDebugModeEnabled = doc.Root.GetAttributeBool("textmanagerdebugmodeenabled", TextManagerDebugModeEnabled); + ModBreakerMode = doc.Root.GetAttributeBool("modbreakermode", ModBreakerMode); #endif XElement gameplayElement = doc.Root.Element("gameplay"); jobPreferences = new List>(); @@ -1579,6 +1602,32 @@ namespace Barotrauma CurrentCorePackage = null; enabledRegularPackages.Clear(); +#if DEBUG && CLIENT + if (ModBreakerMode) + { + CurrentCorePackage = ContentPackage.CorePackages.GetRandom(); + foreach (var regularPackage in ContentPackage.RegularPackages) + { + if (Rand.Range(0.0, 1.0) <= 0.5) + { + enabledRegularPackages.Add(regularPackage); + } + } + ContentPackage.SortContentPackages(p => + { + return Rand.Int(int.MaxValue); + }, config: this); + + if (CurrentCorePackage == null) + { + CurrentCorePackage = ContentPackage.CorePackages.First(); + } + + TextManager.LoadTextPacks(AllEnabledPackages); + return; + } +#endif + var contentPackagesElement = doc.Root.Element("contentpackages"); if (contentPackagesElement != null) { @@ -1725,6 +1774,7 @@ namespace Barotrauma AutoUpdateWorkshopItems = true; TextScale = 1; textScaleDirty = false; + DisableInGameHints = false; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index 3f055adb5..7e2666efa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -123,7 +123,16 @@ namespace Barotrauma public override bool CanBePut(Item item, int i) { - return base.CanBePut(item, i) && item.AllowedSlots.Contains(SlotTypes[i]); + return + base.CanBePut(item, i) && item.AllowedSlots.Any(s => s.HasFlag(SlotTypes[i])) && + (SlotTypes[i] == InvSlotType.Any || slots[i].ItemCount < 1); + } + + public override bool CanBePut(ItemPrefab itemPrefab, int i) + { + return + base.CanBePut(itemPrefab, i) && + (SlotTypes[i] == InvSlotType.Any || slots[i].ItemCount < 1); } public bool CanBeAutoMovedToCorrectSlots(Item item) @@ -141,6 +150,44 @@ namespace Barotrauma return false; } + public override void RemoveItem(Item item) + { + RemoveItem(item, tryEquipFromSameStack: false); + } + + public void RemoveItem(Item item, bool tryEquipFromSameStack) + { + if (!Contains(item)) { return; } + + bool wasEquipped = character.HasEquippedItem(item); + var indices = FindIndices(item); + + base.RemoveItem(item); +#if CLIENT + CreateSlots(); +#endif + //if the item was equipped and there are more items in the same stack, equip one of those items + if (tryEquipFromSameStack && wasEquipped) + { + int limbSlot = indices.Find(j => SlotTypes[j] != InvSlotType.Any); + foreach (int i in indices) + { + var itemInSameSlot = GetItemAt(i); + if (itemInSameSlot != null) + { + if (TryPutItem(itemInSameSlot, limbSlot, allowSwapping: false, allowCombine: false, character)) + { +#if CLIENT + visualSlots[i].ShowBorderHighlight(GUI.Style.Green, 0.1f, 0.412f); +#endif + } + break; + } + } + } + } + + /// /// If there is no room in the generic inventory (InvSlotType.Any), check if the item can be auto-equipped into its respective limbslot /// @@ -156,6 +203,19 @@ namespace Barotrauma } } + if (allowedSlots != null && !allowedSlots.Contains(InvSlotType.Any)) + { + int slot = FindLimbSlot(allowedSlots.First()); + if (slot > -1 && slots[slot].Items.Any(it => it != item) && slots[slot].First().AllowDroppingOnSwapWith(item)) + { + foreach (Item existingItem in slots[slot].Items.ToList()) + { + existingItem.Drop(user); + if (existingItem.ParentInventory != null) { existingItem.ParentInventory.RemoveItem(existingItem); } + } + } + } + return TryPutItem(item, user, allowedSlots, createNetworkEvent); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index 41502725e..dabd8d959 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -38,13 +38,15 @@ namespace Barotrauma.Items.Components private Fixture outsideBlocker; private Body doorBody; + private float dockingCooldown; + private bool docked; private bool obstructedWayPointsDisabled; private float forceLockTimer; //if the submarine isn't in the correct position to lock within this time after docking has been activated, //force the sub to the correct position - const float ForceLockDelay = 1.0f; + const float ForceLockDelay = 1.0f; public int DockingDir { get; set; } @@ -147,20 +149,12 @@ namespace Barotrauma.Items.Components DockingDir = GetDir(DockingTarget); DockingTarget.DockingDir = -DockingDir; } - if (joint != null) - { - CreateJoint(joint is WeldJoint); - LinkHullsToGaps(); - } - else if (DockingTarget.joint != null) - { - if (!GameMain.World.BodyList.Contains(DockingTarget.joint.BodyA) || - !GameMain.World.BodyList.Contains(DockingTarget.joint.BodyB)) - { - DockingTarget.CreateJoint(DockingTarget.joint is WeldJoint); - } - DockingTarget.LinkHullsToGaps(); - } + + //undock and redock to recreate the hulls, gaps and physics bodies + var prevDockingTarget = DockingTarget; + Undock(applyEffects: false); + Dock(prevDockingTarget); + Lock(isNetworkMessage: true, applyEffects: false); } } @@ -187,15 +181,15 @@ namespace Barotrauma.Items.Components private void AttemptDock() { var adjacentPort = FindAdjacentPort(); - - if (adjacentPort != null) Dock(adjacentPort); + if (adjacentPort != null) { Dock(adjacentPort); } } public void Dock(DockingPort target) { - if (item.Submarine.DockedTo.Contains(target.item.Submarine)) return; + if (item.Submarine.DockedTo.Contains(target.item.Submarine)) { return; } forceLockTimer = 0.0f; + dockingCooldown = 0.1f; if (DockingTarget != null) { @@ -237,7 +231,6 @@ namespace Barotrauma.Items.Components #if SERVER if (GameMain.Server != null && (!item.Submarine?.Loading ?? true)) { - originalDockingTargetID = DockingTarget.item.ID; item.CreateServerEvent(this); } #endif @@ -246,8 +239,7 @@ namespace Barotrauma.Items.Components OnDocked = null; } - - public void Lock(bool isNetworkMessage, bool forcePosition = false) + public void Lock(bool isNetworkMessage, bool applyEffects = true) { #if CLIENT if (GameMain.Client != null && !isNetworkMessage) { return; } @@ -264,7 +256,10 @@ namespace Barotrauma.Items.Components DockingDir = GetDir(DockingTarget); DockingTarget.DockingDir = -DockingDir; - ApplyStatusEffects(ActionType.OnUse, 1.0f); + if (applyEffects) + { + ApplyStatusEffects(ActionType.OnUse, 1.0f); + } Vector2 jointDiff = joint.WorldAnchorB - joint.WorldAnchorA; if (item.Submarine.PhysicsBody.Mass < DockingTarget.item.Submarine.PhysicsBody.Mass || @@ -284,7 +279,6 @@ namespace Barotrauma.Items.Components #if SERVER if (GameMain.Server != null && (!item.Submarine?.Loading ?? true)) { - originalDockingTargetID = DockingTarget.item.ID; item.CreateServerEvent(this); } #else @@ -846,13 +840,17 @@ namespace Barotrauma.Items.Components } } - public void Undock() + public void Undock(bool applyEffects = true) { - if (DockingTarget == null || !docked) return; + if (DockingTarget == null || !docked) { return; } forceLockTimer = 0.0f; + dockingCooldown = 0.1f; - ApplyStatusEffects(ActionType.OnSecondaryUse, 1.0f); + if (applyEffects) + { + ApplyStatusEffects(ActionType.OnSecondaryUse, 1.0f); + } DockingTarget.item.Submarine.ConnectedDockingPorts.Remove(item.Submarine); item.Submarine.ConnectedDockingPorts.Remove(DockingTarget.item.Submarine); @@ -877,6 +875,7 @@ namespace Barotrauma.Items.Components Item.Submarine.EnableObstructedWaypoints(DockingTarget.Item.Submarine); obstructedWayPointsDisabled = false; + Item.Submarine.RefreshOutdoorNodes(); DockingTarget.Undock(); DockingTarget = null; @@ -924,7 +923,6 @@ namespace Barotrauma.Items.Components #if SERVER if (GameMain.Server != null && (!item.Submarine?.Loading ?? true)) { - originalDockingTargetID = Entity.NullEntityID; item.CreateServerEvent(this); } #endif @@ -934,14 +932,13 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { + dockingCooldown -= deltaTime; if (DockingTarget == null) { dockingState = MathHelper.Lerp(dockingState, 0.0f, deltaTime * 10.0f); - if (dockingState < 0.01f) docked = false; - - item.SendSignal(0, "0", "state_out", null); - item.SendSignal(0, (FindAdjacentPort() != null) ? "1" : "0", "proximity_sensor", null); - + if (dockingState < 0.01f) { docked = false; } + item.SendSignal("0", "state_out"); + item.SendSignal((FindAdjacentPort() != null) ? "1" : "0", "proximity_sensor"); } else { @@ -987,7 +984,7 @@ namespace Barotrauma.Items.Components } else { - Lock(isNetworkMessage: false, forcePosition: true); + Lock(isNetworkMessage: false); } } else @@ -999,11 +996,12 @@ namespace Barotrauma.Items.Components dockingState = MathHelper.Lerp(dockingState, 1.0f, deltaTime * 10.0f); } - item.SendSignal(0, IsLocked ? "1" : "0", "state_out", null); + item.SendSignal(IsLocked ? "1" : "0", "state_out"); } if (!obstructedWayPointsDisabled && dockingState >= 0.99f) { Item.Submarine.DisableObstructedWayPoints(DockingTarget?.Item.Submarine); + Item.Submarine.RefreshOutdoorNodes(); obstructedWayPointsDisabled = true; } } @@ -1104,39 +1102,41 @@ namespace Barotrauma.Items.Components } } - public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f) + public override void ReceiveSignal(Signal signal, Connection connection) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + if (dockingCooldown > 0.0f) { return; } + bool wasDocked = docked; DockingPort prevDockingTarget = DockingTarget; switch (connection.Name) { case "toggle": - if (signal != "0") + if (signal.value != "0") { Docked = !docked; } break; case "set_active": case "set_state": - Docked = signal != "0"; + Docked = signal.value != "0"; break; } #if SERVER - if (sender != null && docked != wasDocked) + if (signal.sender != null && docked != wasDocked) { if (docked) { if (item.Submarine != null && DockingTarget?.item?.Submarine != null) - GameServer.Log(GameServer.CharacterLogName(sender) + " docked " + item.Submarine.Info.Name + " to " + DockingTarget.item.Submarine.Info.Name, ServerLog.MessageType.ItemInteraction); + GameServer.Log(GameServer.CharacterLogName(signal.sender) + " docked " + item.Submarine.Info.Name + " to " + DockingTarget.item.Submarine.Info.Name, ServerLog.MessageType.ItemInteraction); } else { if (item.Submarine != null && prevDockingTarget?.item?.Submarine != null) - GameServer.Log(GameServer.CharacterLogName(sender) + " undocked " + item.Submarine.Info.Name + " from " + prevDockingTarget.item.Submarine.Info.Name, ServerLog.MessageType.ItemInteraction); + GameServer.Log(GameServer.CharacterLogName(signal.sender) + " undocked " + item.Submarine.Info.Name + " from " + prevDockingTarget.item.Submarine.Info.Name, ServerLog.MessageType.ItemInteraction); } } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index c7d133f31..528fd48a5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -305,9 +305,21 @@ namespace Barotrauma.Items.Components private void ToggleState(ActionType actionType, Character user) { - if (toggleCooldownTimer > 0.0f && user != lastUser) { OnFailedToOpen(); return; } + if (toggleCooldownTimer > 0.0f && user != lastUser) + { + OnFailedToOpen(); + return; + } toggleCooldownTimer = ToggleCoolDown; - if (IsStuck || IsJammed) { toggleCooldownTimer = 1.0f; OnFailedToOpen(); return; } + if (IsStuck || IsJammed) + { +#if CLIENT + if (IsStuck) { HintManager.OnTryOpenStuckDoor(user); } +#endif + toggleCooldownTimer = 1.0f; + OnFailedToOpen(); + return; + } lastUser = user; SetState(PredictedState == null ? !isOpen : !PredictedState.Value, false, true, forcedOpen: actionType == ActionType.OnPicked); } @@ -395,7 +407,7 @@ namespace Barotrauma.Items.Components //don't use the predicted state here, because it might set //other items to an incorrect state if the prediction is wrong - item.SendSignal(0, isOpen ? "1" : "0", "state_out", null); + item.SendSignal(isOpen ? "1" : "0", "state_out"); } partial void UpdateProjSpecific(float deltaTime); @@ -651,7 +663,7 @@ namespace Barotrauma.Items.Components } } - public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f) + public override void ReceiveSignal(Signal signal, Connection connection) { if (IsStuck || IsJammed) { return; } @@ -659,24 +671,24 @@ namespace Barotrauma.Items.Components if (connection.Name == "toggle") { - if (signal == "0") { return; } - if (toggleCooldownTimer > 0.0f && sender != lastUser) { OnFailedToOpen(); return; } + if (signal.value == "0") { return; } + if (toggleCooldownTimer > 0.0f && signal.sender != lastUser) { OnFailedToOpen(); return; } if (IsStuck) { toggleCooldownTimer = 1.0f; OnFailedToOpen(); return; } toggleCooldownTimer = ToggleCoolDown; - lastUser = sender; + lastUser = signal.sender; SetState(!wasOpen, false, true, forcedOpen: false); } else if (connection.Name == "set_state") { - bool signalOpen = signal != "0"; + bool signalOpen = signal.value != "0"; if (IsStuck && signalOpen != wasOpen) { toggleCooldownTimer = 1.0f; OnFailedToOpen(); return; } SetState(signalOpen, false, true, forcedOpen: false); } #if SERVER - if (sender != null && wasOpen != isOpen) + if (signal.sender != null && wasOpen != isOpen) { - GameServer.Log(GameServer.CharacterLogName(sender) + (isOpen ? " opened " : " closed ") + item.Name, ServerLog.MessageType.ItemInteraction); + GameServer.Log(GameServer.CharacterLogName(signal.sender) + (isOpen ? " opened " : " closed ") + item.Name, ServerLog.MessageType.ItemInteraction); } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index ecf87e040..60b08dd4a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -39,6 +39,12 @@ namespace Barotrauma.Items.Components get; private set; } + [Serialize(true, true, description: "Is the item currently able to push characters around? True by default. Only valid if blocksplayers is set to true.")] + public bool CanPush + { + get; + set; + } //the angle in which the Character holds the item protected float holdAngle; @@ -208,6 +214,7 @@ namespace Barotrauma.Items.Components if (other.Body.UserData is Character character) { if (!IsActive) { return false; } + if (!CanPush) { return false; } return character != picker; } else @@ -436,6 +443,7 @@ namespace Barotrauma.Items.Components } else { + //not attached -> pick the item instantly, ignoring picking time return OnPicked(picker); } @@ -443,6 +451,10 @@ namespace Barotrauma.Items.Components public override bool OnPicked(Character picker) { + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) + { + return false; + } if (base.OnPicked(picker)) { DeattachFromWall(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index abae924a3..a8b797122 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -62,6 +62,7 @@ namespace Barotrauma.Items.Components { if (!subElement.Name.ToString().Equals("attack", StringComparison.OrdinalIgnoreCase)) { continue; } Attack = new Attack(subElement, item.Name + ", MeleeWeapon"); + Attack.DamageRange = item.body == null ? 10.0f : ConvertUnits.ToDisplayUnits(item.body.GetMaxExtent()); } item.IsShootable = true; // TODO: should define this in xml if we have melee weapons that don't require aim to use diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs index 945b05189..7e5aa6ddd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs @@ -41,25 +41,25 @@ namespace Barotrauma.Items.Components public override bool Use(float deltaTime, Character character = null) { if (character == null || character.Removed) return false; - if (!character.IsKeyDown(InputType.Aim) || character.Stun > 0.0f) return false; + if (!character.IsKeyDown(InputType.Aim) || character.Stun > 0.0f) { return false; } IsActive = true; useState = 0.1f; if (character.AnimController.InWater) { - if (UsableIn == UseEnvironment.Air) return true; + if (UsableIn == UseEnvironment.Air) { return true; } } else { - if (UsableIn == UseEnvironment.Water) return true; + if (UsableIn == UseEnvironment.Water) { return true; } } Vector2 dir = Vector2.Normalize(character.CursorPosition - character.Position); //move upwards if the cursor is at the position of the character if (!MathUtils.IsValid(dir)) dir = Vector2.UnitY; - Vector2 propulsion = dir * Force; + Vector2 propulsion = dir * Force * character.PropulsionSpeedMultiplier; if (character.AnimController.InWater) character.AnimController.TargetMovement = dir; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index 0a8abef5a..21d31ac64 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -12,7 +12,8 @@ namespace Barotrauma.Items.Components { partial class RangedWeapon : ItemComponent { - private float reload, reloadTimer; + private float reload; + public float ReloadTimer { get; private set; } private Vector2 barrelPos; @@ -75,17 +76,17 @@ namespace Barotrauma.Items.Components public override void Equip(Character character) { - reloadTimer = Math.Min(reload, 1.0f); + ReloadTimer = Math.Min(reload, 1.0f); IsActive = true; } public override void Update(float deltaTime, Camera cam) { - reloadTimer -= deltaTime; + ReloadTimer -= deltaTime; - if (reloadTimer < 0.0f) + if (ReloadTimer < 0.0f) { - reloadTimer = 0.0f; + ReloadTimer = 0.0f; IsActive = false; } } @@ -101,10 +102,10 @@ namespace Barotrauma.Items.Components public override bool Use(float deltaTime, Character character = null) { if (character == null || character.Removed) { return false; } - if ((item.RequireAimToUse && !character.IsKeyDown(InputType.Aim)) || reloadTimer > 0.0f) { return false; } + if ((item.RequireAimToUse && !character.IsKeyDown(InputType.Aim)) || ReloadTimer > 0.0f) { return false; } IsActive = true; - reloadTimer = reload; + ReloadTimer = reload; if (item.AiTarget != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index 4fa45a4b8..5ad4e89bf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -796,7 +796,7 @@ namespace Barotrauma.Items.Components bool leakFixed = (leak.Open <= 0.0f || leak.Removed) && (leak.ConnectedWall == null || leak.ConnectedWall.Sections.Average(s => s.damage) < 1); - if (leakFixed && leak.FlowTargetHull?.DisplayName != null) + if (leakFixed && leak.FlowTargetHull?.DisplayName != null && character.IsOnPlayerTeam) { if (!leak.FlowTargetHull.ConnectedGaps.Any(g => !g.IsRoomToRoom && g.Open > 0.0f)) { @@ -854,9 +854,11 @@ namespace Barotrauma.Items.Components object value = property.GetValue(target); if (door.Stuck > 0) { + bool isCutting = effect.propertyEffects[i].GetType() == typeof(float) && (float)effect.propertyEffects[i] < 0; var progressBar = user.UpdateHUDProgressBar(door, door.Item.WorldPosition, door.Stuck / 100, Color.DarkGray * 0.5f, Color.White, - effect.propertyEffects[i].GetType() == typeof(float) && (float)effect.propertyEffects[i] < 0 ? "progressbar.cutting" : "progressbar.welding"); + textTag: isCutting ? "progressbar.cutting" : "progressbar.welding"); if (progressBar != null) { progressBar.Size = new Vector2(60.0f, 20.0f); } + if (!isCutting) { HintManager.OnWeldingDoor(user, door); } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index dd00b291c..c4a873586 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -432,27 +432,27 @@ namespace Barotrauma.Items.Components //called then the item is dropped or dragged out of a "limbslot" public virtual void Unequip(Character character) { } - public virtual void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f) + public virtual void ReceiveSignal(Signal signal, Connection connection) { switch (connection.Name) { case "activate": case "use": case "trigger_in": - if (signal != "0") + if (signal.value != "0") { - item.Use(1.0f, sender); + item.Use(1.0f, signal.sender); } break; case "toggle": - if (signal != "0") + if (signal.value != "0") { IsActive = !isActive; } break; case "set_active": case "set_state": - IsActive = signal != "0"; + IsActive = signal.value != "0"; break; } } @@ -771,6 +771,10 @@ namespace Barotrauma.Items.Components brokenEffects.ForEach(e => e.SetUser(user)); } } + +#if CLIENT + HintManager.OnStatusEffectApplied(this, type, character); +#endif } public virtual void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap) @@ -952,26 +956,6 @@ namespace Barotrauma.Items.Components #region AI related protected const float AIUpdateInterval = 0.2f; protected float aiUpdateTimer; - private int itemIndex; - private Character previousUser; - protected bool FindSuitableContainer(Character character, Func priority, out Item suitableContainer) - { - suitableContainer = null; - if (character.AIController is HumanAIController aiController) - { - if (previousUser != character) - { - previousUser = character; - itemIndex = 0; - } - if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: aiController.IgnoredItems, customPriorityFunction: priority, positionalReference: Item)) - { - suitableContainer = targetContainer; - return true; - } - } - return false; - } protected AIObjectiveContainItem AIContainItems(ItemContainer container, Character character, AIObjective currentObjective, int itemCount, bool equip, bool removeEmpty, bool spawnItemIfNotFound = false, bool dropItemOnDeselected = false) where T : ItemComponent { @@ -1014,83 +998,6 @@ namespace Barotrauma.Items.Components } return containObjective; } - - /// - /// Returns true when done seeking the suitable container. - /// - protected bool AIDecontainEmptyItems(Character character, AIObjective objective, bool equip, ItemContainer sourceContainer = null) - { - if (character.AIController is HumanAIController aiController) - { - ItemContainer sourceC = sourceContainer ?? (item.OwnInventory?.Owner is Item it ? it.GetComponent() : null); - var containedItems = sourceContainer != null ? sourceContainer.Inventory.AllItems : item.OwnInventory.AllItems; - foreach (Item containedItem in containedItems) - { - if (containedItem != null && containedItem.Condition <= 0.0f) - { - if (FindSuitableContainer(character, - i => - { - if (i.IsThisOrAnyContainerIgnoredByAI()) { return 0; } - var container = i.GetComponent(); - if (container == null) { return 0; } - if (!container.Inventory.CanBePut(containedItem)) { return 0; } - // Ignore containers that are identical to the source container - if (sourceC != null && container.Item.Prefab == sourceC.Item.Prefab) { return 0; } - if (container.ShouldBeContained(containedItem, out bool isRestrictionsDefined)) - { - if (isRestrictionsDefined) - { - return 10; - } - else - { - if (containedItem.IsContainerPreferred(container, out bool isPreferencesDefined, out bool isSecondary)) - { - return isPreferencesDefined ? isSecondary ? 2 : 5 : 1; - } - else - { - return isPreferencesDefined ? 0 : 1; - } - } - } - else - { - return 0; - } - }, out Item targetContainer)) - { - var decontainObjective = new AIObjectiveDecontainItem(character, containedItem, objective.objectiveManager, sourceC, targetContainer?.GetComponent()) - { - Equip = equip - }; - decontainObjective.Abandoned += () => - { - itemIndex = 0; - if (targetContainer != null) - { - aiController.IgnoredItems.Add(targetContainer); - } - }; - decontainObjective.Completed += () => - { - if (targetContainer == null) - { - itemIndex = 0; - } - }; - objective.AddSubObjectiveInQueue(decontainObjective); - } - else - { - return false; - } - } - } - } - return true; - } #endregion } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemLabel.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemLabel.cs index a523fbc60..9cf2e1842 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemLabel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemLabel.cs @@ -13,13 +13,13 @@ namespace Barotrauma.Items.Components partial void OnStateChanged(); - public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1) + public override void ReceiveSignal(Signal signal, Connection connection) { switch (connection.Name) { case "set_text": - if (Text == signal) { return; } - Text = signal; + if (Text == signal.value) { return; } + Text = signal.value; OnStateChanged(); break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index 710414f2e..adfb53e05 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -152,7 +152,7 @@ namespace Barotrauma.Items.Components if (IsToggle) { - item.SendSignal(0, State ? "1" : "0", "signal_out", sender: null); + item.SendSignal(State ? "1" : "0", "signal_out"); } if (user == null @@ -277,7 +277,7 @@ namespace Barotrauma.Items.Components return false; } - item.SendSignal(0, "1", "trigger_out", user); + item.SendSignal(new Signal("1", sender: user), "trigger_out"); ApplyStatusEffects(ActionType.OnUse, 1.0f, activator); @@ -343,14 +343,14 @@ namespace Barotrauma.Items.Components public Item GetFocusTarget() { - item.SendSignal(0, MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture), "position_out", user); + item.SendSignal(new Signal(MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture), sender: user), "position_out"); for (int i = item.LastSentSignalRecipients.Count - 1; i >= 0; i--) { - if (item.LastSentSignalRecipients[i].Condition <= 0.0f) continue; - if (item.LastSentSignalRecipients[i].Prefab.FocusOnSelected) + if (item.LastSentSignalRecipients[i].Item.Condition <= 0.0f) { continue; } + if (item.LastSentSignalRecipients[i].Item.Prefab.FocusOnSelected) { - return item.LastSentSignalRecipients[i]; + return item.LastSentSignalRecipients[i].Item; } } @@ -374,7 +374,7 @@ namespace Barotrauma.Items.Components } else { - item.SendSignal(0, "1", "signal_out", picker); + item.SendSignal(new Signal("1", sender: picker), "signal_out"); } #if CLIENT PlaySound(ActionType.OnUse, picker); @@ -442,7 +442,7 @@ namespace Barotrauma.Items.Components #if SERVER item.CreateServerEvent(this); #endif - item.SendSignal(0, "1", "signal_out", user); + item.SendSignal(new Signal("1", sender: user), "signal_out"); return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs index eb947a5ae..e4502d68c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs @@ -175,7 +175,7 @@ namespace Barotrauma.Items.Components Vector2 propellerWorldPos = item.WorldPosition + PropellerPos * item.Scale; foreach (Character character in Character.CharacterList) { - if (character.Submarine != null || !character.Enabled || character.Removed) { continue; } + if (!character.Enabled || character.Removed) { continue; } float distSqr = Vector2.DistanceSquared(character.WorldPosition, propellerWorldPos); if (distSqr > scaledDamageRange * scaledDamageRange) { continue; } character.LastDamageSource = item; @@ -201,17 +201,17 @@ namespace Barotrauma.Items.Components PropellerPos = new Vector2(PropellerPos.X, -PropellerPos.Y); } - public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f) + public override void ReceiveSignal(Signal signal, Connection connection) { - base.ReceiveSignal(stepsTaken, signal, connection, source, sender, power, signalStrength); + base.ReceiveSignal(signal, connection); if (connection.Name == "set_force") { - if (float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float tempForce)) + if (float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float tempForce)) { controlLockTimer = 0.1f; targetForce = MathHelper.Clamp(tempForce, -100.0f, 100.0f); - User = sender; + User = signal.sender; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index dd3f19214..7e19a9248 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Security.Cryptography; using System.Xml.Linq; namespace Barotrauma.Items.Components @@ -96,6 +97,12 @@ namespace Barotrauma.Items.Components fabricationRecipes.Add(recipe); } } + fabricationRecipes.Sort((r1, r2) => + { + int hash1 = (int)r1.TargetItem.UIntIdentifier; + int hash2 = (int)r2.TargetItem.UIntIdentifier; + return hash1 - hash2; + }); state = FabricatorState.Stopped; @@ -114,11 +121,10 @@ namespace Barotrauma.Items.Components inputContainer = containers[0]; outputContainer = containers[1]; - + foreach (var recipe in fabricationRecipes) { - int ingredientCount = recipe.RequiredItems.Sum(it => it.Amount); - if (ingredientCount > inputContainer.Capacity) + if (recipe.RequiredItems.Count > inputContainer.Capacity) { DebugConsole.ThrowError("Error in item \"" + item.Name + "\": There's not enough room in the input inventory for the ingredients of \"" + recipe.TargetItem.Name + "\"!"); } @@ -205,6 +211,7 @@ namespace Barotrauma.Items.Components progressState = 0.0f; timeUntilReady = 0.0f; + UpdateRequiredTimeProjSpecific(); inputContainer.Inventory.Locked = false; outputContainer.Inventory.Locked = false; @@ -272,6 +279,7 @@ namespace Barotrauma.Items.Components if (powerConsumption <= 0) { Voltage = 1.0f; } timeUntilReady -= deltaTime * Math.Min(Voltage, 1.0f); + UpdateRequiredTimeProjSpecific(); if (timeUntilReady > 0.0f) { return; } @@ -353,6 +361,8 @@ namespace Barotrauma.Items.Components } } + partial void UpdateRequiredTimeProjSpecific(); + private bool CanBeFabricated(FabricationRecipe fabricableItem) { if (fabricableItem == null) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs index bba73efce..8ffa96100 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs @@ -87,8 +87,9 @@ namespace Barotrauma.Items.Components return picker != null; } - public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1.0f) + public override void ReceiveSignal(Signal signal, Connection connection) { + Item source = signal.source; if (source == null || source.CurrentHull == null) { return; } Hull sourceHull = source.CurrentHull; @@ -116,7 +117,7 @@ namespace Barotrauma.Items.Components case "oxygen_data_in": float oxy; - if (!float.TryParse(signal, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out oxy)) + if (!float.TryParse(signal.value, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out oxy)) { oxy = Rand.Range(0.0f, 100.0f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index a8d8edf58..b46e28726 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -142,7 +142,7 @@ namespace Barotrauma.Items.Components partial void UpdateProjSpecific(float deltaTime); - public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f) + public override void ReceiveSignal(Signal signal, Connection connection) { if (Hijacked) { return; } @@ -153,12 +153,12 @@ namespace Barotrauma.Items.Components } else if (connection.Name == "set_active") { - IsActive = signal != "0"; + IsActive = signal.value != "0"; isActiveLockTimer = 0.1f; } else if (connection.Name == "set_speed") { - if (float.TryParse(signal, NumberStyles.Any, CultureInfo.InvariantCulture, out float tempSpeed)) + if (float.TryParse(signal.value, NumberStyles.Any, CultureInfo.InvariantCulture, out float tempSpeed)) { flowPercentage = MathHelper.Clamp(tempSpeed, -100.0f, 100.0f); TargetLevel = null; @@ -167,9 +167,9 @@ namespace Barotrauma.Items.Components } else if (connection.Name == "set_targetlevel") { - if (float.TryParse(signal, NumberStyles.Any, CultureInfo.InvariantCulture, out float tempTarget)) + if (float.TryParse(signal.value, NumberStyles.Any, CultureInfo.InvariantCulture, out float tempTarget)) { - TargetLevel = MathHelper.Clamp(tempTarget + 50.0f, 0.0f, 100.0f); + TargetLevel = MathUtils.InverseLerp(-100.0f, 100.0f, tempTarget) * 100.0f; pumpSpeedLockTimer = 0.1f; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs index a769590c8..6e0073d67 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs @@ -221,6 +221,13 @@ namespace Barotrauma.Items.Components } } +#if CLIENT + if(PowerOn && AvailableFuel < 1) + { + HintManager.OnReactorOutOfFuel(this); + } +#endif + prevAvailableFuel = AvailableFuel; ApplyStatusEffects(ActionType.OnActive, deltaTime, null); @@ -326,7 +333,7 @@ namespace Barotrauma.Items.Components if (item.CurrentHull != null) { var aiTarget = item.CurrentHull.AiTarget; - if (aiTarget != null) + if (aiTarget != null && MaxPowerOutput > 0) { float range = Math.Abs(currPowerConsumption) / MaxPowerOutput; float noise = MathHelper.Lerp(aiTarget.MinSoundRange, aiTarget.MaxSoundRange, range); @@ -334,7 +341,7 @@ namespace Barotrauma.Items.Components } } - if (item.AiTarget != null) + if (item.AiTarget != null && MaxPowerOutput > 0) { var aiTarget = item.AiTarget; float range = Math.Abs(currPowerConsumption) / MaxPowerOutput; @@ -342,10 +349,10 @@ namespace Barotrauma.Items.Components } } - item.SendSignal(0, ((int)(temperature * 100.0f)).ToString(), "temperature_out", null); - item.SendSignal(0, ((int)-CurrPowerConsumption).ToString(), "power_value_out", null); - item.SendSignal(0, ((int)load).ToString(), "load_value_out", null); - item.SendSignal(0, ((int)AvailableFuel).ToString(), "fuel_out", null); + item.SendSignal(((int)(temperature * 100.0f)).ToString(), "temperature_out"); + item.SendSignal(((int)-CurrPowerConsumption).ToString(), "power_value_out"); + item.SendSignal(((int)load).ToString(), "load_value_out"); + item.SendSignal(((int)AvailableFuel).ToString(), "fuel_out"); UpdateFailures(deltaTime); #if CLIENT @@ -427,7 +434,7 @@ namespace Barotrauma.Items.Components { if (temperature > allowedTemperature.Y) { - item.SendSignal(0, "1", "meltdown_warning", null); + item.SendSignal("1", "meltdown_warning"); //faster meltdown if the item is in a bad condition meltDownTimer += MathHelper.Lerp(deltaTime * 2.0f, deltaTime, item.Condition / item.MaxCondition); @@ -439,7 +446,7 @@ namespace Barotrauma.Items.Components } else { - item.SendSignal(0, "0", "meltdown_warning", null); + item.SendSignal("0", "meltdown_warning"); meltDownTimer = Math.Max(0.0f, meltDownTimer - deltaTime); } @@ -509,7 +516,7 @@ namespace Barotrauma.Items.Components { base.UpdateBroken(deltaTime, cam); - item.SendSignal(0, ((int)(temperature * 100.0f)).ToString(), "temperature_out", null); + item.SendSignal(((int)(temperature * 100.0f)).ToString(), "temperature_out"); currPowerConsumption = 0.0f; Temperature -= deltaTime * 1000.0f; @@ -568,49 +575,53 @@ namespace Barotrauma.Items.Components //characters with insufficient skill levels don't refuel the reactor if (degreeOfSuccess > refuelLimit) { - if (objective.SubObjectives.None()) - { - if (!AIDecontainEmptyItems(character, objective, equip: false)) - { - return false; - } - } - if (aiUpdateTimer > 0.0f) { aiUpdateTimer -= deltaTime; return false; } aiUpdateTimer = AIUpdateInterval; - // load more fuel if the current maximum output is only 50% of the current load // or if the fuel rod is (almost) deplenished - float minCondition = fuelConsumptionRate * MathUtils.Pow((degreeOfSuccess - refuelLimit) * 2, 2); + float minCondition = fuelConsumptionRate * MathUtils.Pow2((degreeOfSuccess - refuelLimit) * 2); if (NeedMoreFuel(minimumOutputRatio: 0.5f, minCondition: minCondition)) { + bool outOfFuel = false; var container = item.GetComponent(); if (objective.SubObjectives.None()) { - int itemCount = item.ContainedItems.Count(i => i != null && container.ContainableItems.Any(ri => ri.MatchesItem(i))) + 1; - AIContainItems(container, character, objective, itemCount, equip: false, removeEmpty: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC, dropItemOnDeselected: true); + var containObjective = AIContainItems(container, character, objective, itemCount: 1, equip: true, removeEmpty: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC, dropItemOnDeselected: true); + containObjective.Completed += ReportFuelRodCount; + containObjective.Abandoned += ReportFuelRodCount; character.Speak(TextManager.Get("DialogReactorFuel"), null, 0.0f, "reactorfuel", 30.0f); - } - return false; - } - else if (TooMuchFuel()) - { - if (item.OwnInventory?.AllItems != null) - { - var container = item.GetComponent(); - foreach (Item item in item.OwnInventory.AllItemsMod) + + void ReportFuelRodCount() { - if (container.ContainableItems.Any(ri => ri.MatchesItem(item))) + if (!character.IsOnPlayerTeam) { return; } + int remainingFuelRods = Submarine.MainSub.GetItems(false).Count(i => i.HasTag("reactorfuel") && i.Condition > 1); + if (remainingFuelRods == 0) { - item.Drop(character); - break; + character.Speak(TextManager.Get("DialogOutOfFuelRods"), null, 0.0f, "outoffuelrods", 30.0f); + outOfFuel = true; + } + else if (remainingFuelRods < 3) + { + character.Speak(TextManager.Get("DialogLowOnFuelRods"), null, 0.0f, "lowonfuelrods", 30.0f); } } } + return outOfFuel; + } + else + { + if (TooMuchFuel()) + { + DropFuel(minCondition: 0.1f, maxCondition: 100); + } + else + { + DropFuel(minCondition: 0, maxCondition: 0); + } } } } @@ -619,7 +630,7 @@ namespace Barotrauma.Items.Components { if (lastUser != null && lastUser != character && lastUser != LastAIUser) { - if (lastUser.SelectedConstruction == item) + if (lastUser.SelectedConstruction == item && character.IsOnPlayerTeam) { character.Speak(TextManager.Get("DialogReactorTaken"), null, 0.0f, "reactortaken", 10.0f); } @@ -676,6 +687,23 @@ namespace Barotrauma.Items.Components aiUpdateTimer = AIUpdateInterval; return false; } + + + void DropFuel(float minCondition, float maxCondition) + { + if (item.OwnInventory?.AllItems != null) + { + var container = item.GetComponent(); + foreach (Item item in item.OwnInventory.AllItemsMod) + { + if (item.ConditionPercentage <= maxCondition && item.ConditionPercentage >= minCondition) + { + item.Drop(character); + break; + } + } + } + } } public override void OnMapLoaded() @@ -683,7 +711,7 @@ namespace Barotrauma.Items.Components prevAvailableFuel = AvailableFuel; } - public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power, float signalStrength = 1.0f) + public override void ReceiveSignal(Signal signal, Connection connection) { switch (connection.Name) { @@ -698,7 +726,7 @@ namespace Barotrauma.Items.Components } break; case "set_fissionrate": - if (PowerOn && float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float newFissionRate)) + if (PowerOn && float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float newFissionRate)) { targetFissionRate = newFissionRate; if (GameMain.NetworkMember?.IsServer ?? false) { unsentChanges = true; } @@ -708,7 +736,7 @@ namespace Barotrauma.Items.Components } break; case "set_turbineoutput": - if (PowerOn && float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float newTurbineOutput)) + if (PowerOn && float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float newTurbineOutput)) { targetTurbineOutput = newTurbineOutput; if (GameMain.NetworkMember?.IsServer ?? false) { unsentChanges = true; } @@ -719,6 +747,5 @@ namespace Barotrauma.Items.Components break; } } - } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs index 6e09bdcf2..a656e622b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs @@ -150,7 +150,7 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - currPowerConsumption = powerConsumption; + currPowerConsumption = (currentMode == Mode.Active) ? powerConsumption : powerConsumption * 0.1f; UpdateOnActiveEffects(deltaTime); @@ -252,9 +252,10 @@ namespace Barotrauma.Items.Components } foreach (Character c in Character.CharacterList) { - if (c.AnimController.CurrentHull != null || !c.Enabled) continue; - if (DetectSubmarineWalls && c.AnimController.CurrentHull == null && item.CurrentHull != null) continue; - if (Vector2.DistanceSquared(c.WorldPosition, item.WorldPosition) > range * range) continue; + if (c.IsDead || c.Removed || !c.Enabled) { continue; } + if (c.AnimController.CurrentHull != null || c.Params.HideInSonar) { continue; } + if (DetectSubmarineWalls && c.AnimController.CurrentHull == null && item.CurrentHull != null) { continue; } + if (Vector2.DistanceSquared(c.WorldPosition, item.WorldPosition) > range * range) { continue; } string directionName = GetDirectionName(c.WorldPosition - item.WorldPosition); if (!targetGroups.ContainsKey(directionName)) @@ -277,9 +278,12 @@ namespace Barotrauma.Items.Components dialogTag = "DialogSonarTargetLarge"; } - character.Speak(TextManager.GetWithVariables(dialogTag, new string[2] { "[direction]", "[count]" }, - new string[2] { targetGroup.Key.ToString(), targetGroup.Value.Count.ToString() }, - new bool[2] { true, false }), null, 0, "sonartarget" + targetGroup.Value[0].ID, 60); + if (character.IsOnPlayerTeam) + { + character.Speak(TextManager.GetWithVariables(dialogTag, new string[2] { "[direction]", "[count]" }, + new string[2] { targetGroup.Key.ToString(), targetGroup.Value.Count.ToString() }, + new bool[2] { true, false }), null, 0, "sonartarget" + targetGroup.Value[0].ID, 60); + } //prevent the character from reporting other targets in the group for (int i = 1; i < targetGroup.Value.Count; i++) @@ -321,23 +325,23 @@ namespace Barotrauma.Items.Components return transducerPosSum / connectedTransducers.Count; } - public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1.0f) + public override void ReceiveSignal(Signal signal, Connection connection) { - base.ReceiveSignal(stepsTaken, signal, connection, source, sender, power, signalStrength); + base.ReceiveSignal(signal, connection); if (connection.Name == "transducer_in") { - var transducer = source.GetComponent(); + var transducer = signal.source.GetComponent(); if (transducer == null) return; var connectedTransducer = connectedTransducers.Find(t => t.Transducer == transducer); if (connectedTransducer == null) { - connectedTransducers.Add(new ConnectedTransducer(transducer, signalStrength, 1.0f)); + connectedTransducers.Add(new ConnectedTransducer(transducer, signal.strength, 1.0f)); } else { - connectedTransducer.SignalStrength = signalStrength; + connectedTransducer.SignalStrength = signal.strength; connectedTransducer.DisconnectTimer = 1.0f; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/SonarTransducer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/SonarTransducer.cs index 3adf63eb5..3d09af2aa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/SonarTransducer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/SonarTransducer.cs @@ -24,7 +24,7 @@ namespace Barotrauma.Items.Components sendSignalTimer += deltaTime; if (sendSignalTimer > SendSignalInterval) { - item.SendSignal(0, "0101101101101011010", "data_out", sender: null); + item.SendSignal("0101101101101011010", "data_out"); sendSignalTimer = SendSignalInterval; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index 404318043..eced05614 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -12,16 +12,21 @@ namespace Barotrauma.Items.Components { partial class Steering : Powered, IServerSerializable, IClientSerializable { + public const float AutopilotMinDistToPathNode = 30.0f; + private const float AutopilotRayCastInterval = 0.5f; private const float RecalculatePathInterval = 5.0f; - private const float AutopilotMinDistToPathNode = 30.0f; - private const float AutoPilotSteeringLerp = 0.1f; private const float AutoPilotMaxSpeed = 0.5f; private const float AIPilotMaxSpeed = 1.0f; + /// + /// How fast the steering vector adjusts when the nav terminal is operated by something else than a character (= signals) + /// + const float DefaultSteeringAdjustSpeed = 0.2f; + private Vector2 targetVelocity; private Vector2 steeringInput; @@ -333,13 +338,12 @@ namespace Barotrauma.Items.Components } } - float targetLevel = targetVelocity.X; - if (controlledSub != null && controlledSub.FlippedX) { targetLevel *= -1; } - item.SendSignal(0, targetLevel.ToString(CultureInfo.InvariantCulture), "velocity_x_out", user); + float velX = targetVelocity.X; + if (controlledSub != null && controlledSub.FlippedX) { velX *= -1; } + item.SendSignal(new Signal(velX.ToString(CultureInfo.InvariantCulture), sender: user), "velocity_x_out"); - targetLevel = -targetVelocity.Y; - targetLevel += (neutralBallastLevel - 0.5f) * 100.0f; - item.SendSignal(0, targetLevel.ToString(CultureInfo.InvariantCulture), "velocity_y_out", user); + float velY = MathHelper.Lerp((neutralBallastLevel * 100 - 50) * 2, -100 * Math.Sign(targetVelocity.Y), Math.Abs(targetVelocity.Y) / 100.0f); + item.SendSignal(new Signal(velY.ToString(CultureInfo.InvariantCulture), sender: user), "velocity_y_out"); } private void IncreaseSkillLevel(Character user, float deltaTime) @@ -543,6 +547,10 @@ namespace Barotrauma.Items.Components { TargetVelocity *= 100.0f / velMagnitude; } + +#if CLIENT + HintManager.OnAutoPilotPathUpdated(this); +#endif } private float? GetNodePenalty(PathNode node, PathNode nextNode) @@ -626,7 +634,7 @@ namespace Barotrauma.Items.Components { if (objective.Override) { - if (user != character && user != null && user.SelectedConstruction == item) + if (user != character && user != null && user.SelectedConstruction == item && character.IsOnPlayerTeam) { character.Speak(TextManager.Get("DialogSteeringTaken"), null, 0.0f, "steeringtaken", 10.0f); } @@ -661,7 +669,7 @@ namespace Barotrauma.Items.Components if (Level.IsLoadedOutpost) { break; } if (DockingSources.Any(d => d.Docked)) { - item.SendSignal(0, "1", "toggle_docking", sender: null); + item.SendSignal("1", "toggle_docking"); } if (objective.Override) { @@ -676,7 +684,7 @@ namespace Barotrauma.Items.Components if (Level.IsLoadedOutpost) { break; } if (DockingSources.Any(d => d.Docked)) { - item.SendSignal(0, "1", "toggle_docking", sender: null); + item.SendSignal("1", "toggle_docking"); } if (objective.Override) { @@ -689,22 +697,25 @@ namespace Barotrauma.Items.Components break; } sonar?.AIOperate(deltaTime, character, objective); - if (!MaintainPos && showIceSpireWarning) + if (!MaintainPos && showIceSpireWarning && character.IsOnPlayerTeam) { character.Speak(TextManager.Get("dialogicespirespottedsonar"), null, 0.0f, "icespirespottedsonar", 60.0f); } return false; } - public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f) + public override void ReceiveSignal(Signal signal, Connection connection) { if (connection.Name == "velocity_in") { - TargetVelocity = XMLExtensions.ParseVector2(signal, errorMessages: false); + steeringAdjustSpeed = DefaultSteeringAdjustSpeed; + steeringInput = XMLExtensions.ParseVector2(signal.value, errorMessages: false); + steeringInput.X = MathHelper.Clamp(steeringInput.X, -100.0f, 100.0f); + steeringInput.Y = MathHelper.Clamp(-steeringInput.Y, -100.0f, 100.0f); } else { - base.ReceiveSignal(stepsTaken, signal, connection, source, sender, power, signalStrength); + base.ReceiveSignal(signal, connection); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs index 246217863..f0549f6a9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs @@ -199,9 +199,9 @@ namespace Barotrauma.Items.Components Charge -= CurrPowerOutput / 3600.0f; } - item.SendSignal(0, ((int)Math.Round(Charge)).ToString(), "charge", null); - item.SendSignal(0, ((int)Math.Round(Charge / capacity * 100)).ToString(), "charge_%", null); - item.SendSignal(0, ((int)Math.Round(RechargeSpeed / maxRechargeSpeed * 100)).ToString(), "charge_rate", null); + item.SendSignal(((int)Math.Round(Charge)).ToString(), "charge"); + item.SendSignal(((int)Math.Round(Charge / capacity * 100)).ToString(), "charge_%"); + item.SendSignal(((int)Math.Round(RechargeSpeed / maxRechargeSpeed * 100)).ToString(), "charge_rate"); } public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) @@ -228,11 +228,13 @@ namespace Barotrauma.Items.Components { rechargeSpeedSlider.BarScroll = RechargeSpeed / Math.Max(maxRechargeSpeed, 1.0f); } -#endif - - character.Speak(TextManager.GetWithVariables("DialogChargeBatteries", new string[2] { "[itemname]", "[rate]" }, - new string[2] { item.Name, ((int)(rechargeSpeed / maxRechargeSpeed * 100.0f)).ToString() }, - new bool[2] { true, false }), null, 1.0f, "chargebattery", 10.0f); +#endif + if (character.IsOnPlayerTeam) + { + character.Speak(TextManager.GetWithVariables("DialogChargeBatteries", new string[2] { "[itemname]", "[rate]" }, + new string[2] { item.Name, ((int)(rechargeSpeed / maxRechargeSpeed * 100.0f)).ToString() }, + new bool[2] { true, false }), null, 1.0f, "chargebattery", 10.0f); + } } } else @@ -249,22 +251,25 @@ namespace Barotrauma.Items.Components rechargeSpeedSlider.BarScroll = RechargeSpeed / Math.Max(maxRechargeSpeed, 1.0f); } #endif - character.Speak(TextManager.GetWithVariables("DialogStopChargingBatteries", new string[2] { "[itemname]", "[rate]" }, - new string[2] { item.Name, ((int)(rechargeSpeed / maxRechargeSpeed * 100.0f)).ToString() }, - new bool[2] { true, false }), null, 1.0f, "chargebattery", 10.0f); + if (character.IsOnPlayerTeam) + { + character.Speak(TextManager.GetWithVariables("DialogStopChargingBatteries", new string[2] { "[itemname]", "[rate]" }, + new string[2] { item.Name, ((int)(rechargeSpeed / maxRechargeSpeed * 100.0f)).ToString() }, + new bool[2] { true, false }), null, 1.0f, "chargebattery", 10.0f); + } } } return true; } - public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power, float signalStrength = 1.0f) + public override void ReceiveSignal(Signal signal, Connection connection) { if (connection.IsPower) { return; } if (connection.Name == "set_rate") { - if (float.TryParse(signal, NumberStyles.Any, CultureInfo.InvariantCulture, out float tempSpeed)) + if (float.TryParse(signal.value, NumberStyles.Any, CultureInfo.InvariantCulture, out float tempSpeed)) { if (!MathUtils.IsValid(tempSpeed)) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs index 986af5634..84302e52a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs @@ -342,7 +342,7 @@ namespace Barotrauma.Items.Components powerOut?.SendPowerProbeSignal(source, power); } - public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power, float signalStrength = 1.0f) + public override void ReceiveSignal(Signal signal, Connection connection) { if (item.Condition <= 0.0f || connection.IsPower) { return; } if (!connectedRecipients.ContainsKey(connection)) { return; } @@ -351,16 +351,16 @@ namespace Barotrauma.Items.Components { foreach (Connection recipient in connectedRecipients[connection]) { - if (recipient.Item == item || recipient.Item == source) { continue; } + if (recipient.Item == item || recipient.Item == signal.source) { continue; } - source?.LastSentSignalRecipients.Add(recipient.Item); + signal.source?.LastSentSignalRecipients.Add(recipient); foreach (ItemComponent ic in recipient.Item.Components) { //other junction boxes don't need to receive the signal in the pass-through signal connections //because we relay it straight to the connected items without going through the whole chain of junction boxes if (ic is PowerTransfer && !(ic is RelayComponent) && connection.Name.Contains("signal")) { continue; } - ic.ReceiveSignal(stepsTaken, signal, recipient, source, sender, 0.0f, signalStrength); + ic.ReceiveSignal(signal, recipient); } foreach (StatusEffect effect in recipient.Effects) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index d50f09265..57baf9055 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -481,7 +481,7 @@ namespace Barotrauma.Items.Components character.AnimController.UpdateUseItem(false, item.WorldPosition + new Vector2(0.0f, 100.0f) * ((item.Condition / item.MaxCondition) % 0.1f)); } - public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1) + public override void ReceiveSignal(Signal signal, Connection connection) { //do nothing //Repairables should always stay active, so we don't want to use the default behavior diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs index 4cfe3b515..2c5b8ba0e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs @@ -4,7 +4,7 @@ using System.Xml.Linq; namespace Barotrauma.Items.Components { class AndComponent : ItemComponent - { + { protected string output, falseOutput; //an array to keep track of how long ago a non-zero signal was received on both inputs @@ -27,14 +27,41 @@ namespace Barotrauma.Items.Components public string Output { get { return output; } - set { output = value; } + set + { + if (value == null) { return; } + output = value; + if (output.Length > MaxOutputLength && (item.Submarine == null || !item.Submarine.Loading)) + { + output = output.Substring(0, MaxOutputLength); + } + } } [InGameEditable, Serialize("", true, description: "The signal sent when the condition is met (if empty, no signal is sent).", alwaysUseInstanceValues: true)] public string FalseOutput { get { return falseOutput; } - set { falseOutput = value; } + set + { + if (value == null) { return; } + falseOutput = value; + if (falseOutput.Length > MaxOutputLength && (item.Submarine == null || !item.Submarine.Loading)) + { + falseOutput = falseOutput.Substring(0, MaxOutputLength); + } + } + } + + private int maxOutputLength; + [Editable, Serialize(200, false, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")] + public int MaxOutputLength + { + get { return maxOutputLength; } + set + { + maxOutputLength = Math.Max(value, 0); + } } public AndComponent(Item item, XElement element) @@ -56,23 +83,23 @@ namespace Barotrauma.Items.Components string signalOut = sendOutput ? output : falseOutput; if (string.IsNullOrEmpty(signalOut)) return; - item.SendSignal(0, signalOut, "signal_out", null); + item.SendSignal(signalOut, "signal_out"); } - public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f) + public override void ReceiveSignal(Signal signal, Connection connection) { switch (connection.Name) { case "signal_in1": - if (signal == "0") return; + if (signal.value == "0") return; timeSinceReceived[0] = 0.0f; break; case "signal_in2": - if (signal == "0") return; + if (signal.value == "0") return; timeSinceReceived[1] = 0.0f; break; case "set_output": - output = signal; + output = signal.value; break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs index a2705ec91..98c580ec4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs @@ -67,23 +67,23 @@ namespace Barotrauma.Items.Components float output = Calculate(receivedSignal[0], receivedSignal[1]); if (MathUtils.IsValid(output)) { - item.SendSignal(0, MathHelper.Clamp(output, ClampMin, ClampMax).ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + item.SendSignal(MathHelper.Clamp(output, ClampMin, ClampMax).ToString("G", CultureInfo.InvariantCulture), "signal_out"); } } protected abstract float Calculate(float signal1, float signal2); - public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f) + public override void ReceiveSignal(Signal signal, Connection connection) { switch (connection.Name) { case "signal_in1": - float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[0]); + float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[0]); timeSinceReceived[0] = 0.0f; IsActive = true; break; case "signal_in2": - float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[1]); + float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[1]); timeSinceReceived[1] = 0.0f; IsActive = true; break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ColorComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ColorComponent.cs index 55a681c57..e272750a3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ColorComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ColorComponent.cs @@ -1,6 +1,7 @@ using System; using System.Globalization; using System.Xml.Linq; +using Microsoft.Xna.Framework; namespace Barotrauma.Items.Components { @@ -10,6 +11,9 @@ namespace Barotrauma.Items.Components private string output = "0,0,0,0"; + [InGameEditable, Serialize(false, true, description: "When enabled makes the component translate the signal from HSV into RGB where red is the hue between 0 and 360, green is the saturation between 0 and 1 and blue is the value between 0 and 1.", alwaysUseInstanceValues: true)] + public bool UseHSV { get; set; } + public ColorComponent(Item item, XElement element) : base(item, element) { @@ -19,35 +23,48 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - item.SendSignal(0, output, "signal_out", null); + item.SendSignal(output, "signal_out"); } private void UpdateOutput() { - output = receivedSignal[0].ToString("G", CultureInfo.InvariantCulture); - output += "," + receivedSignal[1].ToString("G", CultureInfo.InvariantCulture); - output += "," + receivedSignal[2].ToString("G", CultureInfo.InvariantCulture); - output += "," + receivedSignal[3].ToString("G", CultureInfo.InvariantCulture); + float signalR = receivedSignal[0], + signalG = receivedSignal[1], + signalB = receivedSignal[2], + signalA = receivedSignal[3]; + + if (UseHSV) + { + Color hsvColor = ToolBox.HSVToRGB(signalR, signalG, signalB); + signalR = hsvColor.R / (float) byte.MaxValue; + signalG = hsvColor.G / (float) byte.MaxValue; + signalB = hsvColor.B / (float) byte.MaxValue; + } + + output = signalR.ToString("G", CultureInfo.InvariantCulture); + output += "," + signalG.ToString("G", CultureInfo.InvariantCulture); + output += "," + signalB.ToString("G", CultureInfo.InvariantCulture); + output += "," + signalA.ToString("G", CultureInfo.InvariantCulture); } - public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f) + public override void ReceiveSignal(Signal signal, Connection connection) { switch (connection.Name) { case "signal_r": - float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[0]); + float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[0]); UpdateOutput(); break; case "signal_g": - float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[1]); + float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[1]); UpdateOutput(); break; case "signal_b": - float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[2]); + float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[2]); UpdateOutput(); break; case "signal_a": - float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[3]); + float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[3]); UpdateOutput(); break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs index d40506693..09339aabc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs @@ -1,5 +1,4 @@ -using Microsoft.Xna.Framework; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; @@ -11,7 +10,10 @@ namespace Barotrauma.Items.Components //how many wires can be linked to connectors by default private const int DefaultMaxWires = 5; - //how many wires can be linked to this connection + //how many wires a player can link to this connection + public readonly int MaxPlayerConnectableWires = 5; + + //how many wires can be linked to this connection in total public readonly int MaxWires = 5; public readonly string Name; @@ -81,6 +83,9 @@ namespace Barotrauma.Items.Components item = connectionPanel.Item; MaxWires = element.GetAttributeInt("maxwires", DefaultMaxWires); + MaxWires = Math.Max(element.Elements().Count(e => e.Name.ToString().Equals("link", StringComparison.OrdinalIgnoreCase)), MaxWires); + + MaxPlayerConnectableWires = element.GetAttributeInt("maxplayerconnectablewires", MaxWires); wires = new Wire[MaxWires]; IsOutput = element.Name.ToString() == "output"; @@ -149,19 +154,15 @@ namespace Barotrauma.Items.Components int index = -1; for (int i = 0; i < MaxWires; i++) { - if (wireId[i] < 1) index = i; + if (wireId[i] < 1) { index = i; } } - if (index == -1) break; + if (index == -1) { break; } int id = subElement.GetAttributeInt("w", 0); - if (id < 0) - { - id = 0; - } + if (id < 0) { id = 0; } wireId[index] = idRemap.GetOffsetId(id); break; - case "statuseffect": Effects.Add(StatusEffect.Load(subElement, item.Name + ", connection " + Name)); break; @@ -251,8 +252,8 @@ namespace Barotrauma.Items.Components } } } - - public void SendSignal(int stepsTaken, string signal, Item source, Character sender, float power, float signalStrength = 1.0f) + + public void SendSignal(Signal signal) { for (int i = 0; i < MaxWires; i++) { @@ -260,22 +261,27 @@ namespace Barotrauma.Items.Components Connection recipient = wires[i].OtherConnection(this); if (recipient == null) { continue; } - if (recipient.item == this.item || recipient.item == source) { continue; } + if (recipient.item == this.item || signal.source?.LastSentSignalRecipients.LastOrDefault() == recipient) { continue; } - source?.LastSentSignalRecipients.Add(recipient.item); + signal.source?.LastSentSignalRecipients.Add(recipient); + + Connection connection = recipient; foreach (ItemComponent ic in recipient.item.Components) { - ic.ReceiveSignal(stepsTaken, signal, recipient, source, sender, power, signalStrength); + ic.ReceiveSignal(signal, connection); } - foreach (StatusEffect effect in recipient.Effects) + if (signal.value != "0") { - recipient.Item.ApplyStatusEffect(effect, ActionType.OnUse, (float)Timing.Step); + foreach (StatusEffect effect in recipient.Effects) + { + recipient.Item.ApplyStatusEffect(effect, ActionType.OnUse, (float)Timing.Step); + } } } } - + public void SendPowerProbeSignal(Item source, float power) { for (int i = 0; i < MaxWires; i++) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs index 7cf83ea93..89543f080 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs @@ -65,10 +65,10 @@ namespace Barotrauma.Items.Components } base.IsActive = true; - InitProjSpecific(element); + InitProjSpecific(); } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(); private bool linksInitialized; public override void OnMapLoaded() @@ -352,7 +352,7 @@ namespace Barotrauma.Items.Components #endif } - public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1) + public override void ReceiveSignal(Signal signal, Connection connection) { //do nothing } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs index 2642b0488..a97527ea6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs @@ -16,13 +16,18 @@ namespace Barotrauma.Items.Components [Serialize("", false, translationTextTag: "Label.", description: "The text displayed on this button/tickbox."), Editable] public string Label { get; set; } + [Serialize("1", false, description: "The signal sent out when this button is pressed or this tickbox checked."), Editable] public string Signal { get; set; } public string PropertyName { get; } public bool TargetOnlyParentProperty { get; } + public int NumberInputMin { get; } public int NumberInputMax { get; } + + public int MaxTextLength { get; } + public const int DefaultNumberInputMin = 0, DefaultNumberInputMax = 99; public bool IsIntegerInput { get; } public bool HasPropertyName { get; } @@ -46,7 +51,7 @@ namespace Barotrauma.Items.Components TargetOnlyParentProperty = element.GetAttributeBool("targetonlyparentproperty", false); NumberInputMin = element.GetAttributeInt("min", DefaultNumberInputMin); NumberInputMax = element.GetAttributeInt("max", DefaultNumberInputMax); - + MaxTextLength = element.GetAttributeInt("maxtextlength", int.MaxValue); HasPropertyName = !string.IsNullOrEmpty(PropertyName); IsIntegerInput = HasPropertyName && element.Name.ToString().ToLowerInvariant() == "integerinput"; @@ -244,7 +249,7 @@ namespace Barotrauma.Items.Components if (btnElement == null) return; if (btnElement.Connection != null) { - item.SendSignal(0, btnElement.Signal, btnElement.Connection, sender: null, source: item); + item.SendSignal(new Signal(btnElement.Signal, 0, null, item), btnElement.Connection); } foreach (StatusEffect effect in btnElement.StatusEffects) { @@ -303,7 +308,7 @@ namespace Barotrauma.Items.Components //TODO: allow changing output when a tickbox is not selected if (!string.IsNullOrEmpty(ciElement.Signal) && ciElement.Connection != null) { - item.SendSignal(0, ciElement.State ? ciElement.Signal : "0", ciElement.Connection, sender: null, source: item); + item.SendSignal(new Signal(ciElement.State ? ciElement.Signal : "0", source: item), ciElement.Connection); } foreach (StatusEffect effect in ciElement.StatusEffects) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs index 63842df7d..ba755346b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs @@ -7,17 +7,15 @@ namespace Barotrauma.Items.Components { class DelayedSignal { - public readonly string Signal; - public readonly float SignalStrength; + public readonly Signal Signal; //in number of frames public int SendTimer; //in number of frames public int SendDuration; - public DelayedSignal(string signal, float signalStrength, int sendTimer) + public DelayedSignal(Signal signal, int sendTimer) { Signal = signal; - SignalStrength = signalStrength; SendTimer = sendTimer; } } @@ -75,34 +73,34 @@ namespace Barotrauma.Items.Components { var signalOut = signalQueue.Peek(); signalOut.SendDuration -= 1; - item.SendSignal(0, signalOut.Signal, "signal_out", null, signalStrength: signalOut.SignalStrength); + item.SendSignal(new Signal(signalOut.Signal.value, strength: signalOut.Signal.strength), "signal_out"); if (signalOut.SendDuration <= 0) { signalQueue.Dequeue(); } else { break; } } } - public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f) + public override void ReceiveSignal(Signal signal, Connection connection) { switch (connection.Name) { case "signal_in": if (signalQueue.Count >= signalQueueSize) { return; } if (ResetWhenSignalReceived) { prevQueuedSignal = null; signalQueue.Clear(); } - if (ResetWhenDifferentSignalReceived && signalQueue.Count > 0 && signalQueue.Peek().Signal != signal) + if (ResetWhenDifferentSignalReceived && signalQueue.Count > 0 && signalQueue.Peek().Signal.value != signal.value) { prevQueuedSignal = null; signalQueue.Clear(); } if (prevQueuedSignal != null && - prevQueuedSignal.Signal == signal && - MathUtils.NearlyEqual(prevQueuedSignal.SignalStrength, signalStrength) && + prevQueuedSignal.Signal.value == signal.value && + MathUtils.NearlyEqual(prevQueuedSignal.Signal.strength, signal.strength) && ((prevQueuedSignal.SendTimer + prevQueuedSignal.SendDuration == delayTicks) || (prevQueuedSignal.SendTimer <= 0 && prevQueuedSignal.SendDuration > 0))) { prevQueuedSignal.SendDuration += 1; return; } - prevQueuedSignal = new DelayedSignal(signal, signalStrength, delayTicks) + prevQueuedSignal = new DelayedSignal(signal, delayTicks) { SendDuration = 1 }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs index 6ba37f496..48296b87b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs @@ -15,18 +15,45 @@ namespace Barotrauma.Items.Components //the output is sent if both inputs have received a signal within the timeframe protected float timeFrame; - [InGameEditable, Serialize("1", true, description: "The signal this item outputs when the condition is met.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("1", true, description: "The signal sent when the condition is met.", alwaysUseInstanceValues: true)] public string Output { get { return output; } - set { output = value; } + set + { + if (value == null) { return; } + output = value; + if (output.Length > MaxOutputLength && (item.Submarine == null || !item.Submarine.Loading)) + { + output = output.Substring(0, MaxOutputLength); + } + } } - [InGameEditable, Serialize("", true, description: "The signal this item outputs when the condition is not met.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("", true, description: "The signal sent when the condition is met (if empty, no signal is sent).", alwaysUseInstanceValues: true)] public string FalseOutput { get { return falseOutput; } - set { falseOutput = value; } + set + { + if (value == null) { return; } + falseOutput = value; + if (falseOutput.Length > MaxOutputLength && (item.Submarine == null || !item.Submarine.Loading)) + { + falseOutput = falseOutput.Substring(0, MaxOutputLength); + } + } + } + + private int maxOutputLength; + [Editable, Serialize(200, false, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")] + public int MaxOutputLength + { + get { return maxOutputLength; } + set + { + maxOutputLength = Math.Max(value, 0); + } } [InGameEditable(DecimalCount = 2), Serialize(0.0f, true, description: "The maximum amount of time between the received signals. If set to 0, the signals must be received at the same time.", alwaysUseInstanceValues: true)] @@ -61,20 +88,20 @@ namespace Barotrauma.Items.Components string signalOut = receivedSignal[0] == receivedSignal[1] ? output : falseOutput; if (string.IsNullOrEmpty(signalOut)) return; - item.SendSignal(0, signalOut, "signal_out", null); + item.SendSignal(signalOut, "signal_out"); } } - public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f) + public override void ReceiveSignal(Signal signal, Connection connection) { switch (connection.Name) { case "signal_in1": - receivedSignal[0] = signal; + receivedSignal[0] = signal.value; timeSinceReceived[0] = 0.0f; break; case "signal_in2": - receivedSignal[1] = signal; + receivedSignal[1] = signal.value; timeSinceReceived[1] = 0.0f; break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ExponentiationComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ExponentiationComponent.cs index 4d8fba217..8ea0ca87b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ExponentiationComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ExponentiationComponent.cs @@ -25,17 +25,18 @@ namespace Barotrauma.Items.Components IsActive = true; } - public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1) + public override void ReceiveSignal(Signal signal, Connection connection) { switch (connection.Name) { case "set_exponent": case "exponent": - float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out exponent); + float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out exponent); break; case "signal_in": - float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float value); - item.SendSignal(0, MathUtils.Pow(value, Exponent).ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float value); + signal.value = MathUtils.Pow(value, Exponent).ToString("G", CultureInfo.InvariantCulture); + item.SendSignal(signal, "signal_out"); break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/FunctionComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/FunctionComponent.cs index e1f02b23f..5d2a76a86 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/FunctionComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/FunctionComponent.cs @@ -28,20 +28,20 @@ namespace Barotrauma.Items.Components IsActive = true; } - public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1) + public override void ReceiveSignal(Signal signal, Connection connection) { - if (connection.Name != "signal_in") return; - if (!float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float value)) return; + if (connection.Name != "signal_in") { return; } + if (!float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float value)) { return; } switch (Function) { case FunctionType.Round: - item.SendSignal(0, Math.Round(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + value = MathF.Round(value); break; case FunctionType.Ceil: - item.SendSignal(0, Math.Ceiling(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + value = MathF.Ceiling(value); break; case FunctionType.Floor: - item.SendSignal(0, Math.Floor(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + value = MathF.Floor(value); break; case FunctionType.Factorial: int intVal = (int)Math.Min(value, 20); @@ -50,20 +50,24 @@ namespace Barotrauma.Items.Components { factorial *= (ulong)i; } - item.SendSignal(0, factorial.ToString(), "signal_out", null); + value = factorial; break; case FunctionType.AbsoluteValue: - item.SendSignal(0, Math.Abs(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + value = MathF.Abs(value); break; case FunctionType.SquareRoot: - if (value > 0) + if (value < 0) { - item.SendSignal(0, Math.Sqrt(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + return; } + value = MathF.Sqrt(value); break; default: throw new NotImplementedException($"Function {Function} has not been implemented."); } + + signal.value = value.ToString("G", CultureInfo.InvariantCulture); + item.SendSignal(signal, "signal_out"); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/GreaterComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/GreaterComponent.cs index 25794cb16..fa8109691 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/GreaterComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/GreaterComponent.cs @@ -27,13 +27,13 @@ namespace Barotrauma.Items.Components string signalOut = val1 > val2 ? output : falseOutput; if (string.IsNullOrEmpty(signalOut)) return; - item.SendSignal(0, signalOut, "signal_out", null); + item.SendSignal(signalOut, "signal_out"); } } - public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f) + public override void ReceiveSignal(Signal signal, Connection connection) { - base.ReceiveSignal(stepsTaken, signal, connection, source, sender, power, signalStrength); + base.ReceiveSignal(signal, connection); float.TryParse(receivedSignal[0], NumberStyles.Float, CultureInfo.InvariantCulture, out val1); float.TryParse(receivedSignal[1], NumberStyles.Float, CultureInfo.InvariantCulture, out val2); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index d2d0920e7..cab1434f4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -308,12 +308,12 @@ namespace Barotrauma.Items.Components partial void OnStateChanged(); - public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f) + public override void ReceiveSignal(Signal signal, Connection connection) { switch (connection.Name) { case "toggle": - if (signal != "0") + if (signal.value != "0") { if (!IgnoreContinuousToggle || lastToggleSignalTime < Timing.TotalTime - 0.1) { @@ -323,10 +323,10 @@ namespace Barotrauma.Items.Components } break; case "set_state": - IsOn = signal != "0"; + IsOn = signal.value != "0"; break; case "set_color": - LightColor = XMLExtensions.ParseColor(signal, false); + LightColor = XMLExtensions.ParseColor(signal.value, false); break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MemoryComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MemoryComponent.cs index ea33ee0d0..33a769f92 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MemoryComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MemoryComponent.cs @@ -1,13 +1,11 @@ using Barotrauma.Networking; +using System; using System.Xml.Linq; namespace Barotrauma.Items.Components { partial class MemoryComponent : ItemComponent, IServerSerializable { - const int MaxValueLength = ChatMessage.MaxLength; - - private string value; [InGameEditable, Serialize("", true, description: "The currently stored signal the item outputs.", alwaysUseInstanceValues: true)] @@ -17,10 +15,25 @@ namespace Barotrauma.Items.Components set { if (value == null) { return; } - this.value = value.Length <= MaxValueLength ? value : value.Substring(0, MaxValueLength); + this.value = value; + if (this.value.Length > MaxValueLength && (item.Submarine == null || !item.Submarine.Loading)) + { + this.value = this.value.Substring(0, MaxValueLength); + } } } - + + private int maxValueLength; + [Editable, Serialize(200, false, description: "The maximum length of the stored value. Warning: Large values can lead to large memory usage or networking issues.")] + public int MaxValueLength + { + get { return maxValueLength; } + set + { + maxValueLength = Math.Max(value, 0); + } + } + protected bool writeable = true; public MemoryComponent(Item item, XElement element) @@ -31,26 +44,29 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - item.SendSignal(0, Value, "signal_out", null); + item.SendSignal(Value, "signal_out"); } partial void OnStateChanged(); - public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f) + public override void ReceiveSignal(Signal signal, Connection connection) { switch (connection.Name) { case "signal_in": if (writeable) { - if (Value == signal) { return; } - Value = signal; - OnStateChanged(); + string prevValue = Value; + Value = signal.value; + if (Value != prevValue) + { + OnStateChanged(); + } } break; case "signal_store": case "lock_state": - writeable = signal == "1"; + writeable = signal.value == "1"; break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ModuloComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ModuloComponent.cs index a66276988..2d8985857 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ModuloComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ModuloComponent.cs @@ -21,18 +21,19 @@ namespace Barotrauma.Items.Components IsActive = true; } - public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1) + public override void ReceiveSignal(Signal signal, Connection connection) { switch (connection.Name) { case "set_modulus": case "modulus": - float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float newModulus); + float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float newModulus); Modulus = newModulus; break; case "signal_in": - float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float value); - item.SendSignal(0, (value % modulus).ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float value); + signal.value = (value % modulus).ToString("G", CultureInfo.InvariantCulture); + item.SendSignal(signal, "signal_out"); break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs index 99eff26f6..53e7374c2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs @@ -74,11 +74,48 @@ namespace Barotrauma.Items.Components } } + private string output; [InGameEditable, Serialize("1", true, description: "The signal the item outputs when it has detected movement.", alwaysUseInstanceValues: true)] - public string Output { get; set; } + public string Output + { + get { return output; } + set + { + if (value == null) { return; } + output = value; + if (output.Length > MaxOutputLength && (item.Submarine == null || !item.Submarine.Loading)) + { + output = output.Substring(0, MaxOutputLength); + } + } + } + private string falseOutput; [InGameEditable, Serialize("", true, description: "The signal the item outputs when it has not detected movement.", alwaysUseInstanceValues: true)] - public string FalseOutput { get; set; } + public string FalseOutput + { + get { return falseOutput; } + set + { + if (value == null) { return; } + falseOutput = value; + if (falseOutput.Length > MaxOutputLength && (item.Submarine == null || !item.Submarine.Loading)) + { + falseOutput = falseOutput.Substring(0, MaxOutputLength); + } + } + } + + private int maxOutputLength; + [Editable, Serialize(200, false, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")] + public int MaxOutputLength + { + get { return maxOutputLength; } + set + { + maxOutputLength = Math.Max(value, 0); + } + } [Editable(DecimalCount = 3), Serialize(0.01f, true, description: "How fast the objects within the detector's range have to be moving (in m/s).", alwaysUseInstanceValues: true)] public float MinimumVelocity @@ -113,7 +150,7 @@ namespace Barotrauma.Items.Components { string signalOut = MotionDetected ? Output : FalseOutput; - if (!string.IsNullOrEmpty(signalOut)) item.SendSignal(1, signalOut, "state_out", null); + if (!string.IsNullOrEmpty(signalOut)) { item.SendSignal(new Signal(signalOut, 1), "state_out"); } updateTimer -= deltaTime; if (updateTimer > 0.0f) return; @@ -138,6 +175,10 @@ namespace Barotrauma.Items.Components { if (IgnoreDead && c.IsDead) { continue; } + //ignore characters that have spawned a second or less ago + //makes it possible to detect when a spawned character moves without triggering the detector immediately as the ragdoll spawns and drops to the ground + if (c.SpawnTime > Timing.TotalTime - 1.0) { continue; } + switch (Target) { case TargetType.Human: diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/NotComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/NotComponent.cs index a5758ce30..99e3cc3e2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/NotComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/NotComponent.cs @@ -24,15 +24,18 @@ namespace Barotrauma.Items.Components base.Update(deltaTime, cam); if (!signalReceived) { - item.SendSignal(0, "1", "signal_out", null, 0.0f); + item.SendSignal("1", "signal_out"); } signalReceived = false; } - public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f) + public override void ReceiveSignal(Signal signal, Connection connection) { if (connection.Name != "signal_in") { return; } - item.SendSignal(stepsTaken, signal == "0" || signal == string.Empty ? "1" : "0", "signal_out", sender, 0.0f, source, signalStrength); + + signal.value = signal.value == "0" || string.IsNullOrEmpty(signal.value) ? "1" : "0"; + signal.power = 0.0f; + item.SendSignal(signal, "signal_out"); signalReceived = true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OrComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OrComponent.cs index a9afe8e22..d0661dc9c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OrComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OrComponent.cs @@ -22,7 +22,7 @@ namespace Barotrauma.Items.Components string signalOut = sendOutput ? output : falseOutput; if (string.IsNullOrEmpty(signalOut)) return; - item.SendSignal(0, signalOut, "signal_out", null); + item.SendSignal(signalOut, "signal_out"); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OscillatorComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OscillatorComponent.cs index caacada54..309bfee73 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OscillatorComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OscillatorComponent.cs @@ -59,29 +59,29 @@ namespace Barotrauma.Items.Components float pulseInterval = 1.0f / frequency; while (phase >= pulseInterval) { - item.SendSignal(0, "1", "signal_out", null); + item.SendSignal("1", "signal_out"); phase -= pulseInterval; } break; case WaveType.Square: phase = (phase + deltaTime * frequency) % 1.0f; - item.SendSignal(0, phase < 0.5f ? "0" : "1", "signal_out", null); + item.SendSignal(phase < 0.5f ? "0" : "1", "signal_out"); break; case WaveType.Sine: phase = (phase + deltaTime * frequency) % 1.0f; - item.SendSignal(0, Math.Sin(phase * MathHelper.TwoPi).ToString(CultureInfo.InvariantCulture), "signal_out", null); + item.SendSignal(Math.Sin(phase * MathHelper.TwoPi).ToString(CultureInfo.InvariantCulture), "signal_out"); break; } } - public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f) + public override void ReceiveSignal(Signal signal, Connection connection) { switch (connection.Name) { case "set_frequency": case "frequency_in": float newFrequency; - if (float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out newFrequency)) + if (float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out newFrequency)) { Frequency = newFrequency; } @@ -90,7 +90,7 @@ namespace Barotrauma.Items.Components case "set_outputtype": case "set_wavetype": WaveType newOutputType; - if (Enum.TryParse(signal, out newOutputType)) + if (Enum.TryParse(signal.value, out newOutputType)) { OutputType = newOutputType; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OxygenDetector.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OxygenDetector.cs index 431b23f72..4724dd56c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OxygenDetector.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OxygenDetector.cs @@ -14,7 +14,7 @@ namespace Barotrauma.Items.Components { if (item.CurrentHull == null) return; - item.SendSignal(0, ((int)item.CurrentHull.OxygenPercentage).ToString(), "signal_out", null); + item.SendSignal(((int)item.CurrentHull.OxygenPercentage).ToString(), "signal_out"); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs index fb0b0ca46..cf86b139c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs @@ -1,4 +1,5 @@ -using System.Text.RegularExpressions; +using System; +using System.Text.RegularExpressions; using System.Xml.Linq; namespace Barotrauma.Items.Components @@ -17,8 +18,22 @@ namespace Barotrauma.Items.Components private bool nonContinuousOutputSent; + private string output; + [InGameEditable, Serialize("1", true, description: "The signal this item outputs when the received signal matches the regular expression.", alwaysUseInstanceValues: true)] - public string Output { get; set; } + public string Output + { + get { return output; } + set + { + if (value == null) { return; } + output = value; + if (output.Length > MaxOutputLength && (item.Submarine == null || !item.Submarine.Loading)) + { + output = output.Substring(0, MaxOutputLength); + } + } + } [InGameEditable, Serialize(false, true, description: "Should the component output a value of a capture group instead of a constant signal.", alwaysUseInstanceValues: true)] public bool UseCaptureGroup { get; set; } @@ -46,12 +61,23 @@ namespace Barotrauma.Items.Components catch { - item.SendSignal(0, "ERROR", "signal_out", null); + item.SendSignal("ERROR", "signal_out"); return; } } } + private int maxOutputLength; + [Editable, Serialize(200, false, description: "The maximum length of the output string. Warning: Large values can lead to large memory usage or networking issues.")] + public int MaxOutputLength + { + get { return maxOutputLength; } + set + { + maxOutputLength = Math.Max(value, 0); + } + } + public RegExFindComponent(Item item, XElement element) : base(item, element) { @@ -74,7 +100,7 @@ namespace Barotrauma.Items.Components } catch { - item.SendSignal(0, "ERROR", "signal_out", null); + item.SendSignal("ERROR", "signal_out"); previousResult = false; return; } @@ -106,25 +132,25 @@ namespace Barotrauma.Items.Components if (ContinuousOutput) { - if (!string.IsNullOrEmpty(signalOut)) { item.SendSignal(0, signalOut, "signal_out", null); } + if (!string.IsNullOrEmpty(signalOut)) { item.SendSignal(signalOut, "signal_out"); } } else if (!nonContinuousOutputSent) { - if (!string.IsNullOrEmpty(signalOut)) { item.SendSignal(0, signalOut, "signal_out", null); } + if (!string.IsNullOrEmpty(signalOut)) { item.SendSignal(signalOut, "signal_out"); } nonContinuousOutputSent = true; } } - public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f) + public override void ReceiveSignal(Signal signal, Connection connection) { switch (connection.Name) { case "signal_in": - receivedSignal = signal; + receivedSignal = signal.value; nonContinuousOutputSent = false; break; case "set_output": - Output = signal; + Output = signal.value; break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs index 8cbd60429..138277cee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs @@ -86,7 +86,7 @@ namespace Barotrauma.Items.Components { RefreshConnections(); - item.SendSignal(0, IsOn ? "1" : "0", "state_out", null); + item.SendSignal(IsOn ? "1" : "0", "state_out"); if (!CanTransfer) { Voltage = 0.0f; return; } @@ -169,23 +169,23 @@ namespace Barotrauma.Items.Components } - public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f) + public override void ReceiveSignal(Signal signal, Connection connection) { if (item.Condition <= 0.0f || connection.IsPower) { return; } if (connectionPairs.TryGetValue(connection.Name, out string outConnection)) { if (!IsOn) { return; } - item.SendSignal(stepsTaken, signal, outConnection, sender, power, source, signalStrength); + item.SendSignal(signal, outConnection); } else if (connection.Name == "toggle") { - if (signal == "0") { return; } + if (signal.value == "0") { return; } SetState(!IsOn, false); } else if (connection.Name == "set_state") { - SetState(signal != "0", false); + SetState(signal.value != "0", false); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Signal.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Signal.cs new file mode 100644 index 000000000..c3c007b44 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Signal.cs @@ -0,0 +1,23 @@ +namespace Barotrauma.Items.Components +{ + public struct Signal + { + internal string value; + internal int stepsTaken; + internal Character sender; + internal Item source; + internal float power; + internal float strength; + + internal Signal(string value, int stepsTaken = 0, Character sender = null, + Item source = null, float power = 0.0f, float strength = 1.0f) + { + this.value = value; + this.stepsTaken = stepsTaken; + this.sender = sender; + this.source = source; + this.power = power; + this.strength = strength; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SignalCheckComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SignalCheckComponent.cs index eb725383d..3a0ce8ba1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SignalCheckComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SignalCheckComponent.cs @@ -1,38 +1,76 @@ -using System.Xml.Linq; +using System; +using System.Xml.Linq; namespace Barotrauma.Items.Components { class SignalCheckComponent : ItemComponent { + private string output; [InGameEditable, Serialize("1", true, description: "The signal this item outputs when the received signal matches the target signal.", alwaysUseInstanceValues: true)] - public string Output { get; set; } + public string Output + { + get { return output; } + set + { + if (value == null) { return; } + output = value; + if (output.Length > MaxOutputLength && (item.Submarine == null || !item.Submarine.Loading)) + { + output = output.Substring(0, MaxOutputLength); + } + } + } + + private string falseOutput; [InGameEditable, Serialize("0", true, description: "The signal this item outputs when the received signal does not match the target signal.", alwaysUseInstanceValues: true)] - public string FalseOutput { get; set; } + public string FalseOutput + { + get { return falseOutput; } + set + { + if (value == null) { return; } + falseOutput = value; + if (falseOutput.Length > MaxOutputLength && (item.Submarine == null || !item.Submarine.Loading)) + { + falseOutput = falseOutput.Substring(0, MaxOutputLength); + } + } + } [InGameEditable, Serialize("", true, description: "The value to compare the received signals against.", alwaysUseInstanceValues: true)] public string TargetSignal { get; set; } + private int maxOutputLength; + [Editable, Serialize(200, false, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")] + public int MaxOutputLength + { + get { return maxOutputLength; } + set + { + maxOutputLength = Math.Max(value, 0); + } + } + public SignalCheckComponent(Item item, XElement element) : base(item, element) { } - public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f) + public override void ReceiveSignal(Signal signal, Connection connection) { switch (connection.Name) { case "signal_in": - string signalOut = (signal == TargetSignal) ? Output : FalseOutput; - - if (string.IsNullOrWhiteSpace(signalOut)) return; - item.SendSignal(stepsTaken, signalOut, "signal_out", sender, signalStrength); - + string signalOut = (signal.value == TargetSignal) ? Output : FalseOutput; + if (string.IsNullOrEmpty(signalOut)) { return; } + signal.value = signalOut; + item.SendSignal(signal, "signal_out"); break; case "set_output": - Output = signal; + Output = signal.value; break; case "set_targetsignal": - TargetSignal = signal; + TargetSignal = signal.value; break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SmokeDetector.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SmokeDetector.cs index 7f71626c7..e0d4a3a38 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SmokeDetector.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SmokeDetector.cs @@ -1,4 +1,5 @@ -using System.Xml.Linq; +using System; +using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -9,11 +10,48 @@ namespace Barotrauma.Items.Components private bool fireInRange; - [InGameEditable, Serialize("1", true, description: "The signal the item outputs when it has detected movement.", alwaysUseInstanceValues: true)] - public string Output { get; set; } + private string output; + [InGameEditable, Serialize("1", true, description: "The signal the item outputs when it has detected a fire.", alwaysUseInstanceValues: true)] + public string Output + { + get { return output; } + set + { + if (value == null) { return; } + output = value; + if (output.Length > MaxOutputLength && (item.Submarine == null || !item.Submarine.Loading)) + { + output = output.Substring(0, MaxOutputLength); + } + } + } - [InGameEditable, Serialize("0", true, description: "The signal the item outputs when it has not detected movement.", alwaysUseInstanceValues: true)] - public string FalseOutput { get; set; } + private string falseOutput; + [InGameEditable, Serialize("0", true, description: "The signal the item outputs when it has not detected a fire.", alwaysUseInstanceValues: true)] + public string FalseOutput + { + get { return falseOutput; } + set + { + if (value == null) { return; } + falseOutput = value; + if (falseOutput.Length > MaxOutputLength && (item.Submarine == null || !item.Submarine.Loading)) + { + falseOutput = falseOutput.Substring(0, MaxOutputLength); + } + } + } + + private int maxOutputLength; + [Editable, Serialize(200, false, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")] + public int MaxOutputLength + { + get { return maxOutputLength; } + set + { + maxOutputLength = Math.Max(value, 0); + } + } public SmokeDetector(Item item, XElement element) : base(item, element) @@ -45,7 +83,8 @@ namespace Barotrauma.Items.Components fireInRange = IsFireInRange(); fireCheckTimer = FireCheckInterval; } - item.SendSignal(0, fireInRange ? Output : FalseOutput, "signal_out", null); + string signalOut = fireInRange ? Output : FalseOutput; + if (!string.IsNullOrEmpty(signalOut)) { item.SendSignal(signalOut, "signal_out"); } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/StringComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/StringComponent.cs index 875705036..45189882f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/StringComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/StringComponent.cs @@ -37,32 +37,35 @@ namespace Barotrauma.Items.Components sealed public override void Update(float deltaTime, Camera cam) { + bool deactivate = true; + bool earlyReturn = false; for (int i = 0; i < timeSinceReceived.Length; i++) { - if (timeSinceReceived[i] > timeFrame) - { - IsActive = false; - return; - } + deactivate &= timeSinceReceived[i] > timeFrame; + earlyReturn |= timeSinceReceived[i] > timeFrame; timeSinceReceived[i] += deltaTime; } + // only stop Update() if both signals timed-out. if IsActive == false, then the component stops updating. + IsActive = !deactivate; + // early return if either of the signal timed-out + if (earlyReturn) { return; } string output = Calculate(receivedSignal[0], receivedSignal[1]); - item.SendSignal(0, output, "signal_out", null); + item.SendSignal(output, "signal_out"); } protected abstract string Calculate(string signal1, string signal2); - public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f) + public override void ReceiveSignal(Signal signal, Connection connection) { switch (connection.Name) { case "signal_in1": - receivedSignal[0] = signal; + receivedSignal[0] = signal.value; timeSinceReceived[0] = 0.0f; IsActive = true; break; case "signal_in2": - receivedSignal[1] = signal; + receivedSignal[1] = signal.value; timeSinceReceived[1] = 0.0f; IsActive = true; break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs index 981770226..fb93543ab 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs @@ -1,5 +1,6 @@ using Barotrauma.Networking; using System.Collections.Generic; +using System.Linq; using System.Xml.Linq; namespace Barotrauma.Items.Components @@ -31,6 +32,19 @@ namespace Barotrauma.Items.Components } } + /// + /// Can be used to display messages on the terminal via status effects + /// + public string ShowMessage + { + get { return messageHistory.Count == 0 ? string.Empty : messageHistory.Last(); } + set + { + if (string.IsNullOrEmpty(value)) { return; } + ShowOnDisplay(value); + } + } + private string OutputValue { get; set; } public Terminal(Item item, XElement element) @@ -42,29 +56,34 @@ namespace Barotrauma.Items.Components partial void InitProjSpecific(XElement element); - partial void ShowOnDisplay(string input); + partial void ShowOnDisplay(string input, bool addToHistory = true); - public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1) + public override void ReceiveSignal(Signal signal, Connection connection) { if (connection.Name != "signal_in") { return; } - if (signal.Length > MaxMessageLength) + if (signal.value.Length > MaxMessageLength) { - signal = signal.Substring(0, MaxMessageLength); + signal.value = signal.value.Substring(0, MaxMessageLength); } - string inputSignal = signal.Replace("\\n", "\n"); + string inputSignal = signal.value.Replace("\\n", "\n"); ShowOnDisplay(inputSignal); } public override void OnItemLoaded() { + bool isSubEditor = false; +#if CLIENT + isSubEditor = Screen.Selected != GameMain.SubEditorScreen || GameMain.GameSession?.GameMode is TestGameMode; +#endif + base.OnItemLoaded(); if (!string.IsNullOrEmpty(DisplayedWelcomeMessage)) { - ShowOnDisplay(DisplayedWelcomeMessage); + ShowOnDisplay(DisplayedWelcomeMessage, addToHistory: !isSubEditor); DisplayedWelcomeMessage = ""; //remove welcome message if a game session is running so it doesn't reappear on successive rounds - if (GameMain.GameSession != null) + if (GameMain.GameSession != null && !isSubEditor) { welcomeMessage = null; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs index 53251fdf4..8588d9bdd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs @@ -56,71 +56,74 @@ namespace Barotrauma.Items.Components { float angle = (float)Math.Atan2(receivedSignal[1], receivedSignal[0]); if (!UseRadians) { angle = MathHelper.ToDegrees(angle); } - item.SendSignal(0, angle.ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + item.SendSignal(angle.ToString("G", CultureInfo.InvariantCulture), "signal_out"); } } } - public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1) + public override void ReceiveSignal(Signal signal, Connection connection) { - float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float value); + float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float value); switch (Function) { case FunctionType.Sin: if (!UseRadians) { value = MathHelper.ToRadians(value); } - item.SendSignal(0, ((float)Math.Sin(value)).ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + value = MathF.Sin(value); break; case FunctionType.Cos: if (!UseRadians) { value = MathHelper.ToRadians(value); } - item.SendSignal(0, ((float)Math.Cos(value)).ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + value = MathF.Cos(value); break; case FunctionType.Tan: if (!UseRadians) { value = MathHelper.ToRadians(value); } //tan is undefined if the value is (π / 2) + πk, where k is any integer if (!MathUtils.NearlyEqual(value % MathHelper.Pi, MathHelper.PiOver2)) { - item.SendSignal(0, ((float)Math.Tan(value)).ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + value = MathF.Tan(value); } break; case FunctionType.Asin: //asin is only defined in the range [-1,1] if (value >= -1.0f && value <= 1.0f) { - float angle = (float)Math.Asin(value); + float angle = MathF.Asin(value); if (!UseRadians) { angle = MathHelper.ToDegrees(angle); } - item.SendSignal(0, angle.ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + value = angle; } break; case FunctionType.Acos: //acos is only defined in the range [-1,1] if (value >= -1.0f && value <= 1.0f) { - float angle = (float)Math.Acos(value); + float angle = MathF.Acos(value); if (!UseRadians) { angle = MathHelper.ToDegrees(angle); } - item.SendSignal(0, angle.ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + value = angle; } break; case FunctionType.Atan: if (connection.Name == "signal_in_x") { timeSinceReceived[0] = 0.0f; - float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[0]); + float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[0]); } else if (connection.Name == "signal_in_y") { timeSinceReceived[1] = 0.0f; - float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[1]); + float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[1]); } else { - float angle = (float)Math.Atan(value); + float angle = MathF.Atan(value); if (!UseRadians) { angle = MathHelper.ToDegrees(angle); } - item.SendSignal(0, angle.ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + value = angle; } break; default: throw new NotImplementedException($"Function {Function} has not been implemented."); } + + signal.value = value.ToString("G", CultureInfo.InvariantCulture); + item.SendSignal(signal, "signal_out"); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs index 3ae59f88d..97ee74d54 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs @@ -12,11 +12,48 @@ namespace Barotrauma.Items.Components private bool isInWater; private float stateSwitchDelay; + private string output; [InGameEditable, Serialize("1", true, description: "The signal the item sends out when it's underwater.", alwaysUseInstanceValues: true)] - public string Output { get; set; } + public string Output + { + get { return output; } + set + { + if (value == null) { return; } + output = value; + if (output.Length > MaxOutputLength && (item.Submarine == null || !item.Submarine.Loading)) + { + output = output.Substring(0, MaxOutputLength); + } + } + } + private string falseOutput; [InGameEditable, Serialize("0", true, description: "The signal the item sends out when it's not underwater.", alwaysUseInstanceValues: true)] - public string FalseOutput { get; set; } + public string FalseOutput + { + get { return falseOutput; } + set + { + if (value == null) { return; } + falseOutput = value; + if (falseOutput.Length > MaxOutputLength && (item.Submarine == null || !item.Submarine.Loading)) + { + falseOutput = falseOutput.Substring(0, MaxOutputLength); + } + } + } + + private int maxOutputLength; + [Editable, Serialize(200, false, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")] + public int MaxOutputLength + { + get { return maxOutputLength; } + set + { + maxOutputLength = Math.Max(value, 0); + } + } public WaterDetector(Item item, XElement element) : base(item, element) @@ -59,13 +96,13 @@ namespace Barotrauma.Items.Components string signalOut = isInWater ? Output : FalseOutput; if (!string.IsNullOrEmpty(signalOut)) { - item.SendSignal(0, signalOut, "signal_out", null); + item.SendSignal(signalOut, "signal_out"); } if (item.CurrentHull != null) { int waterPercentage = MathHelper.Clamp((int)Math.Round(item.CurrentHull.WaterPercentage), 0, 100); - item.SendSignal(0, waterPercentage.ToString(), "water_%", null); + item.SendSignal(waterPercentage.ToString(), "water_%"); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs index 6730cdf94..6ca44bef8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs @@ -152,9 +152,9 @@ namespace Barotrauma.Items.Components channelMemory[index] = MathHelper.Clamp(value, 0, 10000); } - public void TransmitSignal(int stepsTaken, string signal, Item source, Character sender, bool sentFromChat, float signalStrength = 1.0f) + public void TransmitSignal(Signal signal, bool sentFromChat) { - var senderComponent = source?.GetComponent(); + var senderComponent = signal.source?.GetComponent(); if (senderComponent != null && !CanReceive(senderComponent)) { return; } bool chatMsgSent = false; @@ -165,22 +165,24 @@ namespace Barotrauma.Items.Components if (sentFromChat && !wifiComp.LinkToChat) { continue; } //signal strength diminishes by distance - float sentSignalStrength = signalStrength * + float sentSignalStrength = signal.strength * MathHelper.Clamp(1.0f - (Vector2.Distance(item.WorldPosition, wifiComp.item.WorldPosition) / wifiComp.range), 0.0f, 1.0f); - wifiComp.item.SendSignal(stepsTaken, signal, "signal_out", sender, 0, source, sentSignalStrength); + Signal s = new Signal(signal.value, signal.stepsTaken, sender: signal.sender, source: signal.source, + power: 0.0f, strength: sentSignalStrength); + wifiComp.item.SendSignal(s, "signal_out"); - if (source != null) + if (signal.source != null) { - foreach (Item receiverItem in wifiComp.item.LastSentSignalRecipients) + foreach (Connection receiver in wifiComp.item.LastSentSignalRecipients) { - if (!source.LastSentSignalRecipients.Contains(receiverItem)) + if (!signal.source.LastSentSignalRecipients.Contains(receiver)) { - source.LastSentSignalRecipients.Add(receiverItem); + signal.source.LastSentSignalRecipients.Add(receiver); } } } - if (DiscardDuplicateChatMessages && signal == prevSignal) { continue; } + if (DiscardDuplicateChatMessages && signal.value == prevSignal) { continue; } //create a chat message if (LinkToChat && wifiComp.LinkToChat && chatMsgCooldown <= 0.0f && !sentFromChat) @@ -188,7 +190,7 @@ namespace Barotrauma.Items.Components if (wifiComp.item.ParentInventory != null && wifiComp.item.ParentInventory.Owner != null) { - string chatMsg = signal; + string chatMsg = signal.value; if (senderComponent != null) { chatMsg = ChatMessage.ApplyDistanceEffect(chatMsg, 1.0f - sentSignalStrength); @@ -201,7 +203,7 @@ namespace Barotrauma.Items.Components { if (GameMain.Client == null) { - GameMain.GameSession?.CrewManager?.AddSinglePlayerChatMessage(source?.Name ?? "", signal, ChatMessageType.Radio, sender: null); + GameMain.GameSession?.CrewManager?.AddSinglePlayerChatMessage(signal.source?.Name ?? "", signal.value, ChatMessageType.Radio, sender: null); } } #elif SERVER @@ -211,7 +213,7 @@ namespace Barotrauma.Items.Components if (recipientClient != null) { GameMain.Server.SendDirectChatMessage( - ChatMessage.Create(source?.Name ?? "", chatMsg, ChatMessageType.Radio, null), recipientClient); + ChatMessage.Create(signal.source?.Name ?? "", chatMsg, ChatMessageType.Radio, null), recipientClient); } } #endif @@ -225,26 +227,26 @@ namespace Barotrauma.Items.Components IsActive = true; } - prevSignal = signal; + prevSignal = signal.value; } - public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f) + public override void ReceiveSignal(Signal signal, Connection connection) { if (connection == null) { return; } switch (connection.Name) { case "signal_in": - TransmitSignal(stepsTaken, signal, source, sender, false, signalStrength); + TransmitSignal(signal, false); break; case "set_channel": - if (int.TryParse(signal, out int newChannel)) + if (int.TryParse(signal.value, out int newChannel)) { Channel = newChannel; } break; case "set_range": - if (float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float newRange)) + if (float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float newRange)) { Range = newRange; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs index af86d2973..d0cce2fa9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs @@ -37,11 +37,6 @@ namespace Barotrauma.Items.Components angle = MathUtils.VectorToAngle(end - start); length = Vector2.Distance(start, end); - - if (length > 5000.0f) - { - int akjsdnfkjsadf = 1; - } } } @@ -100,6 +95,20 @@ namespace Barotrauma.Items.Components set; } + [Editable, Serialize(false, true, "If enabled, this wire will be ignored by the \"Lock all default wires\" setting.", alwaysUseInstanceValues: true)] + public bool NoAutoLock + { + get; + set; + } + + [Editable, Serialize(false, true, "If enabled, this wire will use the sprite depth instead of a constant depth.")] + public bool UseSpriteDepth + { + get; + set; + } + public Wire(Item item, XElement element) : base(item, element) { @@ -309,6 +318,8 @@ namespace Barotrauma.Items.Components if (Screen.Selected != GameMain.SubEditorScreen) { + if (user != null) { NoAutoLock = true; } + //cannot run wires from sub to another if (item.Submarine != sub && sub != null && item.Submarine != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/XorComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/XorComponent.cs index d6504c6a3..134b53130 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/XorComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/XorComponent.cs @@ -22,7 +22,7 @@ namespace Barotrauma.Items.Components string signalOut = sendOutput == 1 ? output : falseOutput; if (string.IsNullOrEmpty(signalOut)) return; - item.SendSignal(0, signalOut, "signal_out", null); + item.SendSignal(signalOut, "signal_out"); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index e80bd9c66..11740b981 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -251,7 +251,7 @@ namespace Barotrauma.Items.Components private void UpdateTransformedBarrelPos() { - transformedBarrelPos = MathUtils.RotatePointAroundTarget(barrelPos * item.Scale, new Vector2(item.Rect.Width / 2, item.Rect.Height / 2), item.Rotation); + transformedBarrelPos = MathUtils.RotatePointAroundTarget(barrelPos * item.Scale, new Vector2(item.Rect.Width / 2, item.Rect.Height / 2), MathHelper.ToRadians(item.Rotation)); #if CLIENT item.ResetCachedVisibleSize(); #endif @@ -436,23 +436,28 @@ namespace Barotrauma.Items.Components Projectile launchedProjectile = null; for (int i = 0; i < ProjectileCount; i++) { - foreach (MapEntity e in item.linkedTo) + var projectiles = GetLoadedProjectiles(true); + if (projectiles.Any()) { - //use linked projectile containers in case they have to react to the turret being launched somehow - //(play a sound, spawn more projectiles) - if (!(e is Item linkedItem)) { continue; } - ItemContainer projectileContainer = linkedItem.GetComponent(); - if (projectileContainer != null) + ItemContainer projectileContainer = projectiles.First().Item.Container?.GetComponent(); + if (projectileContainer?.Item != item) { projectileContainer?.Item.Use(deltaTime, null); } + } + else + { + foreach (MapEntity e in item.linkedTo) { - linkedItem.Use(deltaTime, null); - var repairable = linkedItem.GetComponent(); - if (repairable != null && failedLaunchAttempts < 2) + //use linked projectile containers in case they have to react to the turret being launched somehow + //(play a sound, spawn more projectiles) + if (!(e is Item linkedItem)) { continue; } + ItemContainer projectileContainer = linkedItem.GetComponent(); + if (projectileContainer != null) { - repairable.LastActiveTime = (float)Timing.TotalTime + 1.0f; + linkedItem.Use(deltaTime, null); + projectiles = GetLoadedProjectiles(true); + if (projectiles.Any()) { break; } } } } - var projectiles = GetLoadedProjectiles(true); if (projectiles.Count == 0 && !LaunchWithoutProjectile) { //coilguns spawns ammo in the ammo boxes with the OnUse statuseffect when the turret is launched, @@ -471,7 +476,6 @@ namespace Barotrauma.Items.Components } failedLaunchAttempts = 0; launchedProjectile = projectiles.FirstOrDefault(); - if (!ignorePower) { var batteries = item.GetConnectedComponents(); @@ -492,6 +496,15 @@ namespace Barotrauma.Items.Components } } + if (launchedProjectile?.Item.Container != null) + { + var repairable = launchedProjectile?.Item.Container.GetComponent(); + if (repairable != null) + { + repairable.LastActiveTime = (float)Timing.TotalTime + 1.0f; + } + } + if (launchedProjectile != null || LaunchWithoutProjectile) { Launch(launchedProjectile?.Item, character); @@ -709,10 +722,7 @@ namespace Barotrauma.Items.Components } else { - float midRotation = (minRotation + maxRotation) / 2.0f; - while (midRotation - angle < -MathHelper.Pi) { angle -= MathHelper.TwoPi; } - while (midRotation - angle > MathHelper.Pi) { angle += MathHelper.TwoPi; } - if (angle < minRotation || angle > maxRotation) { return; } + if (!CheckTurretAngle(angle)) { return; } float enemyAngle = MathUtils.VectorToAngle(target.WorldPosition - item.WorldPosition); float turretAngle = -rotation; if (Math.Abs(MathUtils.GetShortestAngle(enemyAngle, turretAngle)) > 0.15f) { return; } @@ -770,6 +780,7 @@ namespace Barotrauma.Items.Components TryLaunch(deltaTime, ignorePower: true); } + private bool outOfAmmo; public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { if (character.AIController.SelectedAiTarget?.Entity is Character previousTarget && @@ -836,32 +847,37 @@ namespace Barotrauma.Items.Components } if (container == null || container.ContainableItems.Count == 0) { - character.Speak(TextManager.GetWithVariable("DialogCannotLoadTurret", "[itemname]", item.Name, true), null, 0.0f, "cannotloadturret", 30.0f); + if (character.IsOnPlayerTeam) + { + character.Speak(TextManager.GetWithVariable("DialogCannotLoadTurret", "[itemname]", item.Name, true), null, 0.0f, "cannotloadturret", 30.0f); + } return true; } if (objective.SubObjectives.None()) - { - if (!AIDecontainEmptyItems(character, objective, equip: true, sourceContainer: container)) - { - return false; - } - } - if (objective.SubObjectives.None()) { var loadItemsObjective = AIContainItems(container, character, objective, usableProjectileCount + 1, equip: true, removeEmpty: true, dropItemOnDeselected: true); - if (loadItemsObjective == null) + loadItemsObjective.ignoredContainerIdentifiers = new string[] { containerItem.prefab.Identifier }; + if (character.IsOnPlayerTeam) { - if (usableProjectileCount == 0) - { - character.Speak(TextManager.GetWithVariable("DialogCannotLoadTurret", "[itemname]", item.Name, true), null, 0.0f, "cannotloadturret", 30.0f); - return true; - } - } - else - { - loadItemsObjective.ignoredContainerIdentifiers = new string[] { containerItem.prefab.Identifier }; character.Speak(TextManager.GetWithVariable("DialogLoadTurret", "[itemname]", item.Name, true), null, 0.0f, "loadturret", 30.0f); - return false; + } + loadItemsObjective.Abandoned += CheckRemainingAmmo; + loadItemsObjective.Completed += CheckRemainingAmmo; + return false; + + void CheckRemainingAmmo() + { + if (!character.IsOnPlayerTeam) { return; } + string ammoType = container.Item.HasTag("railgunammosource") ? "railgunammo" : container.Item.HasTag("coilgunammosource") ? "coilgunammo" : "turretammo"; + int remainingAmmo = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(ammoType) && i.Condition > 1); + if (remainingAmmo == 0) + { + character.Speak(TextManager.Get($"DialogOutOf{ammoType}"), null, 0.0f, "outofammo", 30.0f); + } + else if (remainingAmmo < 3) + { + character.Speak(TextManager.Get($"DialogLowOn{ammoType}"), null, 0.0f, "outofammo", 30.0f); + } } } if (objective.SubObjectives.Any()) @@ -873,23 +889,51 @@ namespace Barotrauma.Items.Components //enough shells and power Character closestEnemy = null; Vector2? targetPos = null; + float maxDistance = 10000; float shootDistance = AIRange * item.OffsetOnSelectedMultiplier; - float closestDistance = shootDistance * shootDistance; + float closestDistance = maxDistance * maxDistance; foreach (Character enemy in Character.CharacterList) { // Ignore dead, friendly, and those that are inside the same sub if (enemy.IsDead || !enemy.Enabled || enemy.Submarine == character.Submarine) { continue; } + // Don't aim monsters that are inside a submarine. + if (!enemy.IsHuman && enemy.CurrentHull != null) { continue; } if (HumanAIController.IsFriendly(character, enemy)) { continue; } float dist = Vector2.DistanceSquared(enemy.WorldPosition, item.WorldPosition); if (dist > closestDistance) { continue; } - if (!CheckTurretAngle(enemy.WorldPosition)) { continue; } + if (dist < shootDistance * shootDistance) + { + // Only check the angle to targets that are close enough to be shot at + // We shouldn't check the angle when a long creature is traveling outside of the shooting range, because doing so would not allow us to shoot the limbs that might be close enough to shoot at. + if (!CheckTurretAngle(enemy.WorldPosition)) { continue; } + } closestEnemy = enemy; closestDistance = dist; } if (closestEnemy != null) { + // Target the closest limb. Doesn't make much difference with smaller creatures, but enables the bots to shoot longer abyss creatures like the endworm. Otherwise they just target the main body = head. targetPos = closestEnemy.WorldPosition; + float closestDist = closestDistance; + foreach (Limb limb in closestEnemy.AnimController.Limbs) + { + if (limb.IsSevered) { continue; } + if (limb.Hidden) { continue; } + if (!CheckTurretAngle(limb.WorldPosition)) { continue; } + float dist = Vector2.DistanceSquared(limb.WorldPosition, item.WorldPosition); + if (dist < closestDist) + { + closestDist = dist; + targetPos = limb.WorldPosition; + } + } + if (closestDist > shootDistance * shootDistance) + { + // Not close enough to shoot + closestEnemy = null; + targetPos = null; + } } else if (item.Submarine != null && Level.Loaded != null) { @@ -949,29 +993,32 @@ namespace Barotrauma.Items.Components if (closestEnemy != null && character.AIController.SelectedAiTarget != closestEnemy.AiTarget) { - if (character.AIController.SelectedAiTarget == null) + if (character.IsOnPlayerTeam) { - if (GameMain.Config.RecentlyEncounteredCreatures.Contains(closestEnemy.SpeciesName)) + if (character.AIController.SelectedAiTarget == null) { - character.Speak(TextManager.Get("DialogNewTargetSpotted"), null, 0.0f, "newtargetspotted", 30.0f); + if (GameMain.Config.RecentlyEncounteredCreatures.Contains(closestEnemy.SpeciesName)) + { + character.Speak(TextManager.Get("DialogNewTargetSpotted"), null, 0.0f, "newtargetspotted", 30.0f); + } + else if (GameMain.Config.EncounteredCreatures.Any(name => name.Equals(closestEnemy.SpeciesName, StringComparison.OrdinalIgnoreCase))) + { + character.Speak(TextManager.GetWithVariable("DialogIdentifiedTargetSpotted", "[speciesname]", closestEnemy.DisplayName), null, 0.0f, "identifiedtargetspotted", 30.0f); + } + else + { + character.Speak(TextManager.Get("DialogUnidentifiedTargetSpotted"), null, 0.0f, "unidentifiedtargetspotted", 5.0f); + } } - else if (GameMain.Config.EncounteredCreatures.Any(name => name.Equals(closestEnemy.SpeciesName, StringComparison.OrdinalIgnoreCase))) - { - character.Speak(TextManager.GetWithVariable("DialogIdentifiedTargetSpotted", "[speciesname]", closestEnemy.DisplayName), null, 0.0f, "identifiedtargetspotted", 30.0f); - } - else + else if (GameMain.Config.EncounteredCreatures.None(name => name.Equals(closestEnemy.SpeciesName, StringComparison.OrdinalIgnoreCase))) { character.Speak(TextManager.Get("DialogUnidentifiedTargetSpotted"), null, 0.0f, "unidentifiedtargetspotted", 5.0f); } + character.AddEncounter(closestEnemy); } - else if (GameMain.Config.EncounteredCreatures.None(name => name.Equals(closestEnemy.SpeciesName, StringComparison.OrdinalIgnoreCase))) - { - character.Speak(TextManager.Get("DialogUnidentifiedTargetSpotted"), null, 0.0f, "unidentifiedtargetspotted", 5.0f); - } - character.AddEncounter(closestEnemy); character.AIController.SelectTarget(closestEnemy.AiTarget); } - else if (closestEnemy == null) + else if (closestEnemy == null && character.IsOnPlayerTeam) { character.Speak(TextManager.Get("DialogIceSpireSpotted"), null, 0.0f, "icespirespotted", 60.0f); } @@ -1037,20 +1084,24 @@ namespace Barotrauma.Items.Components return false; } } - character.Speak(TextManager.Get("DialogFireTurret"), null, 0.0f, "fireturret", 10.0f); + if (character.IsOnPlayerTeam) + { + character.Speak(TextManager.Get("DialogFireTurret"), null, 0.0f, "fireturret", 10.0f); + } character.SetInput(InputType.Shoot, true, true); return false; } - private bool CheckTurretAngle(Vector2 target) + private bool CheckTurretAngle(float angle) { - float angle = -MathUtils.VectorToAngle(target - item.WorldPosition); float midRotation = (minRotation + maxRotation) / 2.0f; while (midRotation - angle < -MathHelper.Pi) { angle -= MathHelper.TwoPi; } while (midRotation - angle > MathHelper.Pi) { angle += MathHelper.TwoPi; } - return angle > minRotation && angle < maxRotation; + return angle >= minRotation && angle <= maxRotation; } + private bool CheckTurretAngle(Vector2 target) => CheckTurretAngle(-MathUtils.VectorToAngle(target - item.WorldPosition)); + protected override void RemoveComponentSpecific() { base.RemoveComponentSpecific(); @@ -1110,8 +1161,6 @@ namespace Barotrauma.Items.Components public override void FlipX(bool relativeToSub) { - BaseRotation = MathHelper.ToDegrees(MathUtils.WrapAngleTwoPi(MathHelper.ToRadians(-BaseRotation))); - minRotation = MathHelper.Pi - minRotation; maxRotation = MathHelper.Pi - maxRotation; @@ -1152,12 +1201,13 @@ namespace Barotrauma.Items.Components UpdateTransformedBarrelPos(); } - public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power, float signalStrength = 1.0f) + public override void ReceiveSignal(Signal signal, Connection connection) { + Character sender = signal.sender; switch (connection.Name) { case "position_in": - if (float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float newRotation)) + if (float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float newRotation)) { if (!MathUtils.IsValid(newRotation)) { return; } targetRotation = MathHelper.ToRadians(newRotation); @@ -1167,6 +1217,7 @@ namespace Barotrauma.Items.Components resetUserTimer = 10.0f; break; case "trigger_in": + if (signal.value == "0") { return; } item.Use((float)Timing.Step, sender); user = sender; resetUserTimer = 10.0f; @@ -1178,7 +1229,7 @@ namespace Barotrauma.Items.Components } break; case "toggle_light": - if (lightComponent != null && signal != "0") + if (lightComponent != null && signal.value != "0") { lightComponent.IsOn = !lightComponent.IsOn; } @@ -1186,7 +1237,7 @@ namespace Barotrauma.Items.Components case "set_light": if (lightComponent != null) { - lightComponent.IsOn = signal != "0"; + lightComponent.IsOn = signal.value != "0"; } break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index b8b67e1b0..6c6ebc229 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -659,17 +659,33 @@ namespace Barotrauma else { swapSuccessful = - (existingItems.All(existingItem => otherInventory.TryPutItem(existingItem, otherIndex, false, false, user, createNetworkEvent)) || - existingItems.Count == 1 && otherInventory.TryPutItem(existingItems.First(),user, CharacterInventory.anySlot, createNetworkEvent)) + (existingItems.All(existingItem => otherInventory.TryPutItem(existingItem, otherIndex, false, false, user, createNetworkEvent)) || + existingItems.Count == 1 && otherInventory.TryPutItem(existingItems.First(), user, CharacterInventory.anySlot, createNetworkEvent)) && stackedItems.Distinct().All(stackedItem => TryPutItem(stackedItem, index, false, false, user, createNetworkEvent)); + + if (!swapSuccessful && existingItems.Count == 1 && existingItems[0].AllowDroppingOnSwapWith(item)) + { + existingItems[0].Drop(user, createNetworkEvent); + swapSuccessful = stackedItems.Distinct().Any(stackedItem => TryPutItem(stackedItem, index, false, false, user, createNetworkEvent)); +#if CLIENT + if (swapSuccessful) + { + SoundPlayer.PlayUISound(GUISoundType.DropItem); + if (otherInventory.visualSlots != null && otherIndex > -1) + { + otherInventory.visualSlots[otherIndex].ShowBorderHighlight(Color.Transparent, 0.1f, 0.1f); + } + } +#endif + } } //if the item in the slot can be moved to the slot of the moved item if (swapSuccessful) { System.Diagnostics.Debug.Assert(slots[index].Contains(item), "Something when wrong when swapping items, item is not present in the inventory."); - System.Diagnostics.Debug.Assert(otherInventory.Contains(existingItems.FirstOrDefault()), "Something when wrong when swapping items, item is not present in the other inventory."); + System.Diagnostics.Debug.Assert(!existingItems.Any(it => !it.Prefab.AllowDroppingOnSwap && !otherInventory.Contains(it)), "Something when wrong when swapping items, item is not present in the other inventory."); #if CLIENT if (visualSlots != null) { @@ -773,12 +789,12 @@ namespace Barotrauma return match; } - public List FindAllItems(Func predicate, bool recursive = false, List list = null) + public List FindAllItems(Func predicate = null, bool recursive = false, List list = null) { list ??= new List(); foreach (var item in AllItems) { - if (predicate(item)) + if (predicate == null || predicate(item)) { list.Add(item); } @@ -826,7 +842,8 @@ namespace Barotrauma { slots[index].Add(item); item.ParentInventory = this; - if (item.body != null) + bool equipped = (this as CharacterInventory)?.Owner is Character character && character.HasEquippedItem(item); + if (item.body != null && !equipped) { item.body.Enabled = false; item.body.BodyType = FarseerPhysics.BodyType.Dynamic; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index d5a89bef0..75a55da54 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -348,7 +348,7 @@ namespace Barotrauma { if (AiTarget != null) { - AiTarget.SonarLabel = value; + AiTarget.SonarLabel = !string.IsNullOrEmpty(value) && value.Length > 200 ? value.Substring(200) : value; } } } @@ -527,6 +527,8 @@ namespace Barotrauma } } + public bool AllowStealing = true; + private string originalOutpost; [Serialize("", true, alwaysUseInstanceValues: true)] public string OriginalOutpost @@ -593,13 +595,13 @@ namespace Barotrauma } /// - /// A list of items the last signal sent by this item went through + /// A list of connections the last signal sent by this item went through /// - public List LastSentSignalRecipients + public List LastSentSignalRecipients { get; private set; - } = new List(20); + } = new List(20); public string ConfigFile { @@ -874,7 +876,7 @@ namespace Barotrauma DebugConsole.Log("Created " + Name + " (" + ID + ")"); - if (Components.Any() && Components.All(ic => ic is Wire || ic is Holdable)) { isWire = true; } + if (Components.Any(ic => ic is Wire) && Components.All(ic => ic is Wire || ic is Holdable)) { isWire = true; } if (HasTag("logic")) { isLogic = true; } } @@ -1100,6 +1102,27 @@ namespace Barotrauma if (findNewHull) { FindHull(); } } + /// + /// Is dropping the item allowed when trying to swap it with the other item + /// + public bool AllowDroppingOnSwapWith(Item otherItem) + { + if (!Prefab.AllowDroppingOnSwap || otherItem == null) { return false; } + if (Prefab.AllowDroppingOnSwapWith.Any()) + { + foreach (string tagOrIdentifier in Prefab.AllowDroppingOnSwapWith) + { + if (otherItem.prefab.Identifier.Equals(tagOrIdentifier, StringComparison.OrdinalIgnoreCase)) { return true; } + if (otherItem.HasTag(tagOrIdentifier)) { return true; } + } + return false; + } + else + { + return true; + } + } + public void SetActiveSprite() { SetActiveSpriteProjSpecific(); @@ -1712,6 +1735,11 @@ namespace Barotrauma flippedX = false; return; } + + if (Prefab.AllowRotatingInEditor) + { + rotationRad = MathUtils.WrapAngleTwoPi(-rotationRad); + } #if CLIENT if (Prefab.CanSpriteFlipX) { @@ -1890,42 +1918,63 @@ namespace Barotrauma return controller != null; } - public void SendSignal(int stepsTaken, string signal, string connectionName, Character sender, float power = 0.0f, Item source = null, float signalStrength = 1.0f) + public void SendSignal(string signal, string connectionName) { - if (connections == null) { return; } - if (!connections.TryGetValue(connectionName, out Connection c)) { return; } - SendSignal(stepsTaken, signal, c, sender, power, source, signalStrength); + SendSignal(new Signal(signal), connectionName); } - public void SendSignal(int stepsTaken, string signal, Connection connection, Character sender, float power = 0.0f, Item source = null, float signalStrength = 1.0f) + public void SendSignal(Signal signal, string connectionName) + { + if (connections == null) { return; } + if (!connections.TryGetValue(connectionName, out Connection connection)) { return; } + + signal.source ??= this; + SendSignal(signal, connection); + } + + public void SendSignal(Signal signal, Connection connection) { LastSentSignalRecipients.Clear(); if (connections == null || connection == null) { return; } - stepsTaken++; + signal.stepsTaken++; - if (stepsTaken > 10) + if (signal.stepsTaken > 10) { + //if the signal has been passed through this item multiple times already, interrupt it to prevent infinite loops + if (signal.source != null) + { + if (signal.source.LastSentSignalRecipients.Count(recipient => recipient == connection) > 2) + { + return; + } + } //use a coroutine to prevent infinite loops by creating a one //frame delay if the "signal chain" gets too long - CoroutineManager.StartCoroutine(SendSignal(signal, connection, sender, power, signalStrength)); + CoroutineManager.StartCoroutine(DelaySignal(signal, connection)); } else { foreach (StatusEffect effect in connection.Effects) { if (condition <= 0.0f && effect.type != ActionType.OnBroken) { continue; } - if (signal != "0" && !string.IsNullOrEmpty(signal)) { ApplyStatusEffect(effect, ActionType.OnUse, (float)Timing.Step); } + if (signal.value != "0" && !string.IsNullOrEmpty(signal.value)) { ApplyStatusEffect(effect, ActionType.OnUse, (float)Timing.Step); } } - connection.SendSignal(stepsTaken, signal, source ?? this, sender, power, signalStrength); + + signal.source ??= this; + connection.SendSignal(signal); } + } - private IEnumerable SendSignal(string signal, Connection connection, Character sender, float power = 0.0f, float signalStrength = 1.0f) + + private IEnumerable DelaySignal(Signal signal, Connection connection) { //wait one frame yield return CoroutineStatus.Running; - connection.SendSignal(0, signal, this, sender, power, signalStrength); + signal.stepsTaken = 0; + signal.source = this; + connection.SendSignal(signal); yield return CoroutineStatus.Success; } @@ -2781,7 +2830,14 @@ namespace Barotrauma if (parentInventory != null) { - parentInventory.RemoveItem(this); + if (parentInventory is CharacterInventory characterInventory) + { + characterInventory.RemoveItem(this, tryEquipFromSameStack: true); + } + else + { + parentInventory.RemoveItem(this); + } parentInventory = null; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs index e2e783677..83e469f31 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs @@ -111,7 +111,7 @@ namespace Barotrauma public override bool TryPutItem(Item item, int i, bool allowSwapping, bool allowCombine, Character user, bool createNetworkEvent = true) { bool wasPut = base.TryPutItem(item, i, allowSwapping, allowCombine, user, createNetworkEvent); - if (wasPut) + if (wasPut && item.ParentInventory == this) { foreach (Character c in Character.CharacterList) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index a1c97954a..b89df2b70 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -1,13 +1,10 @@ -using FarseerPhysics; +using Barotrauma.IO; +using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using Barotrauma.IO; -using System.Xml.Linq; using System.Linq; -using Barotrauma.Items.Components; -using Barotrauma.Extensions; -using Voronoi2; +using System.Xml.Linq; namespace Barotrauma { @@ -195,7 +192,7 @@ namespace Barotrauma } } - partial class ItemPrefab : MapEntityPrefab + partial class ItemPrefab : MapEntityPrefab, IHasUintIdentifier { private readonly string name; public override string Name => name; @@ -215,7 +212,7 @@ namespace Barotrauma protected Vector2 size; private float impactTolerance; - private readonly PriceInfo defaultPrice; + public readonly PriceInfo DefaultPrice; private readonly Dictionary locationPrices; /// @@ -485,12 +482,14 @@ namespace Barotrauma public int ClusterQuantity { get; } public int ClusterSize { get; } public bool IsIslandSpecifc { get; } + public bool AllowAtStart { get; } - public FixedQuantityResourceInfo(int clusterQuantity, int clusterSize, bool isIslandSpecific) + public FixedQuantityResourceInfo(int clusterQuantity, int clusterSize, bool isIslandSpecific, bool allowAtStart) { ClusterQuantity = clusterQuantity; ClusterSize = clusterSize; IsIslandSpecifc = isIslandSpecific; + AllowAtStart = allowAtStart; } } @@ -515,19 +514,35 @@ namespace Barotrauma set { maxStackSize = MathHelper.Clamp(value, 1, Inventory.MaxStackSize); } } + [Serialize(false, false)] + public bool AllowDroppingOnSwap { get; private set; } + + private readonly HashSet allowDroppingOnSwapWith = new HashSet(); + public IEnumerable AllowDroppingOnSwapWith + { + get { return allowDroppingOnSwapWith; } + } + public Vector2 Size => size; - public bool CanBeBought => (defaultPrice != null && defaultPrice.CanBeBought) || (locationPrices != null && locationPrices.Any(p => p.Value.CanBeBought)); + public bool CanBeBought => (DefaultPrice != null && DefaultPrice.CanBeBought) || (locationPrices != null && locationPrices.Any(p => p.Value.CanBeBought)); + + /// + /// Can the item be chosen as extra cargo in multiplayer. If not set, the item is available if it can be bought from outposts in the campaign. + /// + public bool? AllowAsExtraCargo; /// /// Any item with a Price element in the definition can be sold everywhere. /// - public bool CanBeSold => defaultPrice != null; + public bool CanBeSold => DefaultPrice != null; public bool RandomDeconstructionOutput { get; } public int RandomDeconstructionOutputAmount { get; } + public uint UIntIdentifier { get; set; } + public static void RemoveByFile(string filePath) => Prefabs.RemoveByFile(filePath); public static void LoadFromFile(ContentFile file) @@ -642,7 +657,9 @@ namespace Barotrauma originalName = element.GetAttributeString("name", ""); name = originalName; identifier = element.GetAttributeString("identifier", ""); - if (!Enum.TryParse(element.GetAttributeString("category", "Misc"), true, out MapEntityCategory category)) + + string categoryStr = element.GetAttributeString("category", "Misc"); + if (!Enum.TryParse(categoryStr, true, out MapEntityCategory category)) { category = MapEntityCategory.Misc; } @@ -684,7 +701,7 @@ namespace Barotrauma identifier = GenerateLegacyIdentifier(originalName); } } - + if (string.Equals(parentType, "wrecked", StringComparison.OrdinalIgnoreCase)) { if (!string.IsNullOrEmpty(name)) @@ -692,7 +709,7 @@ namespace Barotrauma name = TextManager.GetWithVariable("wreckeditemformat", "[name]", name); } } - + if (string.IsNullOrEmpty(name)) { DebugConsole.ThrowError($"Unnamed item ({identifier}) in {filePath}!"); @@ -704,11 +721,16 @@ namespace Barotrauma (element.GetAttributeStringArray("aliases", null, convertToLowerInvariant: true) ?? element.GetAttributeStringArray("Aliases", new string[0], convertToLowerInvariant: true)); Aliases.Add(originalName.ToLowerInvariant()); - - Triggers = new List(); - DeconstructItems = new List(); - FabricationRecipes = new List(); - DeconstructTime = 1.0f; + + Triggers = new List(); + DeconstructItems = new List(); + FabricationRecipes = new List(); + DeconstructTime = 1.0f; + + if (element.Attribute("allowasextracargo") != null) + { + AllowAsExtraCargo = element.GetAttributeBool("allowasextracargo", false); + } Tags = new HashSet(element.GetAttributeStringArray("tags", new string[0], convertToLowerInvariant: true)); if (!Tags.Any()) @@ -739,6 +761,16 @@ namespace Barotrauma } } + var allowDroppingOnSwapWith = element.GetAttributeStringArray("allowdroppingonswapwith", new string[0]); + if (allowDroppingOnSwapWith.Any()) + { + AllowDroppingOnSwap = true; + foreach (string tag in allowDroppingOnSwapWith) + { + this.allowDroppingOnSwapWith.Add(tag); + } + } + foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -770,7 +802,7 @@ namespace Barotrauma if (locationPrices == null) { locationPrices = new Dictionary(); } if (subElement.Attribute("baseprice") != null) { - foreach (Tuple priceInfo in PriceInfo.CreatePriceInfos(subElement, out defaultPrice)) + foreach (Tuple priceInfo in PriceInfo.CreatePriceInfos(subElement, out DefaultPrice)) { if (priceInfo == null) { continue; } locationPrices.Add(priceInfo.Item1, priceInfo.Item2); @@ -961,7 +993,8 @@ namespace Barotrauma LevelQuantity.Add(levelName, new FixedQuantityResourceInfo( levelCommonnessElement.GetAttributeInt("clusterquantity", 0), levelCommonnessElement.GetAttributeInt("clustersize", 0), - levelCommonnessElement.GetAttributeBool("isislandspecific", false))); + levelCommonnessElement.GetAttributeBool("isislandspecific", false), + levelCommonnessElement.GetAttributeBool("allowatstart", true))); } } } @@ -985,7 +1018,14 @@ namespace Barotrauma // with separate Price elements and there is no default price explicitly set if (locationPrices != null && locationPrices.Any()) { - defaultPrice ??= new PriceInfo(GetMinPrice() ?? 0, false); + DefaultPrice ??= new PriceInfo(GetMinPrice() ?? 0, false); + } + + //backwards compatibility + if (categoryStr.Equals("Thalamus", StringComparison.OrdinalIgnoreCase)) + { + Category = MapEntityCategory.Wrecked; + Subcategory = "Thalamus"; } if (sprite == null) @@ -1023,6 +1063,7 @@ namespace Barotrauma AllowedLinks = element.GetAttributeStringArray("allowedlinks", new string[0], convertToLowerInvariant: true).ToList(); Prefabs.Add(this, allowOverriding); + this.CalculatePrefabUIntIdentifier(Prefabs); } public float GetTreatmentSuitability(string treatmentIdentifier) @@ -1040,7 +1081,7 @@ namespace Barotrauma } else { - return defaultPrice; + return DefaultPrice; } } @@ -1049,7 +1090,10 @@ namespace Barotrauma priceInfo = null; if (location?.Type == null) { return false; } priceInfo = GetPriceInfo(location); - return priceInfo != null && priceInfo.CanBeBought; + return + priceInfo != null && + priceInfo.CanBeBought && + (location.LevelData?.Difficulty ?? 0) >= priceInfo.MinLevelDifficulty; } public static ItemPrefab Find(string name, string identifier) @@ -1085,9 +1129,9 @@ namespace Barotrauma int? minPrice = locationPrices != null && locationPrices.Values.Any() ? locationPrices?.Values.Min(p => p.Price) : null; if (minPrice.HasValue) { - if (defaultPrice != null) + if (DefaultPrice != null) { - return minPrice < defaultPrice.Price ? minPrice : defaultPrice.Price; + return minPrice < DefaultPrice.Price ? minPrice : DefaultPrice.Price; } else { @@ -1096,7 +1140,7 @@ namespace Barotrauma } else { - return defaultPrice?.Price; + return DefaultPrice?.Price; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs index ffead791e..26d1a03c8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs @@ -87,7 +87,7 @@ namespace Barotrauma.MapCreatures.Behavior internal partial class BallastFloraBehavior : ISerializableEntity { -#if DEBUG || UNSTABLE +#if DEBUG public List> debugSearchLines = new List>(); #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/GrowIdleState.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/GrowIdleState.cs index 69a69b806..ff3ac93bb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/GrowIdleState.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/GrowIdleState.cs @@ -60,7 +60,7 @@ namespace Barotrauma.MapCreatures.Behavior protected virtual void Grow() { List newTiles = GrowRandomly(); -#if DEBUG || UNSTABLE +#if DEBUG Behavior.debugSearchLines.Clear(); #endif if (newTiles.Any(TryScanTargets)) { return; } @@ -135,7 +135,7 @@ namespace Barotrauma.MapCreatures.Behavior Vector2 itemSimPos = ConvertUnits.ToSimUnits(item.Position); -#if DEBUG || UNSTABLE +#if DEBUG Tuple debugLine1 = Tuple.Create(parent.Position - ConvertUnits.ToDisplayUnits(topLeft), parent.Position - ConvertUnits.ToDisplayUnits(itemSimPos - diameter)); Tuple debugLine2 = Tuple.Create(parent.Position - ConvertUnits.ToDisplayUnits(bottomRight), parent.Position - ConvertUnits.ToDisplayUnits(itemSimPos + diameter)); Behavior.debugSearchLines.Add(debugLine2); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index 187f354f3..b68a548e7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -15,7 +15,7 @@ namespace Barotrauma { private static readonly List> prevExplosions = new List>(); - private readonly Attack attack; + public readonly Attack Attack; private readonly float force; @@ -27,6 +27,10 @@ namespace Barotrauma private bool sparks, shockwave, flames, smoke, flash, underwaterBubble; private bool playTinnitus; private bool applyFireEffects; + private string[] ignoreFireEffectsForTags; + private bool ignoreCover; + private bool onlyInside; + private bool onlyOutside; private readonly float flashDuration; private readonly float? flashRange; private readonly string decal; @@ -38,7 +42,7 @@ namespace Barotrauma public Explosion(float range, float force, float damage, float structureDamage, float itemDamage, float empStrength = 0.0f, float ballastFloraStrength = 0.0f) { - attack = new Attack(damage, 0.0f, 0.0f, structureDamage, itemDamage, range) + Attack = new Attack(damage, 0.0f, 0.0f, structureDamage, itemDamage, range) { SeverLimbsProbability = 1.0f }; @@ -50,11 +54,12 @@ namespace Barotrauma smoke = true; flames = true; underwaterBubble = true; + ignoreFireEffectsForTags = new string[0]; } public Explosion(XElement element, string parentDebugName) { - attack = new Attack(element, parentDebugName + ", Explosion"); + Attack = new Attack(element, parentDebugName + ", Explosion"); force = element.GetAttributeFloat("force", 0.0f); @@ -67,6 +72,11 @@ namespace Barotrauma playTinnitus = element.GetAttributeBool("playtinnitus", true); applyFireEffects = element.GetAttributeBool("applyfireeffects", flames); + ignoreFireEffectsForTags = element.GetAttributeStringArray("ignorefireeffectsfortags", new string[0], convertToLowerInvariant: true); + + ignoreCover = element.GetAttributeBool("ignorecover", false); + onlyInside = element.GetAttributeBool("onlyinside", false); + onlyOutside = element.GetAttributeBool("onlyoutside", false); flash = element.GetAttributeBool("flash", true); flashDuration = element.GetAttributeFloat("flashduration", 0.05f); @@ -78,10 +88,10 @@ namespace Barotrauma decal = element.GetAttributeString("decal", ""); decalSize = element.GetAttributeFloat(1.0f, "decalSize", "decalsize"); - cameraShake = element.GetAttributeFloat("camerashake", attack.Range * 0.1f); - cameraShakeRange = element.GetAttributeFloat("camerashakerange", attack.Range); + cameraShake = element.GetAttributeFloat("camerashake", Attack.Range * 0.1f); + cameraShakeRange = element.GetAttributeFloat("camerashakerange", Attack.Range); - screenColorRange = element.GetAttributeFloat("screencolorrange", attack.Range * 0.1f); + screenColorRange = element.GetAttributeFloat("screencolorrange", Attack.Range * 0.1f); screenColor = element.GetAttributeColor("screencolor", Color.Transparent); screenColorDuration = element.GetAttributeFloat("screencolorduration", 0.1f); } @@ -117,7 +127,7 @@ namespace Barotrauma hull.AddDecal(decal, worldPosition, decalSize, isNetworkEvent: false); } - float displayRange = attack.Range; + float displayRange = Attack.Range; Vector2 cameraPos = Character.Controlled != null ? Character.Controlled.WorldPosition : GameMain.GameScreen.Cam.Position; float cameraDist = Vector2.Distance(cameraPos, worldPosition) / 2.0f; @@ -132,9 +142,9 @@ namespace Barotrauma if (displayRange < 0.1f) { return; } - if (attack.GetStructureDamage(1.0f) > 0.0f) + if (Attack.GetStructureDamage(1.0f) > 0.0f || Attack.GetLevelWallDamage(1.0f) > 0.0f) { - RangedStructureDamage(worldPosition, displayRange, attack.GetStructureDamage(1.0f), attack.GetLevelWallDamage(1.0f), attacker); + RangedStructureDamage(worldPosition, displayRange, Attack.GetStructureDamage(1.0f), Attack.GetLevelWallDamage(1.0f), attacker); } if (BallastFloraDamage > 0.0f) @@ -169,12 +179,12 @@ namespace Barotrauma } } - if (MathUtils.NearlyEqual(force, 0.0f) && MathUtils.NearlyEqual(attack.Stun, 0.0f) && MathUtils.NearlyEqual(attack.GetTotalDamage(false), 0.0f)) + if (MathUtils.NearlyEqual(force, 0.0f) && MathUtils.NearlyEqual(Attack.Stun, 0.0f) && MathUtils.NearlyEqual(Attack.GetTotalDamage(false), 0.0f)) { return; } - DamageCharacters(worldPosition, attack, force, damageSource, attacker); + DamageCharacters(worldPosition, Attack, force, damageSource, attacker); if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) { @@ -184,9 +194,9 @@ namespace Barotrauma float dist = Vector2.Distance(item.WorldPosition, worldPosition); float itemRadius = item.body == null ? 0.0f : item.body.GetMaxExtent(); dist = Math.Max(0.0f, dist - ConvertUnits.ToDisplayUnits(itemRadius)); - if (dist > attack.Range) { continue; } + if (dist > Attack.Range) { continue; } - if (dist < attack.Range * 0.5f && applyFireEffects && !item.FireProof) + if (dist < Attack.Range * 0.5f && applyFireEffects && !item.FireProof && ignoreFireEffectsForTags.None(t => item.HasTag(t))) { //don't apply OnFire effects if the item is inside a fireproof container //(or if it's inside a container that's inside a fireproof container, etc) @@ -213,8 +223,8 @@ namespace Barotrauma if (item.Prefab.DamagedByExplosions && !item.Indestructible) { - float distFactor = 1.0f - dist / attack.Range; - float damageAmount = attack.GetItemDamage(1.0f) * item.Prefab.ExplosionDamageMultiplier; + float distFactor = 1.0f - dist / Attack.Range; + float damageAmount = Attack.GetItemDamage(1.0f) * item.Prefab.ExplosionDamageMultiplier; Vector2 explosionPos = worldPosition; if (item.Submarine != null) { explosionPos -= item.Submarine.Position; } @@ -243,6 +253,8 @@ namespace Barotrauma { continue; } + if (onlyInside && c.Submarine == null) { continue; } + else if (onlyOutside && c.Submarine != null) { continue; } Vector2 explosionPos = worldPosition; if (c.Submarine != null) { explosionPos -= c.Submarine.Position; } @@ -271,7 +283,10 @@ namespace Barotrauma float distFactor = 1.0f - dist / attack.Range; //solid obstacles between the explosion and the limb reduce the effect of the explosion - distFactor *= GetObstacleDamageMultiplier(explosionPos, worldPosition, limb.SimPosition); + if (!ignoreCover) + { + distFactor *= GetObstacleDamageMultiplier(explosionPos, worldPosition, limb.SimPosition); + } distFactors.Add(limb, distFactor); modifiedAfflictions.Clear(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index 8da12066f..068076ada 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -203,9 +203,9 @@ namespace Barotrauma public void AutoOrient() { Vector2 searchPosLeft = new Vector2(rect.X, rect.Y - rect.Height / 2); - Hull hullLeft = Hull.FindHullOld(searchPosLeft, null, false); + Hull hullLeft = Hull.FindHullUnoptimized(searchPosLeft, null, false); Vector2 searchPosRight = new Vector2(rect.Right, rect.Y - rect.Height / 2); - Hull hullRight = Hull.FindHullOld(searchPosRight, null, false); + Hull hullRight = Hull.FindHullUnoptimized(searchPosRight, null, false); if (hullLeft != null && hullRight != null && hullLeft != hullRight) { @@ -214,9 +214,9 @@ namespace Barotrauma } Vector2 searchPosTop = new Vector2(rect.Center.X, rect.Y); - Hull hullTop = Hull.FindHullOld(searchPosTop, null, false); + Hull hullTop = Hull.FindHullUnoptimized(searchPosTop, null, false); Vector2 searchPosBottom = new Vector2(rect.Center.X, rect.Y - rect.Height); - Hull hullBottom = Hull.FindHullOld(searchPosBottom, null, false); + Hull hullBottom = Hull.FindHullUnoptimized(searchPosBottom, null, false); if (hullTop != null && hullBottom != null && hullTop != hullBottom) { @@ -261,8 +261,8 @@ namespace Barotrauma for (int i = 0; i < 2; i++) { - hulls[i] = Hull.FindHullOld(searchPos[i], null, false); - if (hulls[i] == null) hulls[i] = Hull.FindHullOld(searchPos[i], null, false, true); + hulls[i] = Hull.FindHullUnoptimized(searchPos[i], null, false); + if (hulls[i] == null) hulls[i] = Hull.FindHullUnoptimized(searchPos[i], null, false, true); } if (hulls[0] == null && hulls[1] == null) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 08a145013..991acbcc4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -939,14 +939,14 @@ namespace Barotrauma /// Approximate distance from this hull to the target hull, moving through open gaps without passing through walls. /// Uses a greedy algo and may not use the most optimal path. Returns float.MaxValue if no path is found. /// - public float GetApproximateDistance(Vector2 startPos, Vector2 endPos, Hull targetHull, float maxDistance) + public float GetApproximateDistance(Vector2 startPos, Vector2 endPos, Hull targetHull, float maxDistance, float distanceMultiplierPerClosedDoor = 0) { - return GetApproximateHullDistance(startPos, endPos, new HashSet(), targetHull, 0.0f, maxDistance); + return GetApproximateHullDistance(startPos, endPos, new HashSet(), targetHull, 0.0f, maxDistance, distanceMultiplierPerClosedDoor); } - private float GetApproximateHullDistance(Vector2 startPos, Vector2 endPos, HashSet connectedHulls, Hull target, float distance, float maxDistance) + private float GetApproximateHullDistance(Vector2 startPos, Vector2 endPos, HashSet connectedHulls, Hull target, float distance, float maxDistance, float distanceMultiplierFromDoors = 0) { - if (distance >= maxDistance) return float.MaxValue; + if (distance >= maxDistance) { return float.MaxValue; } if (this == target) { return distance + Vector2.Distance(startPos, endPos); @@ -956,12 +956,17 @@ namespace Barotrauma foreach (Gap g in ConnectedGaps) { + float distanceMultiplier = 1; if (g.ConnectedDoor != null && !g.ConnectedDoor.IsBroken) { //gap blocked if the door is not open or the predicted state is not open if ((!g.ConnectedDoor.IsOpen && !g.ConnectedDoor.IsBroken) || (g.ConnectedDoor.PredictedState.HasValue && !g.ConnectedDoor.PredictedState.Value)) { - if (g.ConnectedDoor.OpenState < 0.1f) continue; + if (g.ConnectedDoor.OpenState < 0.1f) + { + if (distanceMultiplierFromDoors <= 0) { continue; } + distanceMultiplier *= distanceMultiplierFromDoors; + } } } else if (g.Open <= 0.0f) @@ -973,8 +978,11 @@ namespace Barotrauma { if (g.linkedTo[i] is Hull hull && !connectedHulls.Contains(hull)) { - float dist = hull.GetApproximateHullDistance(g.Position, endPos, connectedHulls, target, distance + Vector2.Distance(startPos, g.Position), maxDistance); - if (dist < float.MaxValue) { return dist; } + float dist = hull.GetApproximateHullDistance(g.Position, endPos, connectedHulls, target, distance + Vector2.Distance(startPos, g.Position) * distanceMultiplier, maxDistance); + if (dist < float.MaxValue) + { + return dist; + } } } } @@ -982,7 +990,13 @@ namespace Barotrauma return float.MaxValue; } - //returns the water block which contains the point (or null if it isn't inside any) + /// + /// Returns the hull which contains the point (or null if it isn't inside any) + /// + /// The position to check + /// This hull is checked first: if the current hull is known, this can be used as an optimization + /// Should world coordinates or the sub's local coordinates be used? + /// Does being exactly at the edge of the hull count as being inside? public static Hull FindHull(Vector2 position, Hull guess = null, bool useWorldCoordinates = true, bool inclusive = true) { if (EntityGrids == null) return null; @@ -1030,20 +1044,19 @@ namespace Barotrauma return null; } - //returns the water block which contains the point (or null if it isn't inside any) - public static Hull FindHullOld(Vector2 position, Hull guess = null, bool useWorldCoordinates = true, bool inclusive = true) + /// + /// Returns the hull which contains the point (or null if it isn't inside any). The difference to FindHull is that this method goes through all hulls without trying + /// to first find the sub the point is inside and checking the hulls in that sub. + /// = This is slower, use with caution in situations where the sub's extents or hulls may have changed after it was loaded. + /// + public static Hull FindHullUnoptimized(Vector2 position, Hull guess = null, bool useWorldCoordinates = true, bool inclusive = true) { - return FindHullOld(position, hullList, guess, useWorldCoordinates, inclusive); - } - - public static Hull FindHullOld(Vector2 position, List hulls, Hull guess = null, bool useWorldCoordinates = true, bool inclusive = true) - { - if (guess != null && hulls.Contains(guess)) + if (guess != null && hullList.Contains(guess)) { if (Submarine.RectContains(useWorldCoordinates ? guess.WorldRect : guess.rect, position, inclusive)) return guess; } - foreach (Hull hull in hulls) + foreach (Hull hull in hullList) { if (Submarine.RectContains(useWorldCoordinates ? hull.WorldRect : hull.rect, position, inclusive)) return hull; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs index 2ed64bd3d..8ce97ea4d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs @@ -135,13 +135,18 @@ namespace Barotrauma } public List CreateInstance(Vector2 position, Submarine sub, bool selectInstance = false) + { + return PasteEntities(position, sub, configElement, FilePath, selectInstance); + } + + public static List PasteEntities(Vector2 position, Submarine sub, XElement configElement, string filePath = null, bool selectInstance = false) { int idOffset = Entity.FindFreeID(1); if (MapEntity.mapEntityList.Any()) { idOffset = MapEntity.mapEntityList.Max(e => e.ID); } - List entities = MapEntity.LoadAll(sub, configElement, FilePath, idOffset); + List entities = MapEntity.LoadAll(sub, configElement, filePath, idOffset); if (entities.Count == 0) { return entities; } - Vector2 offset = sub == null ? Vector2.Zero : sub.HiddenSubPosition; + Vector2 offset = sub?.HiddenSubPosition ?? Vector2.Zero; foreach (MapEntity me in entities) { @@ -168,9 +173,8 @@ namespace Barotrauma MapEntity.SelectedList.Clear(); entities.ForEach(MapEntity.AddSelection); } -#endif +#endif return entities; - } public void Delete() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs index 6bb62d0ab..e365e2c66 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs @@ -101,19 +101,19 @@ namespace Barotrauma public Sprite WallSprite { get; private set; } public Sprite WallEdgeSprite { get; private set; } - public static CaveGenerationParams GetRandom(LevelGenerationParams generationParams, Rand.RandSync rand) + public static CaveGenerationParams GetRandom(LevelGenerationParams generationParams, bool abyss, Rand.RandSync rand) { - if (CaveParams.All(p => p.GetCommonness(generationParams) <= 0.0f)) + if (CaveParams.All(p => p.GetCommonness(generationParams, abyss) <= 0.0f)) { return CaveParams.First(); } - return ToolBox.SelectWeightedRandom(CaveParams, CaveParams.Select(p => p.GetCommonness(generationParams)).ToList(), rand); + return ToolBox.SelectWeightedRandom(CaveParams, CaveParams.Select(p => p.GetCommonness(generationParams, abyss)).ToList(), rand); } - public float GetCommonness(LevelGenerationParams generationParams) + public float GetCommonness(LevelGenerationParams generationParams, bool abyss) { if (generationParams?.Identifier != null && - OverrideCommonness.TryGetValue(generationParams.Identifier, out float commonness)) + OverrideCommonness.TryGetValue(abyss ? "abyss" : generationParams.Identifier, out float commonness)) { return commonness; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs index 21ccdffd0..3eb478afb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs @@ -141,29 +141,21 @@ namespace Barotrauma return cells; } - public static void GeneratePath(Level.Tunnel tunnel, List cells, List[,] cellGrid, int gridCellSize, Rectangle limits) + public static void GeneratePath(Level.Tunnel tunnel, Level level) { var targetCells = new List(); for (int i = 0; i < tunnel.Nodes.Count; i++) { - //a search depth of 2 is large enough to find a cell in almost all maps, but in case it fails, we increase the depth - int searchDepth = 2; - while (searchDepth < 5) + var closestCell = level.GetClosestCell(tunnel.Nodes[i].ToVector2()); + if (closestCell != null && !targetCells.Contains(closestCell)) { - int cellIndex = FindCellIndex(tunnel.Nodes[i], cells, cellGrid, gridCellSize, searchDepth); - if (cellIndex > -1) - { - targetCells.Add(cells[cellIndex]); - break; - } - - searchDepth++; + targetCells.Add(closestCell); } } - tunnel.Cells.AddRange(GeneratePath(targetCells, cells, limits)); + tunnel.Cells.AddRange(GeneratePath(targetCells, level.GetAllCells())); } - public static List GeneratePath(List targetCells, List cells, Rectangle limits) + public static List GeneratePath(List targetCells, List cells) { Stopwatch sw2 = new Stopwatch(); sw2.Start(); @@ -460,10 +452,15 @@ namespace Barotrauma return cellBody; } - + public static List CreateRandomChunk(float radius, int vertexCount, float radiusVariance) { - Debug.Assert(radiusVariance < radius); + return CreateRandomChunk(radius * 2, radius * 2, vertexCount, radiusVariance); + } + + public static List CreateRandomChunk(float width, float height, int vertexCount, float radiusVariance) + { + Debug.Assert(radiusVariance < Math.Min(width, height)); Debug.Assert(vertexCount >= 3); List verts = new List(); @@ -471,72 +468,12 @@ namespace Barotrauma float angle = 0.0f; for (int i = 0; i < vertexCount; i++) { - verts.Add(new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)) * - (radius + Rand.Range(-radiusVariance, radiusVariance, Rand.RandSync.Server))); + Vector2 dir = new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)); + verts.Add(new Vector2(dir.X * width / 2, dir.Y * height / 2) + dir * Rand.Range(-radiusVariance, radiusVariance, Rand.RandSync.Server)); angle += angleStep; } return verts; } - /// - /// find the index of the cell which the point is inside - /// (actually finds the cell whose center is closest, but it's always the correct cell assuming the point is inside the borders of the diagram) - /// - public static int FindCellIndex(Vector2 position,List cells, List[,] cellGrid, int gridCellSize, int searchDepth = 1, Vector2? offset = null) - { - float closestDist = float.PositiveInfinity; - VoronoiCell closestCell = null; - - Vector2 gridOffset = offset == null ? Vector2.Zero : (Vector2)offset; - position -= gridOffset; - - int gridPosX = (int)Math.Floor(position.X / gridCellSize); - int gridPosY = (int)Math.Floor(position.Y / gridCellSize); - - for (int x = Math.Max(gridPosX - searchDepth, 0); x <= Math.Min(gridPosX + searchDepth, cellGrid.GetLength(0) - 1); x++) - { - for (int y = Math.Max(gridPosY - searchDepth, 0); y <= Math.Min(gridPosY + searchDepth, cellGrid.GetLength(1) - 1); y++) - { - for (int i = 0; i < cellGrid[x, y].Count; i++) - { - float dist = Vector2.DistanceSquared(cellGrid[x, y][i].Center, position); - if (dist > closestDist) continue; - - closestDist = dist; - closestCell = cellGrid[x, y][i]; - } - } - } - - return cells.IndexOf(closestCell); - } - - public static int FindCellIndex(Point position, List cells, List[,] cellGrid, int gridCellSize, int searchDepth = 1) - { - int closestDist = int.MaxValue; - VoronoiCell closestCell = null; - - int gridPosX = position.X / gridCellSize; - int gridPosY = position.Y / gridCellSize; - - for (int x = Math.Max(gridPosX - searchDepth, 0); x <= Math.Min(gridPosX + searchDepth, cellGrid.GetLength(0) - 1); x++) - { - for (int y = Math.Max(gridPosY - searchDepth, 0); y <= Math.Min(gridPosY + searchDepth, cellGrid.GetLength(1) - 1); y++) - { - for (int i = 0; i < cellGrid[x, y].Count; i++) - { - int dist = MathUtils.DistanceSquared( - (int)cellGrid[x, y][i].Site.Coord.X, (int)cellGrid[x, y][i].Site.Coord.Y, - position.X, position.Y); - if (dist > closestDist) continue; - - closestDist = dist; - closestCell = cellGrid[x, y][i]; - } - } - } - - return cells.IndexOf(closestCell); - } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index a43c81020..da0727689 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -35,7 +35,9 @@ namespace Barotrauma Cave = 0x4, Ruin = 0x8, Wreck = 0x10, - BeaconStation = 0x20 + BeaconStation = 0x20, + Abyss = 0x40, + AbyssCave = 0x80 } public struct InterestingPosition @@ -146,11 +148,41 @@ namespace Barotrauma private List[,] cellGrid; private List cells; + public Rectangle AbyssArea + { + get; + private set; + } + + public int AbyssStart + { + get { return AbyssArea.Y + AbyssArea.Height; } + } + + public int AbyssEnd + { + get { return AbyssArea.Y; } + } + + public class AbyssIsland + { + public Rectangle Area; + public readonly List Cells; + + public AbyssIsland(Rectangle area, List cells) + { + Debug.Assert(cells != null && cells.Any()); + Area = area; + Cells = cells; + } + } + public List AbyssIslands = new List(); + //TODO: make private public List siteCoordsX, siteCoordsY; //TODO: make private - public List> distanceField; + public List<(Point point, double distance)> distanceField; private Point startPosition, endPosition; @@ -170,6 +202,12 @@ namespace Barotrauma get { return startPosition.ToVector2(); } } + private Vector2 startExitPosition; + public Vector2 StartExitPosition + { + get { return startExitPosition; } + } + public Point Size { get { return LevelData.Size; } @@ -180,6 +218,12 @@ namespace Barotrauma get { return endPosition.ToVector2(); } } + private Vector2 endExitPosition; + public Vector2 EndExitPosition + { + get { return endExitPosition; } + } + public int BottomPos { get; @@ -351,8 +395,11 @@ namespace Barotrauma EntitiesBeforeGenerate = GetEntities().ToList(); EntityCountBeforeGenerate = EntitiesBeforeGenerate.Count(); - StartLocation = GameMain.GameSession?.StartLocation; - EndLocation = GameMain.GameSession?.EndLocation; + if (LevelData.ForceOutpostGenerationParams == null) + { + StartLocation = GameMain.GameSession?.StartLocation; + EndLocation = GameMain.GameSession?.EndLocation; + } EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); @@ -389,31 +436,36 @@ namespace Barotrauma SeaFloorTopPos = GenerationParams.SeaFloorDepth + GenerationParams.MountainHeightMax + GenerationParams.SeaFloorVariance; - int minWidth = Math.Min(GenerationParams.MinTunnelRadius, MaxSubmarineWidth); + int minMainPathWidth = Math.Min(GenerationParams.MinTunnelRadius, MaxSubmarineWidth); + int minWidth = 500; if (Submarine.MainSub != null) { Rectangle dockedSubBorders = Submarine.MainSub.GetDockedBorders(); dockedSubBorders.Inflate(dockedSubBorders.Size.ToVector2() * 0.15f); - minWidth = Math.Max(minWidth, Math.Max(dockedSubBorders.Width, dockedSubBorders.Height)); - minWidth = Math.Min(minWidth, MaxSubmarineWidth); + minWidth = Math.Max(dockedSubBorders.Width, dockedSubBorders.Height); + minMainPathWidth = Math.Max(minMainPathWidth, minWidth); + minMainPathWidth = Math.Min(minMainPathWidth, MaxSubmarineWidth); } - minWidth = Math.Min(minWidth, borders.Width / 5); - LevelData.MinMainPathWidth = minWidth; + minMainPathWidth = Math.Min(minMainPathWidth, borders.Width / 5); + LevelData.MinMainPathWidth = minMainPathWidth; Rectangle pathBorders = borders; pathBorders.Inflate( - -Math.Min(Math.Min(minWidth * 2, MaxSubmarineWidth), borders.Width / 5), - -Math.Min(minWidth, borders.Height / 5)); + -Math.Min(Math.Min(minMainPathWidth * 2, MaxSubmarineWidth), borders.Width / 5), + -Math.Min(minMainPathWidth, borders.Height / 5)); if (pathBorders.Width <= 0) { throw new InvalidOperationException($"The width of the level's path area is invalid ({pathBorders.Width})"); } if (pathBorders.Height <= 0) { throw new InvalidOperationException($"The height of the level's path area is invalid ({pathBorders.Height})"); } startPosition = new Point( - (int)MathHelper.Lerp(minWidth, borders.Width - minWidth, GenerationParams.StartPosition.X), - (int)MathHelper.Lerp(borders.Bottom - minWidth, borders.Y + minWidth, GenerationParams.StartPosition.Y)); + (int)MathHelper.Lerp(minMainPathWidth, borders.Width - minMainPathWidth, GenerationParams.StartPosition.X), + (int)MathHelper.Lerp(borders.Bottom - Math.Max(minMainPathWidth, ExitDistance * 1.5f), borders.Y + minMainPathWidth, GenerationParams.StartPosition.Y)); + startExitPosition = new Vector2(startPosition.X, borders.Bottom); + endPosition = new Point( - (int)MathHelper.Lerp(minWidth, borders.Width - minWidth, GenerationParams.EndPosition.X), - (int)MathHelper.Lerp(borders.Bottom - minWidth, borders.Y + minWidth, GenerationParams.EndPosition.Y)); + (int)MathHelper.Lerp(minMainPathWidth, borders.Width - minMainPathWidth, GenerationParams.EndPosition.X), + (int)MathHelper.Lerp(borders.Bottom - Math.Max(minMainPathWidth, ExitDistance * 1.5f), borders.Y + minMainPathWidth, GenerationParams.EndPosition.Y)); + endExitPosition = new Vector2(endPosition.X, borders.Bottom); EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); @@ -424,14 +476,75 @@ namespace Barotrauma Tunnel mainPath = new Tunnel( TunnelType.MainPath, GeneratePathNodes(startPosition, endPosition, pathBorders, null, GenerationParams.MainPathVariance), - minWidth, parentTunnel: null); + minMainPathWidth, parentTunnel: null); Tunnels.Add(mainPath); + Tunnel startPath = null, endPath = null, endHole = null; + if (GenerationParams.StartPosition.Y < 0.5f && (Mirrored ? !HasEndOutpost() : !HasStartOutpost())) + { + startPath = new Tunnel( + TunnelType.SidePath, + new List() { startExitPosition.ToPoint(), startPosition }, + minWidth / 2, parentTunnel: mainPath); + Tunnels.Add(startPath); + } + else + { + startExitPosition = StartPosition; + } + if (GenerationParams.EndPosition.Y < 0.5f && (Mirrored ? !HasStartOutpost() : !HasEndOutpost())) + { + endPath = new Tunnel( + TunnelType.SidePath, + new List() { endPosition, endExitPosition.ToPoint() }, + minWidth / 2, parentTunnel: mainPath); + Tunnels.Add(endPath); + } + else + { + endExitPosition = EndPosition; + } + + if (GenerationParams.CreateHoleNextToEnd) + { + if (Mirrored) + { + endHole = new Tunnel( + TunnelType.SidePath, + new List() { startPosition, startExitPosition.ToPoint(), new Point(0, Size.Y) }, + minWidth / 2, parentTunnel: mainPath); + } + else + { + endHole = new Tunnel( + TunnelType.SidePath, + new List() { endPosition, endExitPosition.ToPoint(), Size }, + minWidth / 2, parentTunnel: mainPath); + } + Tunnels.Add(endHole); + } + + //create a tunnel from the lowest point in the main path to the abyss + //to ensure there's a way to the abyss in all levels + if (GenerationParams.CreateHoleToAbyss) + { + Point lowestPoint = mainPath.Nodes.First(); + foreach (var pathNode in mainPath.Nodes) + { + if (pathNode.Y < lowestPoint.Y) { lowestPoint = pathNode; } + } + var abyssTunnel = new Tunnel( + TunnelType.SidePath, + new List() { lowestPoint, new Point(lowestPoint.X, 0) }, + minWidth / 2, parentTunnel: mainPath); + Tunnels.Add(abyssTunnel); + } + int sideTunnelCount = Rand.Range(GenerationParams.SideTunnelCount.X, GenerationParams.SideTunnelCount.Y + 1, Rand.RandSync.Server); for (int j = 0; j < sideTunnelCount; j++) { if (mainPath.Nodes.Count < 4) { break; } - var validTunnels = Tunnels.FindAll(t => t.Type != TunnelType.Cave); + var validTunnels = Tunnels.FindAll(t => t.Type != TunnelType.Cave && t != startPath && t != endPath && t != endHole); Tunnel tunnelToBranchOff = validTunnels[Rand.Int(validTunnels.Count, Rand.RandSync.Server)]; if (tunnelToBranchOff == null) { tunnelToBranchOff = mainPath; } @@ -445,7 +558,8 @@ namespace Barotrauma } CalculateTunnelDistanceField(density: 1000); - GenerateSeaFloorPositions(mirror); + GenerateSeaFloorPositions(); + GenerateAbyssArea(); GenerateCaves(mainPath); EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); @@ -525,38 +639,10 @@ namespace Barotrauma siteCoordsY.Add(caveSiteY); } } - } - } } - /*int caveSiteInterval = 500; - foreach (Cave cave in Caves) - { - for (int x = cave.Area.X; x < cave.Area.Right; x += caveSiteInterval) - { - for (int y = cave.Area.Y; y < cave.Area.Bottom; y += caveSiteInterval) - { - int siteX = x + Rand.Int(caveSiteInterval / 2); - int siteY = y + Rand.Int(caveSiteInterval / 2); - - bool tooClose = false; - for (int i = 0; i pathCells = new List(); foreach (Tunnel tunnel in Tunnels) { - CaveGenerator.GeneratePath(tunnel, cells, cellGrid, GridCellSize, pathBorders); + CaveGenerator.GeneratePath(tunnel, this); if (tunnel.Type == TunnelType.MainPath || tunnel.Type == TunnelType.SidePath) { - var distinctCells = tunnel.Cells.Distinct().ToList(); - for (int i = 2; i < distinctCells.Count; i += 3) + if (tunnel != startPath && tunnel != endPath && tunnel != endHole) { - PositionsOfInterest.Add(new InterestingPosition( - new Point((int)distinctCells[i].Site.Coord.X, (int)distinctCells[i].Site.Coord.Y), - tunnel.Type == TunnelType.MainPath ? PositionType.MainPath : PositionType.SidePath, - Caves.Find(cave => cave.Tunnels.Contains(tunnel)))); + var distinctCells = tunnel.Cells.Distinct().ToList(); + for (int i = 2; i < distinctCells.Count; i += 3) + { + PositionsOfInterest.Add(new InterestingPosition( + new Point((int)distinctCells[i].Site.Coord.X, (int)distinctCells[i].Site.Coord.Y), + tunnel.Type == TunnelType.MainPath ? PositionType.MainPath : PositionType.SidePath, + Caves.Find(cave => cave.Tunnels.Contains(tunnel)))); + } } } GenerateWaypoints(tunnel, parentTunnel: tunnel.ParentTunnel); @@ -639,7 +731,10 @@ namespace Barotrauma var potentialIslands = new List(); foreach (var cell in pathCells) { - if (GetDistToTunnel(cell.Center, mainPath) < minWidth) { continue; } + if (GetDistToTunnel(cell.Center, mainPath) < minMainPathWidth || + (startPath != null && GetDistToTunnel(cell.Center, startPath) < minMainPathWidth) || + (endPath != null && GetDistToTunnel(cell.Center, endPath) < minMainPathWidth) || + (endHole != null && GetDistToTunnel(cell.Center, endHole) < minMainPathWidth)) { continue; } if (cell.Edges.Any(e => e.AdjacentCell(cell)?.CellType != CellType.Path || e.NextToCave)) { continue; } potentialIslands.Add(cell); } @@ -661,6 +756,7 @@ namespace Barotrauma } startPosition.X = (int)pathCells[0].Site.Coord.X; + startExitPosition.X = startPosition.X; EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); @@ -680,7 +776,7 @@ namespace Barotrauma }); int xPadding = borders.Width / 5; - pathCells.AddRange(CreateHoles(GenerationParams.BottomHoleProbability, new Rectangle(xPadding, 0, borders.Width - xPadding * 2, Size.Y / 2), minWidth)); + pathCells.AddRange(CreateHoles(GenerationParams.BottomHoleProbability, new Rectangle(xPadding, 0, borders.Width - xPadding * 2, Size.Y / 2), minMainPathWidth)); foreach (VoronoiCell cell in cells) { @@ -688,14 +784,18 @@ namespace Barotrauma cell.Edges.ForEach(e => e.OutsideLevel = true); } + foreach (AbyssIsland abyssIsland in AbyssIslands) + { + cells.AddRange(abyssIsland.Cells); + } + //---------------------------------------------------------------------------------- // initialize the cells that are still left and insert them into the cell grid //---------------------------------------------------------------------------------- - + foreach (VoronoiCell cell in pathCells) { cell.Edges.ForEach(e => e.OutsideLevel = false); - cell.CellType = CellType.Path; cells.Remove(cell); } @@ -744,6 +844,11 @@ namespace Barotrauma } } + foreach (AbyssIsland island in AbyssIslands) + { + island.Area = new Rectangle(borders.Width - island.Area.Right, island.Area.Y, island.Area.Width, island.Area.Height); + } + foreach (Cave cave in Caves) { cave.Area = new Rectangle(borders.Width - cave.Area.Right, cave.Area.Y, cave.Area.Width, cave.Area.Height); @@ -777,8 +882,19 @@ namespace Barotrauma waypoint.Move(new Vector2((borders.Width / 2 - waypoint.Position.X) * 2, 0.0f)); } + for (int i = 0; i < bottomPositions.Count; i++) + { + bottomPositions[i] = new Point(borders.Size.X - bottomPositions[i].X, bottomPositions[i].Y); + } + bottomPositions.Reverse(); + startPosition.X = borders.Width - startPosition.X; endPosition.X = borders.Width - endPosition.X; + + startExitPosition.X = borders.Width - startExitPosition.X; + endExitPosition.X = borders.Width - endExitPosition.X; + + CalculateTunnelDistanceField(density: 1000); } foreach (VoronoiCell cell in cells) @@ -794,7 +910,10 @@ namespace Barotrauma float destructibleWallRatio = MathHelper.Lerp(0.2f, 1.0f, LevelData.Difficulty / 100.0f); foreach (Cave cave in Caves) { - CreatePathToClosestTunnel(cave.StartPos); + if (cave.Area.Y > 0) + { + CreatePathToClosestTunnel(cave.StartPos); + } List caveCells = new List(); caveCells.AddRange(cave.Tunnels.SelectMany(t => t.Cells)); @@ -837,8 +956,8 @@ namespace Barotrauma { if (pos.PositionType != PositionType.MainPath && pos.PositionType != PositionType.SidePath) { continue; } if (pos.Position.X < 5000 || pos.Position.X > Size.X - 5000) { continue; } - if (Math.Abs(pos.Position.X - StartPosition.X) < minWidth * 2 || Math.Abs(pos.Position.X - EndPosition.X) < minWidth * 2) { continue; } - if (GetTooCloseCells(pos.Position.ToVector2(), minWidth * 0.7f).Count > 0) { continue; } + if (Math.Abs(pos.Position.X - StartPosition.X) < minMainPathWidth * 2 || Math.Abs(pos.Position.X - EndPosition.X) < minMainPathWidth * 2) { continue; } + if (GetTooCloseCells(pos.Position.ToVector2(), minMainPathWidth * 0.7f).Count > 0) { continue; } iceChunkPositions.Add(pos.Position); } @@ -849,7 +968,7 @@ namespace Barotrauma float chunkRadius = Rand.Range(500.0f, 1000.0f, Rand.RandSync.Server); var vertices = CaveGenerator.CreateRandomChunk(chunkRadius, 8, chunkRadius * 0.8f); var chunk = CreateIceChunk(vertices, selectedPos.ToVector2()); - chunk.MoveAmount = new Vector2(0.0f, minWidth * 0.7f); + chunk.MoveAmount = new Vector2(0.0f, minMainPathWidth * 0.7f); chunk.MoveSpeed = Rand.Range(100.0f, 200.0f, Rand.RandSync.Server); ExtraWalls.Add(chunk); iceChunkPositions.Remove(selectedPos); @@ -885,43 +1004,46 @@ namespace Barotrauma #if CLIENT - List, Cave>> cellBatches = new List, Cave>> + List<(List cells, Cave parentCave)> cellBatches = new List<(List, Cave)> { - new Pair, Cave>(cellsWithBody.ToList(), null) + (cellsWithBody.ToList(), null) }; foreach (Cave cave in Caves) { - var newCellBatch = new Pair, Cave>(new List(), cave); + (List cells, Cave parentCave) newCellBatch = (new List(), cave); foreach (var caveCell in cave.Tunnels.SelectMany(t => t.Cells)) { foreach (var edge in caveCell.Edges) { if (!edge.NextToCave) { continue; } - if (edge.Cell1?.CellType == CellType.Solid && !newCellBatch.First.Contains(edge.Cell1)) + if (edge.Cell1?.CellType == CellType.Solid && !newCellBatch.cells.Contains(edge.Cell1)) { - cellBatches.ForEach(cb => cb.First.Remove(edge.Cell1)); - newCellBatch.First.Add(edge.Cell1); + Debug.Assert(cellsWithBody.Contains(edge.Cell1)); + cellBatches.ForEach(cb => cb.cells.Remove(edge.Cell1)); + newCellBatch.cells.Add(edge.Cell1); } - if (edge.Cell2?.CellType == CellType.Solid && !newCellBatch.First.Contains(edge.Cell2)) + if (edge.Cell2?.CellType == CellType.Solid && !newCellBatch.cells.Contains(edge.Cell2)) { - cellBatches.ForEach(cb => cb.First.Remove(edge.Cell2)); - newCellBatch.First.Add(edge.Cell2); + Debug.Assert(cellsWithBody.Contains(edge.Cell2)); + cellBatches.ForEach(cb => cb.cells.Remove(edge.Cell2)); + newCellBatch.cells.Add(edge.Cell2); } } } - if (newCellBatch.First.Any()) + if (newCellBatch.cells.Any()) { cellBatches.Add(newCellBatch); } } - cellBatches.RemoveAll(cb => !cb.First.Any()); + cellBatches.RemoveAll(cb => !cb.cells.Any()); - Debug.Assert(cellsWithBody.Count == cellBatches.Sum(cb => cb.First.Count)); + int totalCellsInBatches = cellBatches.Sum(cb => cb.cells.Count); + Debug.Assert(cellsWithBody.Count == totalCellsInBatches); List> triangleLists = new List>(); - foreach (Pair, Cave> cellBatch in cellBatches) + foreach ((List cells, Cave cave) cellBatch in cellBatches) { - bodies.Add(CaveGenerator.GeneratePolygons(cellBatch.First, this, out List triangles)); + bodies.Add(CaveGenerator.GeneratePolygons(cellBatch.cells, this, out List triangles)); triangleLists.Add(triangles); } #else @@ -952,9 +1074,9 @@ namespace Barotrauma { renderer.SetVertices( CaveGenerator.GenerateWallVertices(triangleLists[i], GenerationParams, zCoord: 0.9f).ToArray(), - CaveGenerator.GenerateWallEdgeVertices(cellBatches[i].First, this, zCoord: 0.9f).ToArray(), - cellBatches[i].Second?.CaveGenerationParams?.WallSprite == null ? GenerationParams.WallSprite.Texture : cellBatches[i].Second.CaveGenerationParams.WallSprite.Texture, - cellBatches[i].Second?.CaveGenerationParams?.WallEdgeSprite == null ? GenerationParams.WallEdgeSprite.Texture : cellBatches[i].Second.CaveGenerationParams.WallEdgeSprite.Texture, + CaveGenerator.GenerateWallEdgeVertices(cellBatches[i].cells, this, zCoord: 0.9f).ToArray(), + cellBatches[i].parentCave?.CaveGenerationParams?.WallSprite == null ? GenerationParams.WallSprite.Texture : cellBatches[i].parentCave.CaveGenerationParams.WallSprite.Texture, + cellBatches[i].parentCave?.CaveGenerationParams?.WallEdgeSprite == null ? GenerationParams.WallEdgeSprite.Texture : cellBatches[i].parentCave.CaveGenerationParams.WallEdgeSprite.Texture, GenerationParams.WallColor); } #endif @@ -1010,17 +1132,23 @@ namespace Barotrauma if (mirror) { - Point temp = startPosition; + Point tempP = startPosition; startPosition = endPosition; - endPosition = temp; + endPosition = tempP; + + Vector2 tempV = startExitPosition; + startExitPosition = endExitPosition; + endExitPosition = tempV; } if (StartOutpost != null) { - startPosition = new Point((int)StartOutpost.WorldPosition.X, (int)StartOutpost.WorldPosition.Y); + startExitPosition = StartOutpost.WorldPosition; + startPosition = startExitPosition.ToPoint(); } if (EndOutpost != null) { - endPosition = new Point((int)EndOutpost.WorldPosition.X, (int)EndOutpost.WorldPosition.Y); + endExitPosition = EndOutpost.WorldPosition; + endPosition = endExitPosition.ToPoint(); } CreateWrecks(); @@ -1156,18 +1284,6 @@ namespace Barotrauma List toBeRemoved = new List(); foreach (VoronoiCell cell in cells) { - if (GenerationParams.CreateHoleNextToEnd) - { - if ((!Mirrored && cell.Center.X > endPosition.X) || (Mirrored && cell.Center.X < StartPosition.X)) - { - if (cell.Edges.Any(e => e.Point1.Y > Size.Y - submarineSize || e.Point2.Y > Size.Y - submarineSize)) - { - toBeRemoved.Add(cell); - continue; - } - } - } - if (cell.Edges.Any(e => e.NextToCave)) { continue; } if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) > holeProbability) { continue; } if (!limits.Contains(cell.Site.Coord.X, cell.Site.Coord.Y)) { continue; } @@ -1217,7 +1333,6 @@ namespace Barotrauma for (int i = 0; i < tunnel.Cells.Count; i++) { tunnel.Cells[i].CellType = CellType.Path; - var newWaypoint = new WayPoint(new Rectangle((int)tunnel.Cells[i].Site.Coord.X, (int)tunnel.Cells[i].Center.Y, 10, 10), null) { Tunnel = tunnel @@ -1274,8 +1389,23 @@ namespace Barotrauma ConvertUnits.ToSimUnits(wayPoint.WorldPosition), ConvertUnits.ToSimUnits(closestWaypoint.WorldPosition), collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) == null) { - wayPoint.linkedTo.Add(closestWaypoint); - closestWaypoint.linkedTo.Add(wayPoint); + Vector2 diff = closestWaypoint.WorldPosition - wayPoint.WorldPosition; + float dist = diff.Length(); + float step = ConvertUnits.ToDisplayUnits(Steering.AutopilotMinDistToPathNode) * 0.8f; + + WayPoint prevWaypoint = wayPoint; + for (float x = step; x < dist - step; x += step) + { + var newWaypoint = new WayPoint(wayPoint.WorldPosition + (diff / dist * x), SpawnType.Path, submarine: null) + { + Tunnel = tunnel + }; + prevWaypoint.linkedTo.Add(newWaypoint); + newWaypoint.linkedTo.Add(prevWaypoint); + prevWaypoint = newWaypoint; + } + prevWaypoint.linkedTo.Add(closestWaypoint); + closestWaypoint.linkedTo.Add(prevWaypoint); } } } @@ -1333,11 +1463,178 @@ namespace Barotrauma } } if (tooClose) { tooCloseCells.Add(cell); } - } + } return tooCloseCells.ToList(); } - private void GenerateSeaFloorPositions(bool mirror) + private void GenerateAbyssPositions() + { + int count = 10; + for (int i = 0; i < count; i++) + { + float xPos = MathHelper.Lerp(borders.X, borders.Right, i / (float)(count - 1)); + float seaFloorPos = GetBottomPosition(xPos).Y; + + //above the bottom of the level = can't place a point here + if (seaFloorPos > AbyssStart) { continue; } + + float yPos = MathHelper.Lerp(AbyssStart, Math.Max(seaFloorPos, AbyssArea.Y), Rand.Range(0.2f, 1.0f, Rand.RandSync.Server)); + + foreach (var abyssIsland in AbyssIslands) + { + if (abyssIsland.Area.Contains(new Point((int)xPos, (int)yPos))) + { + xPos = abyssIsland.Area.Center.X + (int)(Rand.Int(1, Rand.RandSync.Server) == 0 ? abyssIsland.Area.Width * -0.6f : 0.6f); + } + } + + PositionsOfInterest.Add(new InterestingPosition(new Point((int)xPos, (int)yPos), PositionType.Abyss)); + } + } + + private void GenerateAbyssArea() + { + int abyssStartY = borders.Y - 5000; + int abyssEndY = Math.Max(abyssStartY - 100000, BottomPos + 1000); + int abyssHeight = abyssStartY - abyssEndY; + + if (abyssHeight < 0) + { + abyssStartY = borders.Y; + abyssEndY = BottomPos; + if (abyssStartY - abyssEndY < 1000) + { +#if DEBUG + DebugConsole.ThrowError("Not enough space to generate Abyss in the level. You may want to move the ocean floor deeper."); +#else + DebugConsole.AddWarning("Not enough space to generate Abyss in the level. You may want to move the ocean floor deeper."); +#endif + } + } + else + { + //if the bottom of the abyss area is below crush depth, try to move it up to keep (most) of the abyss content above crush depth + if (abyssEndY + CrushDepth < 0) + { + abyssEndY += Math.Min(-(abyssEndY + (int)CrushDepth), abyssHeight / 2); + } + + if (abyssStartY - abyssEndY < 10000) + { + abyssStartY = borders.Y; + } + } + + AbyssArea = new Rectangle(borders.X, abyssEndY, borders.Width, abyssStartY - abyssEndY); + } + + private void GenerateAbyssGeometry() + { + //TODO: expose island parameters + + Voronoi voronoi = new Voronoi(1.0); + Point siteInterval = new Point(500, 500); + Point siteVariance = new Point(200, 200); + + Point islandSize = Vector2.Lerp( + GenerationParams.AbyssIslandSizeMin.ToVector2(), + GenerationParams.AbyssIslandSizeMax.ToVector2(), + Rand.Range(0.0f, 1.0f, Rand.RandSync.Server)).ToPoint(); + + if (AbyssArea.Height < islandSize.Y) { return; } + + int islandCount = GenerationParams.AbyssIslandCount; + for (int i = 0; i < islandCount; i++) + { + Point islandPosition = Point.Zero; + Rectangle islandArea = new Rectangle(islandPosition, islandSize); + + //prevent overlaps + int tries = 0; + const int MaxTries = 20; + do + { + islandPosition = new Point( + Rand.Range(AbyssArea.X, AbyssArea.Right - islandSize.X, Rand.RandSync.Server), + Rand.Range(AbyssArea.Y, AbyssArea.Bottom - islandSize.Y, Rand.RandSync.Server)); + + //move the island above the sea floor geometry + islandPosition.Y = Math.Max(islandPosition.Y, (int)GetBottomPosition(islandPosition.X).Y + 500); + islandPosition.Y = Math.Max(islandPosition.Y, (int)GetBottomPosition(islandPosition.X + islandArea.Width).Y + 500); + + islandArea.Location = islandPosition; + + tries++; + } while ((AbyssIslands.Any(island => island.Area.Intersects(islandArea)) || islandArea.Bottom > AbyssArea.Bottom) && tries < MaxTries); + + if (tries >= MaxTries) + { + break; + } + + if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) > GenerationParams.AbyssIslandCaveProbability) + { + float radiusVariance = Math.Min(islandArea.Width, islandArea.Height) * 0.1f; + var vertices = CaveGenerator.CreateRandomChunk(islandArea.Width - (int)(radiusVariance * 2), islandArea.Height - (int)(radiusVariance * 2), 16, radiusVariance: radiusVariance); + Vector2 position = islandArea.Center.ToVector2(); + for (int j = 0; j < vertices.Count; j++) + { + vertices[j] += position; + } + var newChunk = new LevelWall(vertices, GenerationParams.WallColor, this); + AbyssIslands.Add(new AbyssIsland(islandArea, newChunk.Cells)); + continue; + } + + var siteCoordsX = new List((islandSize.Y / siteInterval.Y) * (islandSize.X / siteInterval.Y)); + var siteCoordsY = new List((islandSize.Y / siteInterval.Y) * (islandSize.X / siteInterval.Y)); + + for (int x = islandArea.X; x < islandArea.Right; x += siteInterval.X) + { + for (int y = islandArea.Y; y < islandArea.Bottom; y += siteInterval.Y) + { + siteCoordsX.Add(x + Rand.Range(-siteVariance.X, siteVariance.X, Rand.RandSync.Server)); + siteCoordsY.Add(y + Rand.Range(-siteVariance.Y, siteVariance.Y, Rand.RandSync.Server)); + } + } + + var graphEdges = voronoi.MakeVoronoiGraph(siteCoordsX.ToArray(), siteCoordsY.ToArray(), islandArea); + var islandCells = CaveGenerator.GraphEdgesToCells(graphEdges, islandArea, GridCellSize, out var cellGrid); + + //make the island elliptical + for (int j = islandCells.Count - 1; j >= 0; j--) + { + var cell = islandCells[j]; + double xDiff = (cell.Site.Coord.X - islandArea.Center.X) / (islandArea.Width * 0.5); + double yDiff = (cell.Site.Coord.Y - islandArea.Center.Y) / (islandArea.Height * 0.5); + + //a conical stalactite-like shape at the bottom + if (yDiff < 0) { xDiff += xDiff * Math.Abs(yDiff); } + + double normalizedDist = Math.Sqrt(xDiff * xDiff + yDiff * yDiff); + if (normalizedDist > 0.95 || + cell.Edges.Any(e => MathUtils.NearlyEqual(e.Point1.X, islandArea.X)) || + cell.Edges.Any(e => MathUtils.NearlyEqual(e.Point1.X, islandArea.Right)) || + cell.Edges.Any(e => MathUtils.NearlyEqual(e.Point1.Y, islandArea.Y)) || + cell.Edges.Any(e => MathUtils.NearlyEqual(e.Point1.Y, islandArea.Bottom))) + { + islandCells[j].CellType = CellType.Removed; + islandCells.RemoveAt(j); + } + } + + var caveParams = CaveGenerationParams.GetRandom(GenerationParams, abyss: true, rand: Rand.RandSync.Server); + + float caveScaleRelativeToIsland = 0.7f; + GenerateCave( + caveParams, Tunnels.First(), + new Point(islandArea.Center.X, islandArea.Center.Y + (int)(islandArea.Size.Y * (1.0f - caveScaleRelativeToIsland)) / 2), + new Point((int)(islandArea.Size.X * caveScaleRelativeToIsland), (int)(islandArea.Size.Y * caveScaleRelativeToIsland))); + AbyssIslands.Add(new AbyssIsland(islandArea, islandCells)); + } + } + + private void GenerateSeaFloorPositions() { BottomPos = GenerationParams.SeaFloorDepth; SeaFloorTopPos = BottomPos; @@ -1372,14 +1669,6 @@ namespace Barotrauma currInverval /= 2; } - if (mirror) - { - for (int i = 0; i < bottomPositions.Count; i++) - { - bottomPositions[i] = new Point(borders.Size.X - bottomPositions[i].X, bottomPositions[i].Y); - } - } - SeaFloorTopPos = bottomPositions.Max(p => p.Y); } @@ -1403,87 +1692,100 @@ namespace Barotrauma { for (int i = 0; i < GenerationParams.CaveCount; i++) { - var caveParams = CaveGenerationParams.GetRandom(GenerationParams, Rand.RandSync.Server); + var caveParams = CaveGenerationParams.GetRandom(GenerationParams, abyss: false, rand: Rand.RandSync.Server); Point caveSize = new Point( Rand.Range(caveParams.MinWidth, caveParams.MaxWidth, Rand.RandSync.Server), Rand.Range(caveParams.MinHeight, caveParams.MaxHeight, Rand.RandSync.Server)); - int radius = Math.Max(caveSize.X, caveSize.Y) / 2; int padding = (int)(caveSize.X * 1.2f); Rectangle allowedArea = new Rectangle(padding, padding, Size.X - padding * 2, Size.Y - padding * 2); - var cavePos = FindPosAwayFromMainPath((parentTunnel.MinWidth + radius) * 1.2f, asCloseAsPossible: true, allowedArea); + int radius = Math.Max(caveSize.X, caveSize.Y) / 2; + var cavePos = FindPosAwayFromMainPath((parentTunnel.MinWidth + radius) * 1.5f, asCloseAsPossible: true, allowedArea); - Point closestParentNode = parentTunnel.Nodes.First(); - double closestDist = double.PositiveInfinity; - foreach (Point node in parentTunnel.Nodes) - { - double dist = MathUtils.DistanceSquared((double)node.X, (double)node.Y, (double)cavePos.X, (double)cavePos.Y); - if (dist < closestDist) - { - closestParentNode = node; - closestDist = dist; - } - } - - Rectangle caveArea = new Rectangle(cavePos - new Point(caveSize.X / 2, caveSize.Y / 2), caveSize); - MathUtils.GetLineRectangleIntersection(closestParentNode.ToVector2(), cavePos.ToVector2(), new Rectangle(caveArea.X, caveArea.Y + caveArea.Height, caveArea.Width, caveArea.Height), out Vector2 caveStartPosVector); - - Point caveStartPos = caveStartPosVector.ToPoint(); - Point caveEndPos = cavePos - (caveStartPos - cavePos); - - Cave cave = new Cave(caveParams, caveArea, caveStartPos, caveEndPos); - Caves.Add(cave); - - var caveSegments = MathUtils.GenerateJaggedLine( - caveStartPos.ToVector2(), caveEndPos.ToVector2(), - iterations: 3, - offsetAmount: Vector2.Distance(caveStartPos.ToVector2(), caveEndPos.ToVector2()) * 0.75f); - if (!caveSegments.Any()) { continue; } - - List caveBranches = new List(); - - var tunnel = new Tunnel(TunnelType.Cave, SegmentsToNodes(caveSegments), 100, parentTunnel); - Tunnels.Add(tunnel); - caveBranches.Add(tunnel); - - int branches = Rand.Range(caveParams.MinBranchCount, caveParams.MaxBranchCount, Rand.RandSync.Server); - for (int j = 0; j < branches; j++) - { - Tunnel parentBranch = caveBranches.GetRandom(Rand.RandSync.Server); - Vector2 branchStartPos = parentBranch.Nodes[Rand.Int(parentBranch.Nodes.Count / 2, Rand.RandSync.Server)].ToVector2(); - Vector2 branchEndPos = parentBranch.Nodes[Rand.Range(parentBranch.Nodes.Count / 2, parentBranch.Nodes.Count, Rand.RandSync.Server)].ToVector2(); - var branchSegments = MathUtils.GenerateJaggedLine( - branchStartPos, branchEndPos, - iterations: 3, - offsetAmount: Vector2.Distance(branchStartPos, branchEndPos) * 0.75f); - if (!branchSegments.Any()) { continue; } - - var branch = new Tunnel(TunnelType.Cave, SegmentsToNodes(branchSegments), 0, parentBranch); - Tunnels.Add(branch); - caveBranches.Add(branch); - } - - foreach (Tunnel branch in caveBranches) - { - PositionsOfInterest.Add(new InterestingPosition(branch.Nodes.Last(), PositionType.Cave, cave)); - cave.Tunnels.Add(branch); - } - - static List SegmentsToNodes(List segments) - { - List nodes = new List(); - foreach (Vector2[] segment in segments) - { - nodes.Add(segment[0].ToPoint()); - } - nodes.Add(segments.Last()[1].ToPoint()); - return nodes; - } + GenerateCave(caveParams, parentTunnel, cavePos, caveSize); CalculateTunnelDistanceField(density: 1000); } } + private void GenerateCave(CaveGenerationParams caveParams, Tunnel parentTunnel, Point cavePos, Point caveSize) + { + Rectangle caveArea = new Rectangle(cavePos - new Point(caveSize.X / 2, caveSize.Y / 2), caveSize); + Point closestParentNode = parentTunnel.Nodes.First(); + double closestDist = double.PositiveInfinity; + foreach (Point node in parentTunnel.Nodes) + { + if (caveArea.Contains(node)) { continue; } + double dist = MathUtils.DistanceSquared((double)node.X, (double)node.Y, (double)cavePos.X, (double)cavePos.Y); + if (dist < closestDist) + { + closestParentNode = node; + closestDist = dist; + } + } + + if (!MathUtils.GetLineRectangleIntersection(closestParentNode.ToVector2(), cavePos.ToVector2(), new Rectangle(caveArea.X, caveArea.Y + caveArea.Height, caveArea.Width, caveArea.Height), out Vector2 caveStartPosVector)) + { + caveStartPosVector = caveArea.Location.ToVector2(); + } + + Point caveStartPos = caveStartPosVector.ToPoint(); + Point caveEndPos = cavePos - (caveStartPos - cavePos); + + Cave cave = new Cave(caveParams, caveArea, caveStartPos, caveEndPos); + Caves.Add(cave); + + var caveSegments = MathUtils.GenerateJaggedLine( + caveStartPos.ToVector2(), caveEndPos.ToVector2(), + iterations: 3, + offsetAmount: Vector2.Distance(caveStartPos.ToVector2(), caveEndPos.ToVector2()) * 0.75f, + bounds: caveArea); + + if (!caveSegments.Any()) { return; } + + List caveBranches = new List(); + + var tunnel = new Tunnel(TunnelType.Cave, SegmentsToNodes(caveSegments), 100, parentTunnel); + Tunnels.Add(tunnel); + caveBranches.Add(tunnel); + + int branches = Rand.Range(caveParams.MinBranchCount, caveParams.MaxBranchCount, Rand.RandSync.Server); + for (int j = 0; j < branches; j++) + { + Tunnel parentBranch = caveBranches.GetRandom(Rand.RandSync.Server); + Vector2 branchStartPos = parentBranch.Nodes[Rand.Int(parentBranch.Nodes.Count / 2, Rand.RandSync.Server)].ToVector2(); + Vector2 branchEndPos = parentBranch.Nodes[Rand.Range(parentBranch.Nodes.Count / 2, parentBranch.Nodes.Count, Rand.RandSync.Server)].ToVector2(); + var branchSegments = MathUtils.GenerateJaggedLine( + branchStartPos, branchEndPos, + iterations: 3, + offsetAmount: Vector2.Distance(branchStartPos, branchEndPos) * 0.75f, + bounds: caveArea); + if (!branchSegments.Any()) { continue; } + + var branch = new Tunnel(TunnelType.Cave, SegmentsToNodes(branchSegments), 0, parentBranch); + Tunnels.Add(branch); + caveBranches.Add(branch); + } + + foreach (Tunnel branch in caveBranches) + { + var node = branch.Nodes.Last(); + PositionsOfInterest.Add(new InterestingPosition(node, node.Y < AbyssArea.Bottom ? PositionType.AbyssCave : PositionType.Cave, cave)); + cave.Tunnels.Add(branch); + } + + static List SegmentsToNodes(List segments) + { + List nodes = new List(); + foreach (Vector2[] segment in segments) + { + nodes.Add(segment[0].ToPoint()); + } + nodes.Add(segments.Last()[1].ToPoint()); + return nodes; + } + } + private void GenerateRuin(Tunnel mainPath, bool mirror) { var ruinGenerationParams = RuinGenerationParams.GetRandom(); @@ -1566,30 +1868,30 @@ namespace Barotrauma private Point FindPosAwayFromMainPath(double minDistance, bool asCloseAsPossible, Rectangle? limits = null) { - var validPoints = distanceField.FindAll(d => d.Second >= minDistance && (limits == null || limits.Value.Contains(d.First))); - validPoints.RemoveAll(d => d.First.Y < GetBottomPosition(d.First.X).Y + minDistance); + var validPoints = distanceField.FindAll(d => d.distance >= minDistance && (limits == null || limits.Value.Contains(d.point))); + validPoints.RemoveAll(d => d.point.Y < GetBottomPosition(d.point.X).Y + minDistance); if (asCloseAsPossible || !validPoints.Any()) { if (!validPoints.Any()) { validPoints = distanceField; } - Pair closestPoint = null; + (Point position, double distance) closestPoint = validPoints.First(); foreach (var point in validPoints) { - if (closestPoint == null || point.Second < closestPoint.Second) + if (point.distance < closestPoint.distance) { closestPoint = point; } } - return closestPoint.First; + return closestPoint.position; } else { - return validPoints[Rand.Int(validPoints.Count, Rand.RandSync.Server)].First; + return validPoints[Rand.Int(validPoints.Count, Rand.RandSync.Server)].point; } } private void CalculateTunnelDistanceField(int density) { - distanceField = new List>(); + distanceField = new List<(Point point, double distance)>(); for (int x = 0; x < Size.X; x += density) { for (int y = 0; y < Size.Y; y += density) @@ -1604,8 +1906,10 @@ namespace Barotrauma } } shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)startPosition.X, (double)startPosition.Y)); + shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)startExitPosition.X, (double)borders.Bottom)); shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)endPosition.X, (double)endPosition.Y)); - distanceField.Add(new Pair(point, Math.Sqrt(shortestDistSqr))); + shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)endExitPosition.X, (double)borders.Bottom)); + distanceField.Add((point, Math.Sqrt(shortestDistSqr))); } } } @@ -1770,6 +2074,7 @@ namespace Barotrauma } } + public List AbyssResources { get; } = new List(); public struct ClusterLocation { public VoronoiCell Cell { get; } @@ -1807,8 +2112,8 @@ namespace Barotrauma { string levelName = GenerationParams.Identifier.ToLowerInvariant(); float minCommonness = float.MaxValue, maxCommonness = float.MinValue; - List> levelResources = new List>(); - var fixedResources = new List>(); + List<(ItemPrefab itemPrefab, float commonness)> levelResources = new List<(ItemPrefab itemPrefab, float commonness)>(); + var fixedResources = new List<(ItemPrefab itemPrefab, ItemPrefab.FixedQuantityResourceInfo resourceInfo)>(); foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) { if (itemPrefab.LevelCommonness.TryGetValue(levelName, out float commonness) || @@ -1817,34 +2122,63 @@ namespace Barotrauma if (commonness <= 0.0f) { continue; } if (commonness < minCommonness) { minCommonness = commonness; } if (commonness > maxCommonness) { maxCommonness = commonness; } - levelResources.Add(new Pair(itemPrefab, commonness)); + levelResources.Add((itemPrefab, commonness)); } else if (itemPrefab.LevelQuantity.TryGetValue(levelName, out var fixedQuantityResourceInfo) || itemPrefab.LevelQuantity.TryGetValue("", out fixedQuantityResourceInfo)) { - fixedResources.Add(new Tuple(itemPrefab, fixedQuantityResourceInfo)); + fixedResources.Add((itemPrefab, fixedQuantityResourceInfo)); } } + levelResources.Sort((x, y) => x.commonness.CompareTo(y.commonness)); DebugConsole.Log("Generating level resources..."); var allValidLocations = GetAllValidClusterLocations(); var maxResourceOverlap = 0.4f; - foreach (var fixedResource in fixedResources) + foreach (var (itemPrefab, resourceInfo) in fixedResources) { - for (int i = 0; i < fixedResource.Item2.ClusterQuantity; i++) + for (int i = 0; i < resourceInfo.ClusterQuantity; i++) { var location = allValidLocations.GetRandom(l => { if (l.Cell == null || l.Edge == null) { return false; } - if (fixedResource.Item2.IsIslandSpecifc && !l.Cell.Island) { return false; } - return fixedResource.Item2.ClusterSize <= GetMaxResourcesOnEdge(fixedResource.Item1, l, out _); + if (resourceInfo.IsIslandSpecifc && !l.Cell.Island) { return false; } + if (!resourceInfo.AllowAtStart && l.EdgeCenter.Y > StartPosition.Y && l.EdgeCenter.X < Size.X * 0.25f) { return false; } + if (l.EdgeCenter.Y < AbyssArea.Bottom) { return false; } + return resourceInfo.ClusterSize <= GetMaxResourcesOnEdge(itemPrefab, l, out _); }, randSync: Rand.RandSync.Server); if (location.Cell == null || location.Edge == null) { break; } - PlaceResources(fixedResource.Item1, fixedResource.Item2.ClusterSize, location, out _); + PlaceResources(itemPrefab, resourceInfo.ClusterSize, location, out _); + var locationIndex = allValidLocations.FindIndex(l => l.Equals(location)); + allValidLocations.RemoveAt(locationIndex); + } + } + + //place some of the least common resources in the abyss + AbyssResources.Clear(); + for (int j = 0; j < levelResources.Count && j < 5; j++) + { + for (int i = 0; i < 10; i++) + { + var (itemPrefab, commonness) = levelResources[j]; + var location = allValidLocations.GetRandom(l => + { + if (l.Cell == null || l.Edge == null) { return false; } + if (l.EdgeCenter.Y > AbyssArea.Bottom) { return false; } + l.InitializeResources(); + return l.Resources.Count <= GetMaxResourcesOnEdge(itemPrefab, l, out _); + }, randSync: Rand.RandSync.Server); + + if (location.Cell == null || location.Edge == null) { break; } + int clusterSize = Rand.Range(GenerationParams.ResourceClusterSizeRange.X, GenerationParams.ResourceClusterSizeRange.Y, Rand.RandSync.Server); + PlaceResources(itemPrefab, clusterSize, location, out var abyssResources); + var abyssClusterLocation = new ClusterLocation(location.Cell, location.Edge, initializeResourceList: true); + abyssClusterLocation.Resources.AddRange(abyssResources); + AbyssResources.Add(abyssClusterLocation); var locationIndex = allValidLocations.FindIndex(l => l.Equals(location)); allValidLocations.RemoveAt(locationIndex); } @@ -1852,7 +2186,7 @@ namespace Barotrauma PathPoints.Clear(); nextPathPointId = 0; - + foreach (Tunnel tunnel in Tunnels) { var tunnelLength = 0.0f; @@ -1974,6 +2308,7 @@ namespace Barotrauma { var validLocation = allValidLocations[i]; if (!IsNextToTunnelType(validLocation.Edge, pathPoint.TunnelType)) { continue; } + if (validLocation.EdgeCenter.Y < AbyssArea.Bottom) { continue; } var distanceSquaredToEdge = Vector2.DistanceSquared(pathPoint.Position, validLocation.EdgeCenter); // Edge isn't too far from the path point if (distanceSquaredToEdge > 3.0f * (intervalRange.Y * intervalRange.Y)) { continue; } @@ -2120,8 +2455,8 @@ namespace Barotrauma if (pathPoint.ClusterLocations.Count == 0) { selectedPrefab = ToolBox.SelectWeightedRandom( - levelResources.Select(it => it.First).ToList(), - levelResources.Select(it => it.Second).ToList(), + levelResources.Select(it => it.itemPrefab).ToList(), + levelResources.Select(it => it.commonness).ToList(), Rand.RandSync.Server); selectedPrefab.Tags.ForEach(t => { @@ -2134,18 +2469,18 @@ namespace Barotrauma else { var filteredResources = levelResources.Where(it => - !pathPoint.ResourceIds.Contains(it.First.Identifier) && - pathPoint.ResourceTags.Any() && it.First.Tags.Any(t => pathPoint.ResourceTags.Contains(t))); + !pathPoint.ResourceIds.Contains(it.itemPrefab.Identifier) && + pathPoint.ResourceTags.Any() && it.itemPrefab.Tags.Any(t => pathPoint.ResourceTags.Contains(t))); selectedPrefab = ToolBox.SelectWeightedRandom( - filteredResources.Select(it => it.First).ToList(), - filteredResources.Select(it => it.Second).ToList(), + filteredResources.Select(it => it.itemPrefab).ToList(), + filteredResources.Select(it => it.commonness).ToList(), Rand.RandSync.Server); } if (selectedPrefab == null) { return false; } // Create resources for the cluster - var commonness = levelResources.First(r => r.First == selectedPrefab).Second; + var commonness = levelResources.First(r => r.itemPrefab == selectedPrefab).commonness; var lerpAmount = MathUtils.InverseLerp(minCommonness, maxCommonness, commonness); var maxClusterSize = (int)MathHelper.Lerp(GenerationParams.ResourceClusterSizeRange.X, GenerationParams.ResourceClusterSizeRange.Y, lerpAmount); var maxFitOnEdge = GetMaxResourcesOnEdge(selectedPrefab, location, out var edgeLength); @@ -2356,7 +2691,7 @@ namespace Barotrauma if (Submarine.PickBody( ConvertUnits.ToSimUnits(startPos), ConvertUnits.ToSimUnits(endPos), - ExtraWalls.Where(w => w.Body?.BodyType == BodyType.Dynamic || w is DestructibleLevelWall).Select(w => w.Body), + ExtraWalls.Where(w => w.Body?.BodyType == BodyType.Dynamic || w is DestructibleLevelWall).Select(w => w.Body).Union(Submarine.Loaded.Where(s => s.Info.Type == SubmarineType.Player).Select(s => s.PhysicsBody.FarseerBody)), Physics.CollisionLevel | Physics.CollisionWall) != null) { position = ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) + Vector2.Normalize(startPos - endPos) * offsetFromWall; @@ -2488,12 +2823,13 @@ namespace Barotrauma public Vector2 GetBottomPosition(float xPosition) { int index = (int)Math.Floor(xPosition / Size.X * (bottomPositions.Count - 1)); - if (index < 0 || index >= bottomPositions.Count - 1) return new Vector2(xPosition, BottomPos); + if (index < 0 || index >= bottomPositions.Count - 1) { return new Vector2(xPosition, BottomPos); } - float yPos = MathHelper.Lerp( - bottomPositions[index].Y, - bottomPositions[index + 1].Y, - (xPosition - bottomPositions[index].X) / (bottomPositions[index + 1].X - bottomPositions[index].X)); + float t = (xPosition - bottomPositions[index].X) / (bottomPositions[index + 1].X - bottomPositions[index].X); + Debug.Assert(t < 1.0f); + t = MathHelper.Clamp(t, 0.0f, 1.0f); + + float yPos = MathHelper.Lerp(bottomPositions[index].Y, bottomPositions[index + 1].Y, t); return new Vector2(xPosition, yPos); } @@ -2540,10 +2876,42 @@ namespace Barotrauma tempCells.Add(cell); } } + + foreach (var abyssIsland in AbyssIslands) + { + if (abyssIsland.Area.X > worldPos.X + searchDepth * GridCellSize) { continue; } + if (abyssIsland.Area.Right < worldPos.X - searchDepth * GridCellSize) { continue; } + if (abyssIsland.Area.Y > worldPos.Y + searchDepth * GridCellSize) { continue; } + if (abyssIsland.Area.Bottom < worldPos.Y - searchDepth * GridCellSize) { continue; } + + tempCells.AddRange(abyssIsland.Cells); + } return tempCells; } + public VoronoiCell GetClosestCell(Vector2 worldPos) + { + double closestDist = double.MaxValue; + VoronoiCell closestCell = null; + int searchDepth = 2; + while (searchDepth < 5) + { + foreach (var cell in GetCells(worldPos, searchDepth)) + { + double dist = MathUtils.DistanceSquared(cell.Site.Coord.X, cell.Site.Coord.Y, worldPos.X, worldPos.Y); + if (dist < closestDist) + { + closestDist = dist; + closestCell = cell; + } + } + if (closestCell != null) { break; } + searchDepth++; + } + return closestCell; + } + private void CreatePathToClosestTunnel(Point pos) { VoronoiCell closestPathCell = null; @@ -2701,7 +3069,7 @@ namespace Barotrauma } } // Only spawn thalamus when the wreck has some thalamus items defined. - if (Rand.Value(Rand.RandSync.Server) <= Loaded.GenerationParams.ThalamusProbability && sub.GetItems(false).Any(i => i.Prefab.Category == MapEntityCategory.Thalamus)) + if (Rand.Value(Rand.RandSync.Server) <= Loaded.GenerationParams.ThalamusProbability && sub.GetItems(false).Any(i => i.Prefab.HasSubCategory("thalamus"))) { if (!sub.CreateWreckAI()) { @@ -2717,6 +3085,7 @@ namespace Barotrauma else if (type == SubmarineType.BeaconStation) { sub.ShowSonarMarker = false; + sub.DockedTo.ForEach(s => s.ShowSonarMarker = false); sub.PhysicsBody.FarseerBody.BodyType = BodyType.Static; sub.TeamID = CharacterTeamType.None; } @@ -2951,6 +3320,40 @@ namespace Barotrauma Debug.WriteLine($"{Wrecks.Count} wrecks created in { totalSW.ElapsedMilliseconds.ToString()} (ms)"); } + private bool HasStartOutpost() + { + if (preSelectedStartOutpost != null) { return true; } + if (LevelData.Type != LevelData.LevelType.Outpost) + { + //only create a starting outpost in campaign and tutorial modes +#if CLIENT + if (Screen.Selected != GameMain.LevelEditorScreen && !IsModeStartOutpostCompatible()) + { + return false; + } +#else + if (!IsModeStartOutpostCompatible()) + { + return false; + } +#endif + } + if (StartLocation != null && !StartLocation.Type.HasOutpost) + { + return false; + } + return true; + } + + private bool HasEndOutpost() + { + if (preSelectedStartOutpost != null) { return true; } + //don't create an end outpost for locations + if (LevelData.Type == LevelData.LevelType.Outpost) { return false; } + if (EndLocation != null && !EndLocation.Type.HasOutpost) { return false; } + return true; + } + private void CreateOutposts() { var outpostFiles = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.Outpost).ToList(); @@ -2970,25 +3373,11 @@ namespace Barotrauma bool isStart = (i == 0) == !Mirrored; if (isStart) { - //only create a starting outpost in campaign and tutorial modes -#if CLIENT - if (Screen.Selected != GameMain.LevelEditorScreen && !IsModeStartOutpostCompatible()) - { - continue; - } -#else - if (!IsModeStartOutpostCompatible()) - { - continue; - } -#endif - if (StartLocation != null && !StartLocation.Type.HasOutpost) { continue; } + if (!HasStartOutpost()) { continue; } } else { - //don't create an end outpost for locations - if (LevelData.Type == LevelData.LevelType.Outpost) { continue; } - if (EndLocation != null && !EndLocation.Type.HasOutpost) { continue; } + if (!HasEndOutpost()) { continue; } } SubmarineInfo outpostInfo; @@ -3008,7 +3397,7 @@ namespace Barotrauma { var suitableParams = OutpostGenerationParams.Params .Where(p => location == null || p.AllowedLocationTypes.Contains(location.Type.Identifier)); - if (suitableParams.Count() == 0) + if (!suitableParams.Any()) { suitableParams = OutpostGenerationParams.Params .Where(p => location == null || !p.AllowedLocationTypes.Any()); @@ -3044,6 +3433,14 @@ namespace Barotrauma DebugConsole.NewMessage($"Generating an outpost for the {(isStart ? "start" : "end")} of the level... (Location type: {locationType}, level type: {LevelData.Type})"); outpost = OutpostGenerator.Generate(outpostGenerationParams, locationType, onlyEntrance: LevelData.Type != LevelData.LevelType.Outpost); } + + foreach (string categoryToHide in locationType.HideEntitySubcategories) + { + foreach (MapEntity entityToHide in MapEntity.mapEntityList.Where(me => me.Submarine == outpost && (me.prefab?.HasSubCategory(categoryToHide) ?? false))) + { + entityToHide.HiddenInGame = true; + } + } } else { @@ -3134,13 +3531,22 @@ namespace Barotrauma if ((i == 0) == !Mirrored) { StartOutpost = outpost; - if (StartLocation != null) { outpost.Info.Name = StartLocation.Name; } + if (StartLocation != null) + { + outpost.TeamID = StartLocation.Type.OutpostTeam; + outpost.Info.Name = StartLocation.Name; + } } else { EndOutpost = outpost; - if (EndLocation != null) { outpost.Info.Name = EndLocation.Name; } + if (EndLocation != null) + { + outpost.TeamID = EndLocation.Type.OutpostTeam; + outpost.Info.Name = EndLocation.Name; + } } + } } @@ -3176,15 +3582,28 @@ namespace Barotrauma List beaconItems = Item.ItemList.FindAll(it => it.Submarine == BeaconStation); Item reactorItem = beaconItems.Find(it => it.GetComponent() != null); - Reactor reactorComponent = reactorItem.GetComponent(); - ItemContainer reactorContainer = reactorItem.GetComponent(); - + Reactor reactorComponent = null; + ItemContainer reactorContainer = null; + if (reactorItem != null) + { + reactorComponent = reactorItem.GetComponent(); + reactorComponent.FuelConsumptionRate = 0.0f; + reactorContainer = reactorItem.GetComponent(); + Repairable repairable = reactorItem.GetComponent(); + if (repairable != null) + { + if (repairable != null) + { + repairable.DeteriorationSpeed = 0.0f; + } + } + } if (LevelData.IsBeaconActive) { - if (reactorContainer.Inventory.IsEmpty()) + if (reactorContainer != null && reactorContainer.Inventory.IsEmpty()) { ItemPrefab fuelPrefab = ItemPrefab.Prefabs[reactorContainer.ContainableItems[0].Identifiers[0]]; - Entity.Spawner.AddToSpawnQueue( + Spawner.AddToSpawnQueue( fuelPrefab, reactorContainer.Inventory, onSpawned: (it) => reactorComponent.PowerUpImmediately()); } @@ -3198,29 +3617,48 @@ namespace Barotrauma if (!(GameMain.NetworkMember?.IsClient ?? false)) { //empty the reactor - foreach (Item item in reactorContainer.Inventory.AllItems) + if (reactorContainer != null) { - if (item.NonInteractable) { continue; } - Entity.Spawner.AddToRemoveQueue(item); + foreach (Item item in reactorContainer.Inventory.AllItems) + { + if (item.NonInteractable) { continue; } + Spawner.AddToRemoveQueue(item); + } } //remove wires - foreach (Item item in beaconItems.Where(it => it.GetComponent() != null).ToList()) + float removeWireMinDifficulty = 20.0f; + float removeWireProbability = MathUtils.InverseLerp(removeWireMinDifficulty, 100.0f, LevelData.Difficulty) * 0.5f; + if (removeWireProbability > 0.0f) { - if (item.NonInteractable) { continue; } - Wire wire = item.GetComponent(); - if (wire.Locked) { continue; } - if (wire.Connections[0] != null && (wire.Connections[0].Item.NonInteractable || wire.Connections[0].Item.GetComponent().Locked)) + foreach (Item item in beaconItems.Where(it => it.GetComponent() != null).ToList()) { - continue; - } - if (wire.Connections[1] != null && (wire.Connections[1].Item.NonInteractable || wire.Connections[1].Item.GetComponent().Locked)) - { - continue; - } - if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.25f) - { - Entity.Spawner.AddToRemoveQueue(item); + if (item.NonInteractable) { continue; } + Wire wire = item.GetComponent(); + if (wire.Locked) { continue; } + if (wire.Connections[0] != null && (wire.Connections[0].Item.NonInteractable || wire.Connections[0].Item.GetComponent().Locked)) + { + continue; + } + if (wire.Connections[1] != null && (wire.Connections[1].Item.NonInteractable || wire.Connections[1].Item.GetComponent().Locked)) + { + continue; + } + if (Rand.Range(0f, 1.0f, Rand.RandSync.Unsynced) < removeWireProbability) + { + foreach (Connection connection in wire.Connections) + { + if (connection != null) + { + connection.ConnectionPanel.DisconnectedWires.Add(wire); + wire.RemoveConnection(connection.Item); +#if SERVER + connection.ConnectionPanel.Item.CreateServerEvent(connection.ConnectionPanel); + wire.CreateNetworkEvent(); +#endif + } + } + } } } @@ -3230,7 +3668,7 @@ namespace Barotrauma if (item.NonInteractable) { continue; } if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.5f) { - item.Condition *= Rand.Range(0.2f, 0.6f, Rand.RandSync.Unsynced); + item.Condition *= Rand.Range(0.6f, 0.8f, Rand.RandSync.Unsynced); } } @@ -3274,6 +3712,9 @@ namespace Barotrauma pathPoints.Shuffle(Rand.RandSync.Unsynced); var corpsePoints = allSpawnPoints.FindAll(wp => wp.SpawnType == SpawnType.Corpse); corpsePoints.Shuffle(Rand.RandSync.Unsynced); + + if (!corpsePoints.Any() && !pathPoints.Any()) { continue; } + int spawnCounter = 0; for (int j = 0; j < corpseCount; j++) { @@ -3363,7 +3804,15 @@ namespace Barotrauma /// public float GetRealWorldDepth(float worldPositionY) { - return (-(worldPositionY - GenerationParams.Height) + LevelData.InitialDepth) * Physics.DisplayToRealWorldRatio; + if (GameMain.GameSession?.Campaign == null) + { + //ensure the levels aren't too deep to traverse in non-campaign modes where you don't have the option to upgrade/switch the sub + return (-(worldPositionY - GenerationParams.Height) + 80000.0f) * Physics.DisplayToRealWorldRatio; + } + else + { + return (-(worldPositionY - GenerationParams.Height) + LevelData.InitialDepth) * Physics.DisplayToRealWorldRatio; + } } public void DebugSetStartLocation(Location newStartLocation) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index d18b5cb66..2cf68b72c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -29,6 +29,8 @@ namespace Barotrauma public bool HasBeaconStation; public bool IsBeaconActive; + public bool HasHuntingGrounds, OriginallyHadHuntingGrounds; + public OutpostGenerationParams ForceOutpostGenerationParams; public readonly Point Size; @@ -86,6 +88,9 @@ namespace Barotrauma HasBeaconStation = element.GetAttributeBool("hasbeaconstation", false); IsBeaconActive = element.GetAttributeBool("isbeaconactive", false); + HasHuntingGrounds = element.GetAttributeBool("hashuntinggrounds", false); + OriginallyHadHuntingGrounds = element.GetAttributeBool("originallyhadhuntinggrounds", HasHuntingGrounds); + string generationParamsId = element.GetAttributeString("generationparams", ""); GenerationParams = LevelGenerationParams.LevelParams.Find(l => l.Identifier == generationParamsId || l.OldIdentifier == generationParamsId); if (GenerationParams == null) @@ -112,8 +117,7 @@ namespace Barotrauma EventHistory.AddRange(EventSet.PrefabList.Where(p => prefabNames.Any(n => p.Identifier.Equals(n, StringComparison.InvariantCultureIgnoreCase)))); string[] nonRepeatablePrefabNames = element.GetAttributeStringArray("nonrepeatableevents", new string[] { }); - NonRepeatableEvents.AddRange(EventSet.PrefabList.Where(p => prefabNames.Any(n => p.Identifier.Equals(n, StringComparison.InvariantCultureIgnoreCase)))); - + NonRepeatableEvents.AddRange(EventSet.PrefabList.Where(p => nonRepeatablePrefabNames.Any(n => p.Identifier.Equals(n, StringComparison.InvariantCultureIgnoreCase)))); } @@ -140,7 +144,13 @@ namespace Barotrauma var rand = new MTRandom(ToolBox.StringToInt(Seed)); InitialDepth = (int)MathHelper.Lerp(GenerationParams.InitialDepthMin, GenerationParams.InitialDepthMax, (float)rand.NextDouble()); - HasBeaconStation = rand.NextDouble() < locationConnection.Locations.Select(l => l.Type.BeaconStationChance).Max(); + //minimum difficulty of the level before hunting grounds can appear + float huntingGroundsDifficultyThreshold = 25; + //probability of hunting grounds appearing in 100% difficulty levels + float maxHuntingGroundsProbability = 0.3f; + HasHuntingGrounds = OriginallyHadHuntingGrounds = rand.NextDouble() < MathUtils.InverseLerp(huntingGroundsDifficultyThreshold, 100.0f, Difficulty) * maxHuntingGroundsProbability; + + HasBeaconStation = !HasHuntingGrounds && rand.NextDouble() < locationConnection.Locations.Select(l => l.Type.BeaconStationChance).Max(); IsBeaconActive = false; } @@ -163,7 +173,7 @@ namespace Barotrauma (int)MathUtils.Round(GenerationParams.Height, Level.GridCellSize)); } - public static LevelData CreateRandom(string seed = "", float? difficulty = null, LevelGenerationParams generationParams = null) + public static LevelData CreateRandom(string seed = "", float? difficulty = null, LevelGenerationParams generationParams = null, bool requireOutpost = false) { if (string.IsNullOrEmpty(seed)) { @@ -172,7 +182,9 @@ namespace Barotrauma Rand.SetSyncedSeed(ToolBox.StringToInt(seed)); - LevelType type = generationParams == null ? LevelData.LevelType.LocationConnection : generationParams.Type; + LevelType type = generationParams == null ? + (requireOutpost ? LevelType.Outpost : LevelType.LocationConnection) : + generationParams.Type; if (generationParams == null) { generationParams = LevelGenerationParams.GetRandom(seed, type); } var biome = @@ -191,7 +203,13 @@ namespace Barotrauma levelData.HasBeaconStation = beaconRng < 0.5f; levelData.IsBeaconActive = beaconRng > 0.25f; } - GameMain.GameSession?.GameMode?.Mission?.AdjustLevelData(levelData); + if (GameMain.GameSession?.GameMode != null) + { + foreach (Mission mission in GameMain.GameSession.GameMode.Missions) + { + mission.AdjustLevelData(levelData); + } + } return levelData; } @@ -213,6 +231,17 @@ namespace Barotrauma new XAttribute("isbeaconactive", IsBeaconActive.ToString())); } + if (HasHuntingGrounds) + { + newElement.Add( + new XAttribute("hashuntinggrounds", true)); + } + if (HasHuntingGrounds || OriginallyHadHuntingGrounds) + { + newElement.Add( + new XAttribute("originallyhadhuntinggrounds", true)); + } + if (Type == LevelType.Outpost) { if (EventHistory.Any()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs index 0ce80828c..3905348a3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs @@ -181,6 +181,13 @@ namespace Barotrauma set; } + [Serialize(true, true, "Should the generator force a hole to the bottom of the level to ensure there's a way to the abyss."), Editable] + public bool CreateHoleToAbyss + { + get; + set; + } + [Serialize(1000, true, description: "The total number of level objects (vegetation, vents, etc) in the level."), Editable(MinValueInt = 0, MaxValueInt = 100000)] public int LevelObjectAmount { @@ -404,7 +411,35 @@ namespace Barotrauma set; } - [Serialize(300000, true, description: "How far below the level the sea floor is placed."), Editable(MinValueFloat = Level.MaxEntityDepth, MaxValueFloat = 0.0f)] + [Serialize(5, true), Editable(MinValueInt = 0, MaxValueInt = 20)] + public int AbyssIslandCount + { + get; + set; + } + + [Serialize("4000,7000", true), Editable] + public Point AbyssIslandSizeMin + { + get; + set; + } + + [Serialize("8000,10000", true), Editable] + public Point AbyssIslandSizeMax + { + get; + set; + } + + [Serialize(0.5f, true), Editable()] + public float AbyssIslandCaveProbability + { + get; + set; + } + + [Serialize(-300000, true, description: "How far below the level the sea floor is placed."), Editable(MinValueFloat = Level.MaxEntityDepth, MaxValueFloat = 0.0f)] public int SeaFloorDepth { get { return seaFloorBaseDepth; } @@ -554,7 +589,7 @@ namespace Barotrauma var matchingLevelParams = LevelParams.FindAll(lp => lp.Type == type && lp.allowedBiomes.Any()); if (biome == null) { - matchingLevelParams = matchingLevelParams.FindAll(lp => !lp.allowedBiomes.Any(b => b.IsEndBiome)); + matchingLevelParams = matchingLevelParams.FindAll(lp => !lp.allowedBiomes.All(b => b.IsEndBiome)); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs index 52e78853e..c948f79dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs @@ -101,6 +101,7 @@ namespace Barotrauma sl.filePath = filePath; sl.saveElement = doc.Root; sl.saveElement.Name = "LinkedSubmarine"; + sl.saveElement.SetAttributeValue("filepath", filePath); return sl; } @@ -183,10 +184,10 @@ namespace Barotrauma else { string levelSeed = element.GetAttributeString("location", ""); - LevelData levelData = GameMain.GameSession.Campaign?.NextLevel ?? GameMain.GameSession.LevelData; + LevelData levelData = GameMain.GameSession?.Campaign?.NextLevel ?? GameMain.GameSession?.LevelData; linkedSub = new LinkedSubmarine(submarine, idRemap.AssignMaxId()) { - purchasedLostShuttles = GameMain.GameSession.GameMode is CampaignMode campaign && campaign.PurchasedLostShuttles, + purchasedLostShuttles = GameMain.GameSession?.GameMode is CampaignMode campaign && campaign.PurchasedLostShuttles, saveElement = element }; @@ -236,6 +237,11 @@ namespace Barotrauma DebugConsole.ThrowError("Failed to load a linked submarine (empty XML element). The save file may be corrupted."); return; } + if (!info.SubmarineElement.Elements().Any(e => e.Name.ToString().Equals("hull", StringComparison.OrdinalIgnoreCase))) + { + DebugConsole.ThrowError("Failed to load a linked submarine (the submarine contains no hulls)."); + return; + } IdRemap parentRemap = new IdRemap(Submarine.Info.SubmarineElement, Submarine.IdOffset); sub = Submarine.Load(info, false, parentRemap); @@ -297,7 +303,7 @@ namespace Barotrauma { originalMyPortID = myPort.Item.ID; - myPort.Undock(); + myPort.Undock(applyEffects: false); myPort.DockingDir = 0; //something else is already docked to the port this sub should be docked to @@ -321,8 +327,8 @@ namespace Barotrauma sub.SetPosition((linkedPort.Item.WorldPosition - portDiff) - offset); - myPort.Dock(linkedPort); - myPort.Lock(true); + myPort.Dock(linkedPort); + myPort.Lock(isNetworkMessage: true, applyEffects: false); } } @@ -366,6 +372,11 @@ namespace Barotrauma } saveElement.Name = "LinkedSubmarine"; + if (saveElement.Attribute("previewimage") != null) + { + saveElement.Attribute("previewimage").Remove(); + } + if (saveElement.Attribute("pos") != null) { saveElement.Attribute("pos").Remove(); } saveElement.Add(new XAttribute("pos", XMLExtensions.Vector2ToString(Position - Submarine.HiddenSubPosition))); @@ -392,14 +403,14 @@ namespace Barotrauma bool leaveBehind = false; if (!sub.DockedTo.Contains(Submarine.MainSub)) { - System.Diagnostics.Debug.Assert(Submarine.MainSub.AtEndPosition || Submarine.MainSub.AtStartPosition); - if (Submarine.MainSub.AtEndPosition) + System.Diagnostics.Debug.Assert(Submarine.MainSub.AtEndExit || Submarine.MainSub.AtStartExit); + if (Submarine.MainSub.AtEndExit) { - leaveBehind = sub.AtEndPosition != Submarine.MainSub.AtEndPosition; + leaveBehind = sub.AtEndExit != Submarine.MainSub.AtEndExit; } else { - leaveBehind = sub.AtStartPosition != Submarine.MainSub.AtStartPosition; + leaveBehind = sub.AtStartExit != Submarine.MainSub.AtStartExit; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index 0b4c4e441..f5d8712f3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -33,8 +33,9 @@ namespace Barotrauma { OriginalContainerID = item.OriginalContainerID; } + OriginalID = item.ID; - ModuleIndex = (ushort)item.OriginalModuleIndex; + ModuleIndex = (ushort) item.OriginalModuleIndex; Identifier = item.prefab.Identifier; } @@ -42,6 +43,7 @@ namespace Barotrauma { return obj.OriginalID == OriginalID && obj.OriginalContainerID == OriginalContainerID && obj.ModuleIndex == ModuleIndex && obj.Identifier == Identifier; } + public bool Matches(Item item) { if (item.OriginalContainerID != Entity.NullEntityID) @@ -56,15 +58,17 @@ namespace Barotrauma } public readonly List Connections = new List(); - + private string baseName; private int nameFormatIndex; + private LocationType addInitialMissionsForType; + public bool Discovered; - public readonly Dictionary ProximityTimer = new Dictionary(); - - public Pair PendingLocationTypeChange; + public readonly Dictionary ProximityTimer = new Dictionary(); + public (LocationTypeChange typeChange, int delay, MissionPrefab parentMission)? PendingLocationTypeChange; + public int LocationTypeChangeCooldown; public string BaseName { get => baseName; } @@ -76,12 +80,16 @@ namespace Barotrauma public LocationType Type { get; private set; } + public LocationType OriginalType { get; private set; } + public LevelData LevelData { get; set; } public int PortraitId { get; private set; } public Reputation Reputation { get; set; } + public int TurnsInRadiation { get; set; } + #region Store private const float StoreMaxReputationModifier = 0.1f; @@ -168,12 +176,11 @@ namespace Barotrauma { get { - availableMissions.RemoveAll(m => m.Completed || m.Failed); + availableMissions.RemoveAll(m => m.Completed || (m.Failed && !m.Prefab.AllowRetry)); return availableMissions; } } - public Mission SelectedMission { get; @@ -216,6 +223,8 @@ namespace Barotrauma public int TimeSinceLastTypeChange; + public bool IsGateBetweenBiomes; + private struct LoadedMission { public MissionPrefab MissionPrefab { get; } @@ -239,38 +248,64 @@ namespace Barotrauma return $"Location ({Name ?? "null"})"; } - public Location(Vector2 mapPosition, int? zone, Random rand, bool requireOutpost = false, IEnumerable existingLocations = null) + public Location(Vector2 mapPosition, int? zone, Random rand, bool requireOutpost = false, LocationType? forceLocationType = null, IEnumerable existingLocations = null) { - Type = LocationType.Random(rand, zone, requireOutpost); + Type = OriginalType = forceLocationType ?? LocationType.Random(rand, zone, requireOutpost); Name = RandomName(Type, rand, existingLocations); MapPosition = mapPosition; PortraitId = ToolBox.StringToInt(Name); - Connections = new List(); + Connections = new List(); } public Location(XElement element) { string locationType = element.GetAttributeString("type", ""); Type = LocationType.List.Find(lt => lt.Identifier.Equals(locationType, StringComparison.OrdinalIgnoreCase)); + bool typeNotFound = false; + if (Type == null) + { + //turn lairs into abandoned outposts + if (locationType.Equals("lair", StringComparison.OrdinalIgnoreCase)) + { + Type ??= LocationType.List.Find(lt => lt.Identifier.Equals("Abandoned", StringComparison.OrdinalIgnoreCase)); + addInitialMissionsForType = Type; + } + if (Type == null) + { + DebugConsole.AddWarning($"Could not find location type \"{locationType}\". Using location type \"None\" instead."); + Type ??= LocationType.List.Find(lt => lt.Identifier.Equals("None", StringComparison.OrdinalIgnoreCase)); + Type ??= LocationType.List.First(); + } + if (Type != null) + { + element.SetAttributeValue("type", Type.Identifier); + } + typeNotFound = true; + } + + string originalLocationType = element.GetAttributeString("originaltype", locationType); + OriginalType = LocationType.List.Find(lt => lt.Identifier.Equals(locationType, StringComparison.OrdinalIgnoreCase)); + baseName = element.GetAttributeString("basename", ""); Name = element.GetAttributeString("name", ""); MapPosition = element.GetAttributeVector2("position", Vector2.Zero); Discovered = element.GetAttributeBool("discovered", false); PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 1.0f); + IsGateBetweenBiomes = element.GetAttributeBool("isgatebetweenbiomes", false); MechanicalPriceMultiplier = element.GetAttributeFloat("mechanicalpricemultipler", 1.0f); - TimeSinceLastTypeChange = element.GetAttributeInt("timesincelasttypechange", 0); + TurnsInRadiation = element.GetAttributeInt(nameof(TurnsInRadiation).ToLower(), 0); - for (int i = 0; i < Type.CanChangeTo.Count; i++) + if (!typeNotFound) { - ProximityTimer.Add(Type.CanChangeTo[i], element.GetAttributeInt("proximitytimer" + i, 0)); - } + for (int i = 0; i < Type.CanChangeTo.Count; i++) + { + for (int j = 0; j < Type.CanChangeTo[i].Requirements.Count; j++) + { + ProximityTimer.Add(Type.CanChangeTo[i].Requirements[j], element.GetAttributeInt("proximitytimer" + i + "-" + j, 0)); + } + } - int locationTypeChangeIndex = element.GetAttributeInt("pendinglocationtypechange", -1); - if (locationTypeChangeIndex > 0 && locationTypeChangeIndex < Type.CanChangeTo.Count - 1) - { - PendingLocationTypeChange = new Pair( - Type.CanChangeTo[locationTypeChangeIndex], - element.GetAttributeInt("pendinglocationtypechangetimer", 0)); + LoadLocationTypeChange(element); } string[] takenItemStr = element.GetAttributeStringArray("takenitems", new string[0]); @@ -316,6 +351,42 @@ namespace Barotrauma LoadMissions(element); } + public void LoadLocationTypeChange(XElement locationElement) + { + TimeSinceLastTypeChange = locationElement.GetAttributeInt("timesincelasttypechange", 0); + LocationTypeChangeCooldown = locationElement.GetAttributeInt("locationtypechangecooldown", 0); + foreach (XElement subElement in locationElement.Elements()) + { + switch (subElement.Name.ToString()) + { + case "pendinglocationtypechange": + int timer = subElement.GetAttributeInt("timer", 0); + if (subElement.Attribute("index") != null) + { + int locationTypeChangeIndex = subElement.GetAttributeInt("index", 0); + if (locationTypeChangeIndex < 0 || locationTypeChangeIndex >= Type.CanChangeTo.Count) + { + DebugConsole.AddWarning($"Failed to activate a location type change in the location \"{Name}\". Location index out of bounds ({locationTypeChangeIndex})."); + continue; + } + PendingLocationTypeChange = (Type.CanChangeTo[locationTypeChangeIndex], timer, null); + } + else + { + string missionIdentifier = subElement.GetAttributeString("missionidentifier", ""); + var mission = MissionPrefab.List.Find(mp => mp.Identifier.Equals(missionIdentifier, StringComparison.OrdinalIgnoreCase)); + if (mission == null) + { + DebugConsole.AddWarning($"Failed to activate a location type change from the mission \"{missionIdentifier}\" in location \"{Name}\". Matching mission not found."); + continue; + } + PendingLocationTypeChange = (mission.LocationTypeChangeOnCompleted, timer, mission); + } + break; + } + } + } + public void LoadMissions(XElement locationElement) { if (locationElement.GetChildElement("missions") is XElement missionsElement) @@ -335,9 +406,9 @@ namespace Barotrauma } - public static Location CreateRandom(Vector2 position, int? zone, Random rand, bool requireOutpost, IEnumerable existingLocations = null) + public static Location CreateRandom(Vector2 position, int? zone, Random rand, bool requireOutpost, LocationType? forceLocationType = null, IEnumerable existingLocations = null) { - return new Location(position, zone, rand, requireOutpost, existingLocations); + return new Location(position, zone, rand, requireOutpost, forceLocationType, existingLocations); } public void ChangeType(LocationType newType) @@ -348,6 +419,16 @@ namespace Barotrauma Type = newType; Name = Type.NameFormats == null ? baseName : Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", baseName); + + if (Type.MissionIdentifiers.Any()) + { + UnlockMissionByIdentifier(Type.MissionIdentifiers.GetRandom()); + } + if (Type.MissionTags.Any()) + { + UnlockMissionByTag(Type.MissionTags.GetRandom()); + } + CreateStore(force: true); } @@ -361,6 +442,16 @@ namespace Barotrauma #endif } + public void UnlockMission(MissionPrefab missionPrefab) + { + if (AvailableMissions.Any(m => m.Prefab == missionPrefab)) { return; } + var mission = InstantiateMission(missionPrefab); + availableMissions.Add(mission); +#if CLIENT + GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo(); +#endif + } + public MissionPrefab UnlockMissionByIdentifier(string identifier) { if (AvailableMissions.Any(m => m.Prefab.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase))) { return null; } @@ -399,12 +490,12 @@ namespace Barotrauma var unusedMissions = matchingMissions.Where(m => !availableMissions.Any(mission => mission.Prefab == m)); if (unusedMissions.Any()) { - var suitableMissions = unusedMissions.Where(m => Connections.Any(c => m.IsAllowed(this, c.OtherLocation(this)))); + var suitableMissions = unusedMissions.Where(m => Connections.Any(c => m.IsAllowed(this, c.OtherLocation(this)) || m.IsAllowed(this, this))); if (!suitableMissions.Any()) { suitableMissions = unusedMissions; } - MissionPrefab missionPrefab = suitableMissions.GetRandom(); + MissionPrefab missionPrefab = ToolBox.SelectWeightedRandom(suitableMissions.ToList(), suitableMissions.Select(m => (float)m.Commonness).ToList(), Rand.RandSync.Unsynced); var mission = InstantiateMission(missionPrefab, out LocationConnection connection); //don't allow duplicate missions in the same connection if (AvailableMissions.Any(m => m.Prefab == missionPrefab && m.Locations.Contains(mission.Locations[0]) && m.Locations.Contains(mission.Locations[1]))) @@ -428,6 +519,12 @@ namespace Barotrauma private Mission InstantiateMission(MissionPrefab prefab, out LocationConnection connection) { + if (prefab.IsAllowed(this, this)) + { + connection = null; + return InstantiateMission(prefab); + } + var suitableConnections = Connections.Where(c => prefab.IsAllowed(this, c.OtherLocation(this))); if (!suitableConnections.Any()) { @@ -439,10 +536,7 @@ namespace Barotrauma suitableConnections.Select(c => (c.Passed ? 1.0f : 5.0f) / Math.Max(availableMissions.Count(m => m.Locations.Contains(c.OtherLocation(this))), 1.0f)).ToList(), Rand.RandSync.Unsynced); - Location destination = connection.OtherLocation(this); - var mission = prefab.Instantiate(new Location[] { this, destination }); - mission.AdjustLevelData(connection.LevelData); - return mission; + return InstantiateMission(prefab, connection); } private Mission InstantiateMission(MissionPrefab prefab, LocationConnection connection) @@ -453,26 +547,47 @@ namespace Barotrauma return mission; } + private Mission InstantiateMission(MissionPrefab prefab) + { + var mission = prefab.Instantiate(new Location[] { this, this }); + mission.AdjustLevelData(LevelData); + return mission; + } + public void InstantiateLoadedMissions(Map map) { availableMissions.Clear(); - if (loadedMissions == null || loadedMissions.None()) { return; } - foreach (LoadedMission loadedMission in loadedMissions) - { - Location destination = null; - if (loadedMission.DestinationIndex >= 0 && loadedMission.DestinationIndex < map.Locations.Count) + if (loadedMissions != null && loadedMissions.Any()) + { + foreach (LoadedMission loadedMission in loadedMissions) { - destination = map.Locations[loadedMission.DestinationIndex]; + Location destination; + if (loadedMission.DestinationIndex >= 0 && loadedMission.DestinationIndex < map.Locations.Count) + { + destination = map.Locations[loadedMission.DestinationIndex]; + } + else + { + destination = Connections.First().OtherLocation(this); + } + var mission = loadedMission.MissionPrefab.Instantiate(new Location[] { this, destination }); + availableMissions.Add(mission); + if (loadedMission.SelectedMission) { SelectedMission = mission; } } - else - { - destination = Connections.First().OtherLocation(this); - } - var mission = loadedMission.MissionPrefab.Instantiate(new Location[] { this, destination }); - availableMissions.Add(mission); - if (loadedMission.SelectedMission) { SelectedMission = mission; } + loadedMissions = null; + } + if (addInitialMissionsForType != null) + { + if (addInitialMissionsForType.MissionIdentifiers.Any()) + { + UnlockMissionByIdentifier(addInitialMissionsForType.MissionIdentifiers.GetRandom()); + } + if (addInitialMissionsForType.MissionTags.Any()) + { + UnlockMissionByTag(addInitialMissionsForType.MissionTags.GetRandom()); + } + addInitialMissionsForType = null; } - loadedMissions = null; } /// @@ -484,6 +599,33 @@ namespace Barotrauma SelectedMissionIndex = -1; } + public bool HasOutpost() + { + if (!Type.HasOutpost) { return false; } + + return !IsCriticallyRadiated(); + } + + public bool IsCriticallyRadiated() + { + if (GameMain.GameSession?.Map?.Radiation != null) + { + return TurnsInRadiation > GameMain.GameSession.Map.Radiation.Params.CriticalRadiationThreshold; + } + + return false; + } + + public LocationType GetLocationType() + { + if (IsCriticallyRadiated() && LocationType.List.FirstOrDefault(lt => lt.Identifier.Equals(Type.ReplaceInRadiation, StringComparison.OrdinalIgnoreCase)) is { } newLocationType) + { + return newLocationType; + } + + return Type; + } + public IEnumerable GetMissionsInConnection(LocationConnection connection) { System.Diagnostics.Debug.Assert(Connections.Contains(connection)); @@ -583,6 +725,8 @@ namespace Barotrauma } } + public bool IsRadiated() => GameMain.GameSession?.Map?.Radiation != null && GameMain.GameSession.Map.Radiation.Enabled && GameMain.GameSession.Map.Radiation.Contains(this); + private List CreateStoreStock() { var stock = new List(); @@ -908,27 +1052,55 @@ namespace Barotrauma { var locationElement = new XElement("location", new XAttribute("type", Type.Identifier), + new XAttribute("originaltype", (Type ?? OriginalType).Identifier), new XAttribute("basename", BaseName), new XAttribute("name", Name), new XAttribute("discovered", Discovered), new XAttribute("position", XMLExtensions.Vector2ToString(MapPosition)), new XAttribute("pricemultiplier", PriceMultiplier), + new XAttribute("isgatebetweenbiomes", IsGateBetweenBiomes), new XAttribute("mechanicalpricemultipler", MechanicalPriceMultiplier), - new XAttribute("timesincelasttypechange", TimeSinceLastTypeChange)); + new XAttribute("timesincelasttypechange", TimeSinceLastTypeChange), + new XAttribute(nameof(TurnsInRadiation).ToLower(), TurnsInRadiation)); LevelData.Save(locationElement); for (int i = 0; i < Type.CanChangeTo.Count; i++) { - if (ProximityTimer.ContainsKey(Type.CanChangeTo[i])) + for (int j = 0; j < Type.CanChangeTo[i].Requirements.Count; j++) { - locationElement.Add(new XAttribute("proximitytimer" + i, ProximityTimer[Type.CanChangeTo[i]])); + if (ProximityTimer.ContainsKey(Type.CanChangeTo[i].Requirements[j])) + { + locationElement.Add(new XAttribute("proximitytimer" + i + "-" + j, ProximityTimer[Type.CanChangeTo[i].Requirements[j]])); + } } } - if (PendingLocationTypeChange != null) + if (PendingLocationTypeChange.HasValue) { - locationElement.Add(new XAttribute("pendinglocationtypechange", Type.CanChangeTo.IndexOf(PendingLocationTypeChange.First))); - locationElement.Add(new XAttribute("pendinglocationtypechangetimer", PendingLocationTypeChange.Second)); + var changeElement = new XElement("pendinglocationtypechange", new XAttribute("timer", PendingLocationTypeChange.Value.delay)); + if (PendingLocationTypeChange.Value.parentMission != null) + { + changeElement.Add(new XAttribute("missionidentifier", PendingLocationTypeChange.Value.parentMission.Identifier)); + locationElement.Add(changeElement); + } + else + { + int index = Type.CanChangeTo.IndexOf(PendingLocationTypeChange.Value.typeChange); + changeElement.Add(new XAttribute("index", index)); + if (index == -1) + { + DebugConsole.AddWarning($"Invalid location type change in the location \"{Name}\". Unknown type change ({PendingLocationTypeChange.Value.typeChange.ChangeToType})."); + } + else + { + locationElement.Add(changeElement); + } + } + } + + if (LocationTypeChangeCooldown > 0) + { + locationElement.Add(new XAttribute("locationtypechangecooldown", LocationTypeChangeCooldown)); } if (takenItems.Any()) @@ -987,7 +1159,7 @@ namespace Barotrauma var missionsElement = new XElement("missions"); foreach (Mission mission in missions) { - var location = mission.Locations.FirstOrDefault(l => l != this); + var location = mission.Locations.All(l => l == this) ? this : mission.Locations.FirstOrDefault(l => l != this); var i = map.Locations.IndexOf(location); missionsElement.Add(new XElement("mission", new XAttribute("prefabid", mission.Prefab.Identifier), diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationConnection.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationConnection.cs index 71234355e..964aaceb3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationConnection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationConnection.cs @@ -14,6 +14,8 @@ namespace Barotrauma public bool Passed; + public bool Locked; + public LevelData LevelData { get; set; } public Vector2 CenterPos @@ -32,6 +34,16 @@ namespace Barotrauma private set; } + private readonly List availableMissions = new List(); + public IEnumerable AvailableMissions + { + get + { + availableMissions.RemoveAll(m => m.Completed || (m.Failed && !m.Prefab.AllowRetry)); + return availableMissions; + } + } + public LocationConnection(Location location1, Location location2) { if (location1 == null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs index cb91b64a3..88fe4d6f4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs @@ -13,17 +13,12 @@ namespace Barotrauma class LocationType { public static readonly List List = new List(); - - private readonly List nameFormats; private readonly List names; - - private readonly Sprite symbolSprite; - private readonly List portraits = new List(); // - private List> hireableJobs; - private float totalHireableWeight; + private readonly List> hireableJobs; + private readonly float totalHireableWeight; public Dictionary CommonnessPerZone = new Dictionary(); @@ -32,18 +27,24 @@ namespace Barotrauma public readonly float BeaconStationChance; + public readonly CharacterTeamType OutpostTeam; + public readonly List CanChangeTo = new List(); + public readonly List MissionIdentifiers = new List(); + public readonly List MissionTags = new List(); + + public readonly List HideEntitySubcategories = new List(); + + public bool IsEnterable { get; private set; } + public bool UseInMainMenu { get; private set; } - - public List NameFormats - { - get { return nameFormats; } - } + + public List NameFormats { get; private set; } public bool HasHireableCharacters { @@ -55,11 +56,11 @@ namespace Barotrauma get; private set; } + + public string ReplaceInRadiation { get; } - public Sprite Sprite - { - get { return symbolSprite; } - } + public Sprite Sprite { get; private set; } + public Sprite RadiationSprite { get; } public Color SpriteColor { @@ -79,9 +80,20 @@ namespace Barotrauma BeaconStationChance = element.GetAttributeFloat("beaconstationchance", 0.0f); - nameFormats = TextManager.GetAll("LocationNameFormat." + Identifier); + NameFormats = TextManager.GetAll("LocationNameFormat." + Identifier); UseInMainMenu = element.GetAttributeBool("useinmainmenu", false); HasOutpost = element.GetAttributeBool("hasoutpost", true); + IsEnterable = element.GetAttributeBool("isenterable", HasOutpost); + + MissionIdentifiers = element.GetAttributeStringArray("missionidentifiers", new string[0]).ToList(); + MissionTags = element.GetAttributeStringArray("missiontags", new string[0]).ToList(); + + HideEntitySubcategories = element.GetAttributeStringArray("hideentitysubcategories", new string[0]).ToList(); + + ReplaceInRadiation = element.GetAttributeString(nameof(ReplaceInRadiation).ToLower(), ""); + + string teamStr = element.GetAttributeString("outpostteam", "FriendlyNPC"); + Enum.TryParse(teamStr, out OutpostTeam); string nameFile = element.GetAttributeString("namefile", "Content/Map/locationNames.txt"); try @@ -135,11 +147,14 @@ namespace Barotrauma hireableJobs.Add(hireableJob); break; case "symbol": - symbolSprite = new Sprite(subElement, lazyLoad: true); + Sprite = new Sprite(subElement, lazyLoad: true); SpriteColor = subElement.GetAttributeColor("color", Color.White); break; + case "radiationsymbol": + RadiationSprite = new Sprite(subElement, lazyLoad: true); + break; case "changeto": - CanChangeTo.Add(new LocationTypeChange(Identifier, subElement)); + CanChangeTo.Add(new LocationTypeChange(Identifier, subElement, requireChangeMessages: true)); break; case "portrait": var portrait = new Sprite(subElement, lazyLoad: true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs index 2353a7b8d..4f7c3424c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs @@ -8,36 +8,136 @@ namespace Barotrauma { class LocationTypeChange { + public class Requirement + { + public enum FunctionType + { + Add, + Multiply + } + + public readonly FunctionType Function; + + /// + /// The change can only happen if there's at least one of the given types of locations near this one + /// + public readonly List RequiredLocations; + + /// + /// How close the location needs to be to one of the RequiredLocations for the change to occur + /// + public readonly int RequiredProximity; + + /// + /// Base probability per turn for the location to change if near one of the RequiredLocations + /// + public readonly float Probability; + + /// + /// How close the location needs to be to one of the RequiredLocations for the probability to increase + /// + public readonly int RequiredProximityForProbabilityIncrease; + + /// + /// How much the probability increases per turn if within RequiredProximityForProbabilityIncrease steps of RequiredLocations + /// + public readonly float ProximityProbabilityIncrease; + + /// + /// Does there need to be a beacon station within RequiredProximity + /// + public readonly bool RequireBeaconStation; + + /// + /// Does there need to be hunting grounds within RequiredProximity + /// + public readonly bool RequireHuntingGrounds; + + public Requirement(XElement element, LocationTypeChange change) + { + RequiredLocations = element.GetAttributeStringArray("requiredlocations", element.GetAttributeStringArray("requiredadjacentlocations", new string[0])).ToList(); + RequiredProximity = Math.Max(element.GetAttributeInt("requiredproximity", 1), 1); + ProximityProbabilityIncrease = element.GetAttributeFloat("proximityprobabilityincrease", 0.0f); + RequiredProximityForProbabilityIncrease = element.GetAttributeInt("requiredproximityforprobabilityincrease", -1); + RequireBeaconStation = element.GetAttributeBool("requirebeaconstation", false); + RequireHuntingGrounds = element.GetAttributeBool("requirehuntinggrounds", false); + + string functionStr = element.GetAttributeString("function", "Add"); + if (!Enum.TryParse(functionStr, ignoreCase: true, out Function)) + { + DebugConsole.ThrowError( + $"Invalid location type change in location type \"{change.CurrentType}\". " + + $"\"{functionStr}\" is not a valid function."); + } + + Probability = element.GetAttributeFloat("probability", 1.0f); + + if (RequiredProximityForProbabilityIncrease > 0 || ProximityProbabilityIncrease > 0.0f) + { + if (!RequiredLocations.Any() && !RequireBeaconStation && !RequireHuntingGrounds) + { + DebugConsole.AddWarning( + $"Invalid location type change in location type \"{change.CurrentType}\". " + + "Probability is configured to increase when near some other type of location, but the RequiredLocations attribute is not set."); + } + if (Probability >= 1.0f) + { + DebugConsole.AddWarning( + $"Invalid location type change in location type \"{change.CurrentType}\". " + + "Probability is configured to increase when near some other type of location, but the base probability is already 100%"); + } + } + } + + public bool MatchesLocation(Location location) + { + return RequiredLocations.Contains(location.Type.Identifier) && !location.IsCriticallyRadiated(); + } + + public bool AnyWithinDistance(Location location, int maxDistance, int currentDistance = 0, HashSet checkedLocations = null) + { + if (currentDistance > maxDistance) { return false; } + if (currentDistance > 0 && MatchesLocation(location)) { return true; } + + checkedLocations ??= new HashSet(); + checkedLocations.Add(location); + + foreach (var connection in location.Connections) + { + if (RequireBeaconStation && connection.LevelData.HasBeaconStation && connection.LevelData.IsBeaconActive) + { + return true; + } + if (RequireHuntingGrounds && connection.LevelData.HasHuntingGrounds) + { + return true; + } + + var otherLocation = connection.OtherLocation(location); + if (!checkedLocations.Contains(otherLocation)) + { + if (AnyWithinDistance(otherLocation, maxDistance, currentDistance + 1, checkedLocations)) { return true; } + } + } + + return false; + } + } + + public readonly string CurrentType; + public readonly string ChangeToType; - public readonly bool RequireDiscovered; - - public List Messages = new List(); - - /// - /// The change can only happen if there's at least one of the given types of locations near this one - /// - public readonly List RequiredLocations; - - /// - /// How close the location needs to be to one of the RequiredLocations for the change to occur - /// - public readonly int RequiredProximity; - /// /// Base probability per turn for the location to change if near one of the RequiredLocations /// public readonly float Probability; - /// - /// How close the location needs to be to one of the RequiredLocations for the probability to increase - /// - public readonly int RequiredProximityForProbabilityIncrease; + public readonly bool RequireDiscovered; - /// - /// How much the probability increases per turn if within RequiredProximityForProbabilityIncrease steps of RequiredLocations - /// - public readonly float ProximityProbabilityIncrease; + public List Requirements = new List(); + + public List Messages = new List(); /// /// The change can't happen if there's one or more of the given types of locations near this one @@ -49,41 +149,35 @@ namespace Barotrauma /// public readonly int DisallowedProximity; + /// + /// The location can't change it's type for this many turns after this location type changes occurs + /// + public readonly int CooldownAfterChange; + public readonly Point RequiredDurationRange; - public LocationTypeChange(string currentType, XElement element) + public LocationTypeChange(string currentType, XElement element, bool requireChangeMessages, float defaultProbability = 0.0f) { - ChangeToType = element.GetAttributeString("type", ""); - Probability = element.GetAttributeFloat("probability", 1.0f); + CurrentType = currentType; + ChangeToType = element.GetAttributeString("type", element.GetAttributeString("to", "")); RequireDiscovered = element.GetAttributeBool("requirediscovered", false); - RequiredLocations = element.GetAttributeStringArray("requiredlocations", element.GetAttributeStringArray("requiredadjacentlocations", new string[0])).ToList(); - RequiredProximity = Math.Max(element.GetAttributeInt("requiredproximity", 1), 1); - ProximityProbabilityIncrease = element.GetAttributeFloat("proximityprobabilityincrease", 0.0f); - RequiredProximityForProbabilityIncrease = element.GetAttributeInt("requiredproximityforprobabilityincrease", -1); - - - if (RequiredProximityForProbabilityIncrease > 0 || ProximityProbabilityIncrease > 0.0f) - { - if (!RequiredLocations.Any()) - { - DebugConsole.AddWarning( - $"Invalid location type change in location type \"{currentType}\". "+ - "Probability is configured to increase when near some other type of location, but the RequiredLocations attribute is not set."); - } - if (Probability >= 1.0f) - { - DebugConsole.AddWarning( - $"Invalid location type change in location type \"{currentType}\". " + - "Probability is configured to increase when near some other type of location, but the base probability is already 100%"); - } - } - DisallowedAdjacentLocations = element.GetAttributeStringArray("disallowedadjacentlocations", new string[0]).ToList(); DisallowedProximity = Math.Max(element.GetAttributeInt("disallowedproximity", 1), 1); RequiredDurationRange = element.GetAttributePoint("requireddurationrange", Point.Zero); + + Probability = element.GetAttributeFloat("probability", defaultProbability); + + CooldownAfterChange = Math.Max(element.GetAttributeInt("cooldownafterchange", 0), 0); + + //backwards compatibility + if (element.Attribute("requiredlocations") != null) + { + Requirements.Add(new Requirement(element, this)); + } + //backwards compatibility if (element.Attribute("requiredduration") != null) { @@ -95,34 +189,70 @@ namespace Barotrauma Messages = TextManager.GetAll(messageTag); if (Messages == null) { - DebugConsole.ThrowError("No messages defined for the location type change " + currentType + " -> " + ChangeToType); + if (requireChangeMessages) + { + DebugConsole.ThrowError("No messages defined for the location type change " + currentType + " -> " + ChangeToType); + } + Messages = new List(); + } + + foreach (XElement subElement in element.Elements()) + { + if (subElement.Name.ToString().Equals("requirement", StringComparison.OrdinalIgnoreCase)) + { + Requirements.Add(new Requirement(subElement, this)); + } } } public float DetermineProbability(Location location) { if (RequireDiscovered && !location.Discovered) { return 0.0f; } + if (location.IsCriticallyRadiated()) { return 0.0f; } + if (location.LocationTypeChangeCooldown > 0) { return 0.0f; } + if (location.IsGateBetweenBiomes) { return 0.0f; } - if (RequiredLocations.Any() && !AnyWithinDistance(location, RequiredProximity, (otherLocation) => { return RequiredLocations.Contains(otherLocation.Type.Identifier); })) - { - return 0.0f; - } - if (DisallowedAdjacentLocations.Any() && AnyWithinDistance(location, DisallowedProximity, (otherLocation) => { return DisallowedAdjacentLocations.Contains(otherLocation.Type.Identifier); })) + if (DisallowedAdjacentLocations.Any() && + AnyWithinDistance(location, DisallowedProximity, (otherLocation) => { return DisallowedAdjacentLocations.Contains(otherLocation.Type.Identifier); })) { return 0.0f; } + float probability = Probability; - if (location.ProximityTimer.ContainsKey(this)) + foreach (Requirement requirement in Requirements) { - if (AnyWithinDistance(location, RequiredProximityForProbabilityIncrease, (otherLocation) => { return RequiredLocations.Contains(otherLocation.Type.Identifier); })) + if (requirement.AnyWithinDistance(location, requirement.RequiredProximity)) { - return probability += ProximityProbabilityIncrease * location.ProximityTimer[this]; + if (requirement.Function == Requirement.FunctionType.Add) + { + probability += requirement.Probability; + } + else + { + probability *= requirement.Probability; + } + } + + if (location.ProximityTimer.ContainsKey(requirement)) + { + if (requirement.AnyWithinDistance(location, requirement.RequiredProximityForProbabilityIncrease)) + { + if (requirement.Function == Requirement.FunctionType.Add) + { + probability += requirement.ProximityProbabilityIncrease * location.ProximityTimer[requirement]; + } + else + { + probability *= requirement.ProximityProbabilityIncrease * location.ProximityTimer[requirement]; + } + } } } + return probability; } - public bool AnyWithinDistance(Location location, int maxDistance, Func predicate, int currentDistance = 0, HashSet checkedLocations = null) + private bool AnyWithinDistance(Location location, int maxDistance, Func predicate, int currentDistance = 0, HashSet checkedLocations = null) { if (currentDistance > maxDistance) { return false; } if (currentDistance > 0 && predicate(location)) { return true; } @@ -141,22 +271,5 @@ namespace Barotrauma return false; } - - private int CountWithinRequiredProximity(Location location, int currentDistance = 0, HashSet checkedLocations = null) - { - if (currentDistance > RequiredProximityForProbabilityIncrease) { return 0; } - int count = currentDistance > 0 && RequiredLocations.Contains(location.Type.Identifier) ? 1 : 0; - - checkedLocations ??= new HashSet(); - checkedLocations.Add(location); - - foreach (var connection in location.Connections) - { - var otherLocation = connection.OtherLocation(location); - if (!checkedLocations.Contains(otherLocation)) { count += CountWithinRequiredProximity(otherLocation, currentDistance+1, checkedLocations); } - } - - return count; - } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index 1fd5e93de..f82d99b94 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; @@ -15,8 +16,8 @@ namespace Barotrauma private Location furthestDiscoveredLocation; - private int Width => generationParams.Width; - private int Height => generationParams.Height; + public int Width { get; private set; } + public int Height { get; private set; } public Action OnLocationSelected; /// @@ -56,20 +57,37 @@ namespace Barotrauma public List Connections { get; private set; } - public Map() + public Radiation Radiation; + + public Map(CampaignSettings settings) { generationParams = MapGenerationParams.Instance; + Width = generationParams.Width; + Height = generationParams.Height; Locations = new List(); Connections = new List(); + if (generationParams.RadiationParams != null) + { + Radiation = new Radiation(this, generationParams.RadiationParams) + { + Enabled = settings.RadiationEnabled + }; + } } /// /// Load a previously saved campaign map from XML /// - private Map(CampaignMode campaign, XElement element) : this() + private Map(CampaignMode campaign, XElement element, CampaignSettings settings) : this(settings) { Seed = element.GetAttributeString("seed", "a"); Rand.SetSyncedSeed(ToolBox.StringToInt(Seed)); + + Width = element.GetAttributeInt("width", Width); + Height = element.GetAttributeInt("height", Height); + + bool lairsFound = false; + foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -80,8 +98,15 @@ namespace Barotrauma { Locations.Add(null); } + lairsFound |= subElement.GetAttributeString("type", "").Equals("lair", StringComparison.OrdinalIgnoreCase); Locations[i] = new Location(subElement); break; + case "radiation": + Radiation = new Radiation(this, generationParams.RadiationParams, subElement) + { + Enabled = settings.RadiationEnabled + }; + break; } } System.Diagnostics.Debug.Assert(!Locations.Contains(null)); @@ -90,6 +115,7 @@ namespace Barotrauma Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, $"location.{i}", -100, 100, Rand.Range(-10, 10, Rand.RandSync.Server)); } + List connectionElements = new List(); foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -100,6 +126,7 @@ namespace Barotrauma var connection = new LocationConnection(Locations[locationIndices.X], Locations[locationIndices.Y]) { Passed = subElement.GetAttributeBool("passed", false), + Locked = subElement.GetAttributeBool("locked", false), Difficulty = subElement.GetAttributeFloat("difficulty", 0.0f) }; Locations[locationIndices.X].Connections.Add(connection); @@ -111,6 +138,7 @@ namespace Barotrauma LevelGenerationParams.GetBiomes().FirstOrDefault(b => b.OldIdentifier == biomeId) ?? LevelGenerationParams.GetBiomes().First(); Connections.Add(connection); + connectionElements.Add(subElement); break; } } @@ -149,13 +177,30 @@ namespace Barotrauma } } + //backwards compatibility: if the map contained the now-removed lairs and has no hunting grounds, create some hunting grounds + if (lairsFound && !Connections.Any(c => c.LevelData.HasHuntingGrounds)) + { + for (int i = 0; i < Connections.Count; i++) + { + float maxHuntingGroundsProbability = 0.3f; + Connections[i].LevelData.HasHuntingGrounds = Rand.Range(0.0f, 1.0f) < Connections[i].Difficulty / 100.0f * maxHuntingGroundsProbability; + connectionElements[i].SetAttributeValue("hashuntinggrounds", true); + } + } + + //backwards compatibility: if locations go out of bounds (map saved with different generation parameters before width/height were included in the xml) + float maxX = Locations.Select(l => l.MapPosition.X).Max(); + if (maxX > Width) { Width = (int)(maxX + 10); } + float maxY = Locations.Select(l => l.MapPosition.Y).Max(); + if (maxY > Height) { Height = (int)(maxY + 10); } + InitProjectSpecific(); } /// /// Generate a new campaign map from the seed /// - public Map(CampaignMode campaign, string seed) : this() + public Map(CampaignMode campaign, string seed, CampaignSettings settings) : this(settings) { Seed = seed; Rand.SetSyncedSeed(ToolBox.StringToInt(Seed)); @@ -255,14 +300,14 @@ namespace Barotrauma int positionIndex = Rand.Int(1, Rand.RandSync.Server); Vector2 position = points[positionIndex]; - if (newLocations[1 - i] != null && newLocations[1 - i].MapPosition == position) position = points[1 - positionIndex]; - int zone = MathHelper.Clamp((int)Math.Floor(position.X / zoneWidth) + 1, 1, generationParams.DifficultyZones); - newLocations[i] = Location.CreateRandom(position, zone, Rand.GetRNG(Rand.RandSync.Server), requireOutpost: false, Locations); + if (newLocations[1 - i] != null && newLocations[1 - i].MapPosition == position) { position = points[1 - positionIndex]; } + int zone = GetZoneIndex(position.X); + newLocations[i] = Location.CreateRandom(position, zone, Rand.GetRNG(Rand.RandSync.Server), requireOutpost: false, existingLocations: Locations); Locations.Add(newLocations[i]); } var newConnection = new LocationConnection(newLocations[0], newLocations[1]); - Connections.Add(newConnection); + Connections.Add(newConnection); } //remove connections that are too short @@ -285,20 +330,13 @@ namespace Barotrauma if (connection2.Locations[1] == connection.Locations[0]) { connection2.Locations[1] = connection.Locations[1]; } } } - - HashSet connectedLocations = new HashSet(); + foreach (LocationConnection connection in Connections) { connection.Locations[0].Connections.Add(connection); connection.Locations[1].Connections.Add(connection); - - connectedLocations.Add(connection.Locations[0]); - connectedLocations.Add(connection.Locations[1]); } - //remove orphans - Locations.RemoveAll(c => !connectedLocations.Contains(c)); - //remove locations that are too close to each other float minLocationDistanceSqr = generationParams.MinLocationDistance * generationParams.MinLocationDistance; for (int i = Locations.Count - 1; i >= 0; i--) @@ -350,6 +388,58 @@ namespace Barotrauma } } + LocationConnection[] connectionsBetweenZones = new LocationConnection[generationParams.DifficultyZones]; + foreach (var connection in Connections) + { + int zone1 = GetZoneIndex(connection.Locations[0].MapPosition.X); + int zone2 = GetZoneIndex(connection.Locations[1].MapPosition.X); + if (zone1 == zone2) { continue; } + if (zone1 > zone2) + { + int temp = zone2; + zone2 = zone1; + zone1 = temp; + } + + if (connectionsBetweenZones[zone1] == null) + { + connectionsBetweenZones[zone1] = connection; + } + else + { + if (Math.Abs(connection.CenterPos.Y - Height / 2) < Math.Abs(connectionsBetweenZones[zone1].CenterPos.Y - Height / 2)) + { + connectionsBetweenZones[zone1] = connection; + } + } + } + + for (int i = Connections.Count - 1; i >= 0; i--) + { + int zone1 = GetZoneIndex(Connections[i].Locations[0].MapPosition.X); + int zone2 = GetZoneIndex(Connections[i].Locations[1].MapPosition.X); + if (zone1 == zone2) { continue; } + if (zone1 == generationParams.DifficultyZones || zone2 == generationParams.DifficultyZones) { continue; } + + if (!connectionsBetweenZones.Contains(Connections[i])) + { + Connections.RemoveAt(i); + } + else + { + var leftMostLocation = + Connections[i].Locations[0].MapPosition.X < Connections[i].Locations[1].MapPosition.X ? + Connections[i].Locations[0] : + Connections[i].Locations[1]; + if (!leftMostLocation.Type.HasOutpost || leftMostLocation.Type.Identifier.Equals("abandoned", StringComparison.OrdinalIgnoreCase)) + { + leftMostLocation.ChangeType(LocationType.List.First(lt => lt.HasOutpost && !lt.Identifier.Equals("abandoned", StringComparison.OrdinalIgnoreCase))); + } + leftMostLocation.IsGateBetweenBiomes = true; + Connections[i].Locked = true; + } + } + foreach (Location location in Locations) { for (int i = location.Connections.Count - 1; i >= 0; i--) @@ -361,6 +451,9 @@ namespace Barotrauma } } + //remove orphans + Locations.RemoveAll(l => !Connections.Any(c => c.Locations.Contains(l))); + foreach (LocationConnection connection in Connections) { connection.Difficulty = MathHelper.Clamp((connection.CenterPos.X / Width * 100) + Rand.Range(-10.0f, 0.0f, Rand.RandSync.Server), 1.2f, 100.0f); @@ -371,7 +464,18 @@ namespace Barotrauma foreach (Location location in Locations) { - location.LevelData = new LevelData(location); + location.LevelData = new LevelData(location) + { + Difficulty = MathHelper.Clamp(location.MapPosition.X / Width * 100, 0.0f, 100.0f) + }; + if (location.Type.MissionIdentifiers.Any()) + { + location.UnlockMissionByIdentifier(location.Type.MissionIdentifiers.GetRandom()); + } + if (location.Type.MissionTags.Any()) + { + location.UnlockMissionByTag(location.Type.MissionTags.GetRandom()); + } } foreach (LocationConnection connection in Connections) { @@ -381,6 +485,12 @@ namespace Barotrauma partial void GenerateLocationConnectionVisuals(); + private int GetZoneIndex(float xPos) + { + float zoneWidth = Width / generationParams.DifficultyZones; + return MathHelper.Clamp((int)Math.Floor(xPos / zoneWidth) + 1, 1, generationParams.DifficultyZones); + } + public Biome GetBiome(Vector2 mapPos) { return GetBiome(mapPos.X); @@ -412,7 +522,7 @@ namespace Barotrauma { allowedBiomes.Clear(); allowedBiomes.AddRange(biomes.Where(b => b.AllowedZones.Contains(generationParams.DifficultyZones - i))); - float zoneX = Width - zoneWidth * i; + float zoneX = zoneWidth * (generationParams.DifficultyZones - i); foreach (Location location in Locations) { @@ -425,7 +535,7 @@ namespace Barotrauma foreach (LocationConnection connection in Connections) { if (connection.Biome != null) { continue; } - connection.Biome = connection.Locations[0].Biome; + connection.Biome = connection.Locations[0].MapPosition.X > connection.Locations[1].MapPosition.X ? connection.Locations[0].Biome : connection.Locations[1].Biome; } System.Diagnostics.Debug.Assert(Locations.All(l => l.Biome != null)); @@ -558,10 +668,12 @@ namespace Barotrauma CurrentLocation.CreateStore(); OnLocationChanged?.Invoke(prevLocation, CurrentLocation); - if (GameMain.GameSession?.GameMode is CampaignMode campaign && campaign.CampaignMetadata is { } metadata) + if (GameMain.GameSession is { Campaign: { CampaignMetadata: { } metadata } }) { metadata.SetValue("campaign.location.id", CurrentLocationIndex); metadata.SetValue("campaign.location.name", CurrentLocation.Name); + metadata.SetValue("campaign.location.biome", CurrentLocation.Biome?.Identifier ?? "null"); + metadata.SetValue("campaign.location.type", CurrentLocation.Type?.Identifier ?? "null"); } } @@ -614,9 +726,14 @@ namespace Barotrauma } SelectedLocation = Locations[index]; + var currentDisplayLocation = GameMain.GameSession?.Campaign?.GetCurrentDisplayLocation(); SelectedConnection = - Connections.Find(c => c.Locations.Contains(GameMain.GameSession?.Campaign?.CurrentDisplayLocation) && c.Locations.Contains(SelectedLocation)) ?? + Connections.Find(c => c.Locations.Contains(currentDisplayLocation) && c.Locations.Contains(SelectedLocation)) ?? Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation)); + if (SelectedConnection?.Locked ?? false) + { + DebugConsole.ThrowError("A locked connection was selected - this should not be possible.\n" + Environment.StackTrace.CleanupStackTrace()); + } OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection); } @@ -632,12 +749,15 @@ namespace Barotrauma SelectedLocation = location; SelectedConnection = Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation)); + if (SelectedConnection?.Locked ?? false) + { + DebugConsole.ThrowError("A locked connection was selected - this should not be possible.\n" + Environment.StackTrace.CleanupStackTrace()); + } OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection); } public void SelectMission(int missionIndex) { - if (SelectedConnection == null) { return; } if (CurrentLocation == null) { string errorMsg = "Failed to select a mission (current location not set)."; @@ -647,11 +767,18 @@ 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) + if (CurrentLocation.SelectedMission == null) { return; } + + if (CurrentLocation.SelectedMission.Locations[0] != CurrentLocation || + CurrentLocation.SelectedMission.Locations[1] != CurrentLocation) { - CurrentLocation.SelectedMissionIndex = -1; + if (SelectedConnection == null) { return; } + //the destination must be the same as the destination of the mission + if (CurrentLocation.SelectedMission != null && + CurrentLocation.SelectedMission.Locations[1] != SelectedLocation) + { + CurrentLocation.SelectedMissionIndex = -1; + } } OnMissionSelected?.Invoke(SelectedConnection, CurrentLocation.SelectedMission); @@ -659,7 +786,7 @@ namespace Barotrauma public void SelectRandomLocation(bool preferUndiscovered) { - List nextLocations = CurrentLocation.Connections.Select(c => c.OtherLocation(CurrentLocation)).ToList(); + List nextLocations = CurrentLocation.Connections.Where(c => !c.Locked).Select(c => c.OtherLocation(CurrentLocation)).ToList(); List undiscoveredLocations = nextLocations.FindAll(l => !l.Discovered); if (undiscoveredLocations.Count > 0 && preferUndiscovered) @@ -687,6 +814,8 @@ namespace Barotrauma { ProgressWorld(); } + + Radiation?.OnStep(steps); } private void ProgressWorld() @@ -710,7 +839,7 @@ namespace Barotrauma continue; } - if (location == CurrentLocation || location == SelectedLocation) { continue; } + if (location == CurrentLocation || location == SelectedLocation || location.IsGateBetweenBiomes) { continue; } ProgressLocationTypeChanges(location); @@ -724,20 +853,24 @@ namespace Barotrauma private void ProgressLocationTypeChanges(Location location) { location.TimeSinceLastTypeChange++; + location.LocationTypeChangeCooldown--; if (location.PendingLocationTypeChange != null) { - if (location.PendingLocationTypeChange.First.DetermineProbability(location) <= 0.0f) + if (location.PendingLocationTypeChange.Value.typeChange.DetermineProbability(location) <= 0.0f) { //remove pending type change if it's no longer allowed location.PendingLocationTypeChange = null; } else { - location.PendingLocationTypeChange.Second--; - if (location.PendingLocationTypeChange.Second <= 0) + location.PendingLocationTypeChange = + (location.PendingLocationTypeChange.Value.typeChange, + location.PendingLocationTypeChange.Value.delay - 1, + location.PendingLocationTypeChange.Value.parentMission); + if (location.PendingLocationTypeChange.Value.delay <= 0) { - ChangeLocationType(location, location.PendingLocationTypeChange.First); + ChangeLocationType(location, location.PendingLocationTypeChange.Value.typeChange); } return; } @@ -764,9 +897,10 @@ namespace Barotrauma { if (selectedTypeChange.RequiredDurationRange.X > 0) { - location.PendingLocationTypeChange = new Pair( - selectedTypeChange, - Rand.Range(selectedTypeChange.RequiredDurationRange.X, selectedTypeChange.RequiredDurationRange.Y)); + location.PendingLocationTypeChange = + (selectedTypeChange, + Rand.Range(selectedTypeChange.RequiredDurationRange.X, selectedTypeChange.RequiredDurationRange.Y), + null); } else { @@ -778,20 +912,19 @@ namespace Barotrauma foreach (LocationTypeChange typeChange in location.Type.CanChangeTo) { - if (typeChange.AnyWithinDistance( - location, - typeChange.RequiredProximityForProbabilityIncrease, - (otherLocation) => { return typeChange.RequiredLocations.Contains(otherLocation.Type.Identifier); })) + foreach (var requirement in typeChange.Requirements) { - if (!location.ProximityTimer.ContainsKey(typeChange)) { location.ProximityTimer[typeChange] = 0; } - location.ProximityTimer[typeChange] += 1; - } - else - { - location.ProximityTimer.Remove(typeChange); + if (requirement.AnyWithinDistance(location, requirement.RequiredProximityForProbabilityIncrease)) + { + if (!location.ProximityTimer.ContainsKey(requirement)) { location.ProximityTimer[requirement] = 0; } + location.ProximityTimer[requirement] += 1; + } + else + { + location.ProximityTimer.Remove(requirement); + } } } - } public int DistanceToClosestLocationWithOutpost(Location startingLocation, out Location endingLocation) @@ -844,8 +977,12 @@ namespace Barotrauma string prevName = location.Name; location.ChangeType(LocationType.List.Find(lt => lt.Identifier.Equals(change.ChangeToType, StringComparison.OrdinalIgnoreCase))); ChangeLocationTypeProjSpecific(location, prevName, change); - location.ProximityTimer.Remove(change); + foreach (var requirement in change.Requirements) + { + location.ProximityTimer.Remove(requirement); + } location.TimeSinceLastTypeChange = 0; + location.LocationTypeChangeCooldown = change.CooldownAfterChange; location.PendingLocationTypeChange = null; } @@ -856,9 +993,9 @@ namespace Barotrauma /// /// Load a previously saved map from an xml element /// - public static Map Load(CampaignMode campaign, XElement element) + public static Map Load(CampaignMode campaign, XElement element, CampaignSettings settings) { - Map map = new Map(campaign, element); + Map map = new Map(campaign, element, settings); map.LoadState(element, false); #if CLIENT map.DrawOffset = -map.CurrentLocation.MapPosition; @@ -889,16 +1026,12 @@ namespace Barotrauma location.ProximityTimer.Clear(); for (int i = 0; i < location.Type.CanChangeTo.Count; i++) { - location.ProximityTimer.Add(location.Type.CanChangeTo[i], subElement.GetAttributeInt("changetimer" + i, 0)); + for (int j = 0; j < location.Type.CanChangeTo[i].Requirements.Count; j++) + { + location.ProximityTimer.Add(location.Type.CanChangeTo[i].Requirements[j], subElement.GetAttributeInt("changetimer" + i + "-" + j, 0)); + } } - int locationTypeChangeIndex = subElement.GetAttributeInt("pendinglocationtypechange", -1); - if (locationTypeChangeIndex > 0 && locationTypeChangeIndex < location.Type.CanChangeTo.Count - 1) - { - location.PendingLocationTypeChange = new Pair( - location.Type.CanChangeTo[locationTypeChangeIndex], - subElement.GetAttributeInt("pendinglocationtypechangetimer", 0)); - } - location.TimeSinceLastTypeChange = subElement.GetAttributeInt("timesincelasttypechange", 0); + location.LoadLocationTypeChange(subElement); location.Discovered = subElement.GetAttributeBool("discovered", false); if (location.Discovered) { @@ -933,6 +1066,10 @@ namespace Barotrauma case "connection": int connectionIndex = subElement.GetAttributeInt("i", 0); Connections[connectionIndex].Passed = subElement.GetAttributeBool("passed", false); + Connections[connectionIndex].Locked = subElement.GetAttributeBool("locked", false); + break; + case "radiation": + Radiation = new Radiation(this, generationParams.RadiationParams, subElement); break; } } @@ -945,6 +1082,7 @@ namespace Barotrauma int currentLocationConnection = element.GetAttributeInt("currentlocationconnection", -1); if (currentLocationConnection >= 0) { + Connections[currentLocationConnection].Locked = false; SelectLocation(Connections[currentLocationConnection].OtherLocation(CurrentLocation)); } else @@ -975,6 +1113,8 @@ namespace Barotrauma mapElement.Add(new XAttribute("currentlocationconnection", Connections.IndexOf(Connections.Find(c => c.LevelData == Level.Loaded.LevelData)))); } } + mapElement.Add(new XAttribute("width", Width)); + mapElement.Add(new XAttribute("height", Height)); mapElement.Add(new XAttribute("selectedlocation", SelectedLocationIndex)); mapElement.Add(new XAttribute("startlocation", Locations.IndexOf(StartLocation))); mapElement.Add(new XAttribute("endlocation", Locations.IndexOf(EndLocation))); @@ -993,6 +1133,7 @@ namespace Barotrauma var connectionElement = new XElement("connection", new XAttribute("passed", connection.Passed), + new XAttribute("locked", connection.Locked), new XAttribute("difficulty", connection.Difficulty), new XAttribute("biome", connection.Biome.Identifier), new XAttribute("locations", Locations.IndexOf(connection.Locations[0]) + "," + Locations.IndexOf(connection.Locations[1]))); @@ -1000,6 +1141,11 @@ namespace Barotrauma mapElement.Add(connectionElement); } + if (Radiation != null) + { + mapElement.Add(Radiation.Save()); + } + element.Add(mapElement); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/MapGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/MapGenerationParams.cs index b745dff26..6e9194dd6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/MapGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/MapGenerationParams.cs @@ -125,6 +125,8 @@ namespace Barotrauma get; private set; } + public RadiationParams RadiationParams; + public static void Init() { @@ -238,6 +240,9 @@ namespace Barotrauma TypeChangeIcon = new Sprite(subElement); break; #endif + case "radiationparams": + RadiationParams = new RadiationParams(subElement); + break; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs new file mode 100644 index 000000000..d10ae10d8 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs @@ -0,0 +1,155 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + internal partial class Radiation : ISerializableEntity + { + public string Name => nameof(Radiation); + + [Serialize(defaultValue: 0f, isSaveable: true)] + public float Amount { get; set; } + + [Serialize(defaultValue: true, isSaveable: true)] + public bool Enabled { get; set; } + + public Dictionary SerializableProperties { get; } + + public readonly Map Map; + public readonly RadiationParams Params; + + private float radiationTimer; + + private float increasedAmount; + private float lastIncrease; + + public Radiation(Map map, RadiationParams radiationParams, XElement? element = null) + { + SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + Map = map; + Params = radiationParams; + radiationTimer = Params.RadiationDamageDelay; + if (element == null) + { + Amount = Params.StartingRadiation; + } + } + + /// + /// Advances the progress of the radiation. + /// + /// + public void OnStep(float steps = 1) + { + if (!Enabled) { return; } + if (steps <= 0) { return; } + + float increaseAmount = Params.RadiationStep * steps; + + if (Params.MaxRadiation > 0 && Params.MaxRadiation < Amount + increaseAmount) + { + increaseAmount = Params.MaxRadiation - Amount; + } + + IncreaseRadiation(increaseAmount); + + int amountOfOutposts = Map.Locations.Count(location => location.Type.HasOutpost && !location.IsCriticallyRadiated()); + + foreach (Location location in Map.Locations.Where(Contains)) + { + if (location.IsGateBetweenBiomes) + { + location.Connections.ForEach(c => c.Locked = false); + continue; + } + + if (amountOfOutposts <= Params.MinimumOutpostAmount) { break; } + + if (Map.CurrentLocation is { } currLocation) + { + // Don't advance on nearby locations to avoid buggy behavior + if (currLocation == location || currLocation.Connections.Any(lc => lc.OtherLocation(currLocation) == location)) { continue; } + } + + bool wasCritical = location.IsCriticallyRadiated(); + + location.TurnsInRadiation++; + + if (location.Type.HasOutpost && !wasCritical && location.IsCriticallyRadiated()) + { + amountOfOutposts--; + } + } + } + + public void IncreaseRadiation(float amount) + { + Amount += amount; + increasedAmount = lastIncrease = amount; + } + + public void UpdateRadiation(float deltaTime) + { + if (!(GameMain.GameSession?.IsCurrentLocationRadiated() ?? false)) { return; } + + if (GameMain.NetworkMember is { IsClient: true }) { return; } + + if (radiationTimer > 0) + { + radiationTimer -= deltaTime; + return; + } + + radiationTimer = Params.RadiationDamageDelay; + + foreach (Character character in Character.CharacterList) + { + if (character.IsDead || character.Removed || !(character.CharacterHealth is { } health)) { continue; } + + if (IsEntityRadiated(character)) + { + health.ApplyAffliction(null, new Affliction(AfflictionPrefab.RadiationSickness, Params.RadiationDamageAmount)); + } + } + } + + public bool Contains(Location location) + { + return Contains(location.MapPosition); + } + + public bool Contains(Vector2 pos) + { + return pos.X < Amount; + } + + public bool IsEntityRadiated(Entity entity) + { + if (!Enabled) { return false; } + if (Level.Loaded is { Type: LevelData.LevelType.LocationConnection, StartLocation: { } startLocation, EndLocation: { } endLocation } level) + { + if (Contains(startLocation) && Contains(endLocation)) { return true; } + + float distance = MathHelper.Clamp((entity.WorldPosition.X - level.StartPosition.X) / (level.EndPosition.X - level.StartPosition.X), 0.0f, 1.0f); + var (startX, startY) = startLocation.MapPosition; + var (endX, endY) = endLocation.MapPosition; + Vector2 mapPos = new Vector2(startX + (endX - startX), startY + (endY - startY)) * distance; + + return Contains(mapPos); + } + + return false; + } + + public XElement Save() + { + XElement element = new XElement(nameof(Radiation)); + SerializableProperty.SerializeProperties(this, element); + return element; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/RadiationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/RadiationParams.cs new file mode 100644 index 000000000..b61811521 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/RadiationParams.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Xml.Linq; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + internal class RadiationParams: ISerializableEntity + { + public string Name => nameof(RadiationParams); + public Dictionary SerializableProperties { get; } + + [Serialize(defaultValue: -100f, isSaveable: false, "How much radiation the world starts with.")] + public float StartingRadiation { get; set; } + + [Serialize(defaultValue: 100f, isSaveable: false, "How much radiation is added on each step.")] + public float RadiationStep { get; set; } + + [Serialize(defaultValue: 10, isSaveable: false, "How many turns in radiation does it take for an outpost to be removed from the map.")] + public int CriticalRadiationThreshold { get; set; } + + [Serialize(defaultValue: 3, isSaveable: false, "Minimum amount of outposts in the level that cannot be removed due to radiation.")] + public int MinimumOutpostAmount { get; set; } + + [Serialize(defaultValue: 3f, isSaveable: false, "How fast the radiation increase animation goes.")] + public float AnimationSpeed { get; set; } + + [Serialize(defaultValue: 10f, isSaveable: false, "How long it takes to apply more radiation damage while in a radiated zone.")] + public float RadiationDamageDelay { get; set; } + + [Serialize(defaultValue: 1f, isSaveable: false, "How much is the radiation affliction increased by while in a radiated zone.")] + public float RadiationDamageAmount { get; set; } + + [Serialize(defaultValue: -1.0f, isSaveable: false, "Maximum amount of radiation.")] + public float MaxRadiation { get; set; } + + [Serialize(defaultValue: "139,0,0,85", isSaveable: false, "The color of the radiated area.")] + public Color RadiationAreaColor { get; set; } + + [Serialize(defaultValue: "255,0,0,255", isSaveable: false, "The tint of the radiation border sprites.")] + public Color RadiationBorderTint { get; set; } + + [Serialize(defaultValue: 16.66f, isSaveable: false, "Speed of the border spritesheet animation.")] + public float BorderAnimationSpeed { get; set; } + + public RadiationParams(XElement element) + { + SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs index a3c0bc602..93eedb775 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs @@ -3,14 +3,23 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Xml.Linq; namespace Barotrauma { [Flags] enum MapEntityCategory { - Structure = 1, Decorative = 2, Machine = 4, Equipment = 8, Electrical = 16, Material = 32, Misc = 64, Alien = 128, Wrecked = 256, Thalamus = 512, ItemAssembly = 1024, Legacy = 2048 + Structure = 1, + Decorative = 2, + Machine = 4, + Equipment = 8, + Electrical = 16, + Material = 32, + Misc = 64, + Alien = 128, + Wrecked = 256, + ItemAssembly = 512, + Legacy = 1024 } abstract partial class MapEntityPrefab : IPrefab, IDisposable @@ -54,6 +63,7 @@ namespace Barotrauma //is it possible to stretch the entity horizontally/vertically [Serialize(false, false)] public bool ResizeHorizontal { get; protected set; } + [Serialize(false, false)] public bool ResizeVertical { get; protected set; } @@ -118,6 +128,9 @@ namespace Barotrauma [Serialize(false, false)] public bool HideInMenus { get; set; } + [Serialize("", false)] + public string Subcategory { get; set; } + [Serialize(false, false)] public bool Linkable { @@ -215,6 +228,11 @@ namespace Barotrauma return string.IsNullOrWhiteSpace(AllowedUpgrades) ? new string[0] : AllowedUpgrades.Split(","); } + public bool HasSubCategory(string subcategory) + { + return subcategory?.Equals(this.Subcategory, StringComparison.OrdinalIgnoreCase) ?? false; + } + protected virtual void CreateInstance(Rectangle rect) { if (constructor == null) return; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs index 99d83d7b2..577750efd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs @@ -39,6 +39,37 @@ namespace Barotrauma set; } + [Serialize(false, isSaveable: true), Editable] + public bool AlwaysDestructible + { + get; + set; + } + + [Serialize(false, isSaveable: true), Editable] + public bool AlwaysRewireable + { + get; + set; + } + + [Serialize(false, isSaveable: true), Editable] + public bool AllowStealing + { + get; + set; + } + + [Serialize(true, isSaveable: true), Editable] + public bool SpawnCrewInsideOutpost + { + get; + set; + } + + [Serialize("", isSaveable: true), Editable] + public string ReplaceInRadiation { get; set; } + private readonly Dictionary moduleCounts = new Dictionary(); public IEnumerable> ModuleCounts diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index 0ddc1b730..bef44a09f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -68,6 +68,15 @@ namespace Barotrauma private static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, Location location, bool onlyEntrance = false) { var outpostModuleFiles = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.OutpostModule); + if (location != null) + { + if (location.IsCriticallyRadiated() && OutpostGenerationParams.Params.FirstOrDefault(p => p.Identifier.Equals(generationParams.ReplaceInRadiation, StringComparison.OrdinalIgnoreCase)) is { } newParams) + { + generationParams = newParams; + } + + locationType = location.GetLocationType(); + } //load the infos of the outpost module files List outpostModules = new List(); @@ -169,6 +178,7 @@ namespace Barotrauma Type = SubmarineType.Outpost }; generationFailed = false; + outpostInfo.OutpostGenerationParams = generationParams; sub = new Submarine(outpostInfo, loadEntities: loadEntities); sub.Info.OutpostGenerationParams = generationParams; if (!generationFailed) @@ -669,10 +679,15 @@ namespace Barotrauma if (availableModules.Count() == 0) { return null; } - var modulesSuitableForLocationType = - availableModules.Where(m => - !m.OutpostModuleInfo.AllowedLocationTypes.Any() || - m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier.ToLowerInvariant())); + //try to search for modules made specifically for this location type first + var modulesSuitableForLocationType = + availableModules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier.ToLowerInvariant())); + + //if not found, search for modules suitable for any location type + if (!modulesSuitableForLocationType.Any()) + { + modulesSuitableForLocationType = availableModules.Where(m => !m.OutpostModuleInfo.AllowedLocationTypes.Any()); + } if (!modulesSuitableForLocationType.Any()) { @@ -705,10 +720,15 @@ namespace Barotrauma if (availableModules.Count() == 0) { return null; } + //try to search for modules made specifically for this location type first var modulesSuitableForLocationType = - availableModules.Where(m => - !m.OutpostModuleInfo.AllowedLocationTypes.Any() || - m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier.ToLowerInvariant())); + availableModules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier.ToLowerInvariant())); + + //if not found, search for modules suitable for any location type + if (!modulesSuitableForLocationType.Any()) + { + modulesSuitableForLocationType = availableModules.Where(m => !m.OutpostModuleInfo.AllowedLocationTypes.Any()); + } if (!modulesSuitableForLocationType.Any()) { @@ -963,7 +983,7 @@ namespace Barotrauma var moduleEntities = MapEntity.LoadAll(sub, hallwayInfo.SubmarineElement, hallwayInfo.FilePath, -1); //remove items that don't fit in the hallway - moduleEntities.Where(e => e is Item item && item.GetComponent() == null && e.Rect.Width > hallwayLength).ForEach(e => e.Remove()); + moduleEntities.Where(e => e is Item item && item.GetComponent() == null && (isHorizontal ? e.Rect.Width : e.Rect.Height) > hallwayLength).ForEach(e => e.Remove()); //find the largest hull to use it as the center point of the hallway //and the bounds of all the hulls, used when resizing the hallway to fit between the modules @@ -1036,11 +1056,11 @@ namespace Barotrauma } } } - else if (me is Structure structure) + else if (me is Structure || (me is Item item && item.GetComponent() == null)) { if (isHorizontal) { - if (!structure.ResizeHorizontal) + if (!me.ResizeHorizontal) { int xPos = (int)(leftHull.WorldRect.Right + (me.WorldPosition.X - hullBounds.X) * scaleFactor); me.Rect = new Rectangle(xPos - me.RectWidth / 2, me.Rect.Y, me.Rect.Width, me.Rect.Height); @@ -1054,9 +1074,9 @@ namespace Barotrauma } else { - if (!structure.ResizeVertical) + if (!me.ResizeVertical) { - int yPos = (int)(topHull.WorldRect.Y - topHull.RectHeight + (me.WorldPosition.X - hullBounds.Bottom) * scaleFactor); + int yPos = (int)(topHull.WorldRect.Y - topHull.RectHeight + (me.WorldPosition.Y - hullBounds.Bottom) * scaleFactor); me.Rect = new Rectangle(me.Rect.X, yPos + me.RectHeight / 2, me.Rect.Width, me.Rect.Height); } else @@ -1084,8 +1104,35 @@ namespace Barotrauma DebugConsole.ThrowError($"Failed to connect waypoints between outpost modules. No waypoint in the {GetOpposingGapPosition(module.ThisGapPosition).ToString().ToLower()} gap of the module \"{module.PreviousModule.Info.Name}\"."); continue; } - startWaypoint.linkedTo.Add(endWaypoint); - endWaypoint.linkedTo.Add(startWaypoint); + + if (startWaypoint.WorldPosition.X > endWaypoint.WorldPosition.X) + { + var temp = startWaypoint; + startWaypoint = endWaypoint; + endWaypoint = temp; + } + + if (hallwayLength > 100 && isHorizontal) + { + WayPoint prevWayPoint = startWaypoint; + for (float x = leftHull.Rect.Right + 50; x < rightHull.Rect.X - 50; x += 100.0f) + { + var newWayPoint = new WayPoint(new Vector2(x, hullBounds.Y + 110.0f), SpawnType.Path, sub); + prevWayPoint.linkedTo.Add(newWayPoint); + newWayPoint.linkedTo.Add(prevWayPoint); + prevWayPoint = newWayPoint; + } + if (prevWayPoint != null) + { + prevWayPoint.linkedTo.Add(endWaypoint); + endWaypoint.linkedTo.Add(prevWayPoint); + } + } + else + { + startWaypoint.linkedTo.Add(endWaypoint); + endWaypoint.linkedTo.Add(startWaypoint); + } WayPoint closestWaypoint = null; float closestDistSqr = 30.0f * 30.0f; @@ -1381,10 +1428,9 @@ namespace Barotrauma Rand.SetSyncedSeed(ToolBox.StringToInt(characterInfo.Name)); ISpatialEntity gotoTarget = SpawnAction.GetSpawnPos(SpawnAction.SpawnLocationType.Outpost, SpawnType.Human, humanPrefab.GetModuleFlags(), humanPrefab.GetSpawnPointTags()); - if (gotoTarget == null) { - gotoTarget = outpost.GetHulls(true).GetRandom(); + gotoTarget = outpost.GetHulls(true).GetRandom(Rand.RandSync.Server); } characterInfo.TeamID = CharacterTeamType.FriendlyNPC; var npc = Character.Create(CharacterPrefab.HumanConfigFile, SpawnAction.OffsetSpawnPos(gotoTarget.WorldPosition, 100.0f), ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true); @@ -1406,27 +1452,11 @@ namespace Barotrauma humanPrefab.GiveItems(npc, outpost, Rand.RandSync.Server); foreach (Item item in npc.Inventory.FindAllItems(it => it != null, recursive: true)) { + item.AllowStealing = outpost.Info.OutpostGenerationParams.AllowStealing; item.SpawnedInOutpost = true; } npc.GiveIdCardTags(gotoTarget as WayPoint); - if (npc.AIController is HumanAIController humanAI) - { - var idleObjective = humanAI.ObjectiveManager.GetObjective(); - if (humanPrefab.CampaignInteractionType != CampaignMode.InteractionType.None) - { - idleObjective.Behavior = AIObjectiveIdle.BehaviorType.StayInHull; - idleObjective.TargetHull = AIObjectiveGoTo.GetTargetHull(gotoTarget); - (GameMain.GameSession.GameMode as CampaignMode)?.AssignNPCMenuInteraction(npc, humanPrefab.CampaignInteractionType); - } - else - { - idleObjective.Behavior = humanPrefab.Behavior; - foreach (string moduleType in humanPrefab.PreferredOutpostModuleTypes) - { - idleObjective.PreferredOutpostModuleTypes.Add(moduleType); - } - } - } + humanPrefab.InitializeCharacter(npc, gotoTarget); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs index e9eea6444..78a7e14f3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs @@ -20,15 +20,20 @@ namespace Barotrauma /// Can the item be a Daily Special or a Requested Good /// public readonly bool CanBeSpecial; + /// + /// The item isn't available in stores unless the level's difficulty is above this value + /// + public readonly int MinLevelDifficulty; /// /// Support for the old style of determining item prices /// when there were individual Price elements for each location type /// where the item was for sale. /// - public PriceInfo (XElement element) + public PriceInfo(XElement element) { Price = element.GetAttributeInt("buyprice", 0); + MinLevelDifficulty = element.GetAttributeInt("minleveldifficulty", 0); CanBeBought = true; var minAmount = GetMinAmount(element); MinAvailableAmount = Math.Min(minAmount, CargoManager.MaxQuantity); @@ -37,13 +42,14 @@ namespace Barotrauma MaxAvailableAmount = Math.Max(maxAmount, MinAvailableAmount); } - public PriceInfo(int price, bool canBeBought, int minAmount = 0, int maxAmount = 0, bool canBeSpecial = true) + public PriceInfo(int price, bool canBeBought, int minAmount = 0, int maxAmount = 0, bool canBeSpecial = true, int minLevelDifficulty = 0) { Price = price; CanBeBought = canBeBought; MinAvailableAmount = Math.Min(minAmount, CargoManager.MaxQuantity); maxAmount = Math.Min(maxAmount, CargoManager.MaxQuantity); MaxAvailableAmount = Math.Max(maxAmount, minAmount); + MinLevelDifficulty = minLevelDifficulty; CanBeSpecial = canBeSpecial; } @@ -54,6 +60,7 @@ namespace Barotrauma var soldByDefault = element.GetAttributeBool("soldbydefault", true); var minAmount = GetMinAmount(element); var maxAmount = GetMaxAmount(element); + var minLevelDifficulty = element.GetAttributeInt("minleveldifficulty", 0); var canBeSpecial = element.GetAttributeBool("canbespecial", true); var priceInfos = new List>(); @@ -65,14 +72,16 @@ namespace Barotrauma new PriceInfo(price: (int)(priceMultiplier * basePrice), canBeBought: sold, minAmount: sold ? GetMinAmount(childElement, minAmount) : 0, maxAmount: sold ? GetMaxAmount(childElement, maxAmount) : 0, - canBeSpecial: canBeSpecial))); + canBeSpecial, + childElement.GetAttributeInt("minleveldifficulty", minLevelDifficulty)))); } var canBeBoughtAtOtherLocations = soldByDefault && element.GetAttributeBool("soldeverywhere", true); defaultPrice = new PriceInfo(basePrice, canBeBoughtAtOtherLocations, minAmount: canBeBoughtAtOtherLocations ? minAmount : 0, maxAmount: canBeBoughtAtOtherLocations ? maxAmount : 0, - canBeSpecial: canBeSpecial); + canBeSpecial, + minLevelDifficulty); return priceInfos; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index f027e8008..6c7fe54dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -637,7 +637,7 @@ namespace Barotrauma Vector2 bodyPos = WorldPosition + BodyOffset; - Vector2 transformedMousePos = MathUtils.RotatePointAroundTarget(position, bodyPos, MathHelper.ToDegrees(BodyRotation)); + Vector2 transformedMousePos = MathUtils.RotatePointAroundTarget(position, bodyPos, BodyRotation); return Math.Abs(transformedMousePos.X - bodyPos.X) < rectSize.X / 2.0f && @@ -819,16 +819,12 @@ namespace Barotrauma } for (int i = 1; i <= particleAmount; i++) { + var worldRect = section.WorldRect; Vector2 particlePos = new Vector2( - Rand.Range(section.rect.X, section.rect.Right), - Rand.Range(section.rect.Y - section.rect.Height, section.rect.Y)); + Rand.Range(worldRect.X, worldRect.Right), + Rand.Range(worldRect.Y - worldRect.Height, worldRect.Y)); - if (Submarine != null) - { - particlePos += Submarine.DrawPosition; - } - - var particle = GameMain.ParticleManager.CreateParticle("shrapnel", particlePos, Rand.Vector(Rand.Range(1.0f, 50.0f))); + var particle = GameMain.ParticleManager.CreateParticle("shrapnel", particlePos, Rand.Vector(Rand.Range(1.0f, 50.0f)), collisionIgnoreTimer: 1f); if (particle == null) break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs index 6afe488b6..d4e620846 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs @@ -360,7 +360,8 @@ namespace Barotrauma } } - if (!Enum.TryParse(element.GetAttributeString("category", "Structure"), true, out MapEntityCategory category)) + string categoryStr = element.GetAttributeString("category", "Structure"); + if (!Enum.TryParse(categoryStr, true, out MapEntityCategory category)) { category = MapEntityCategory.Structure; } @@ -419,6 +420,13 @@ namespace Barotrauma } } + //backwards compatibility + if (categoryStr.Equals("Thalamus", StringComparison.OrdinalIgnoreCase)) + { + sp.Category = MapEntityCategory.Wrecked; + sp.Subcategory = "Thalamus"; + } + if (string.IsNullOrEmpty(sp.identifier)) { DebugConsole.ThrowError( diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index 783bf3730..e6e6254ee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -133,7 +133,7 @@ namespace Barotrauma public Rectangle Borders { - get + get { return subBody == null ? Rectangle.Empty : subBody.Borders; } @@ -155,7 +155,7 @@ namespace Barotrauma private float? realWorldCrushDepth; public float RealWorldCrushDepth { - get + get { if (!realWorldCrushDepth.HasValue) { @@ -165,13 +165,11 @@ namespace Barotrauma if (structure.Submarine != this || !structure.HasBody || structure.Indestructible) { continue; } realWorldCrushDepth = Math.Min(structure.CrushDepth, realWorldCrushDepth.Value); } - if (Info.SubmarineClass == SubmarineClass.DeepDiver) - { - realWorldCrushDepth *= 1.2f; - } + realWorldCrushDepth *= Info.GetRealWorldCrushDepthMultiplier(); } return realWorldCrushDepth.Value; } + set { realWorldCrushDepth = value; } } /// @@ -179,34 +177,30 @@ namespace Barotrauma /// public float RealWorldDepth { - get + get { if (Level.Loaded?.GenerationParams == null) { return -WorldPosition.Y * Physics.DisplayToRealWorldRatio; } - else if (GameMain.GameSession?.Campaign == null) - { - return (-(WorldPosition.Y - Level.Loaded.GenerationParams.Height) + 80000.0f) * Physics.DisplayToRealWorldRatio; - } return Level.Loaded.GetRealWorldDepth(WorldPosition.Y); } } - public bool AtEndPosition + public bool AtEndExit { - get + get { if (Level.Loaded == null) { return false; } if (Level.Loaded.EndOutpost != null && DockedTo.Contains(Level.Loaded.EndOutpost)) { return true; } - return (Vector2.DistanceSquared(Position + HiddenSubPosition, Level.Loaded.EndPosition) < Level.ExitDistance * Level.ExitDistance); + return (Vector2.DistanceSquared(Position + HiddenSubPosition, Level.Loaded.EndExitPosition) < Level.ExitDistance * Level.ExitDistance); } } - public bool AtStartPosition + public bool AtStartExit { get { @@ -215,7 +209,7 @@ namespace Barotrauma { return true; } - return (Vector2.DistanceSquared(Position + HiddenSubPosition, Level.Loaded.StartPosition) < Level.ExitDistance * Level.ExitDistance); + return (Vector2.DistanceSquared(Position + HiddenSubPosition, Level.Loaded.StartExitPosition) < Level.ExitDistance * Level.ExitDistance); } } @@ -251,10 +245,10 @@ namespace Barotrauma public bool AtDamageDepth { - get + get { if (Level.Loaded == null || subBody == null) { return false; } - return RealWorldDepth > Level.Loaded.RealWorldCrushDepth & RealWorldDepth > RealWorldCrushDepth; + return RealWorldDepth > Level.Loaded.RealWorldCrushDepth && RealWorldDepth > RealWorldCrushDepth; } } @@ -324,6 +318,7 @@ namespace Barotrauma { Info.Type = SubmarineType.Wreck; ShowSonarMarker = false; + DockedTo.ForEach(s => s.ShowSonarMarker = false); PhysicsBody.FarseerBody.BodyType = BodyType.Static; TeamID = CharacterTeamType.None; @@ -333,7 +328,7 @@ namespace Barotrauma foreach (Item item in Item.ItemList) { if (item.Submarine != this) { continue; } - if (item.prefab.Identifier == "idcardwreck" || item.prefab.Identifier == "idcard") + if (item.prefab.Identifier == "idcardwreck" || item.prefab.Identifier == "idcard") { foreach (string tag in item.GetTags().ToList()) { @@ -341,7 +336,7 @@ namespace Barotrauma string newTag = Level.Loaded.GetWreckIDTag(tag, this); item.ReplaceTag(tag, newTag); ReplaceIDCardTagRequirements(tag, newTag); - } + } } } @@ -455,7 +450,7 @@ namespace Barotrauma public Vector2 FindSpawnPos(Vector2 spawnPos, Point? submarineSize = null, float subDockingPortOffset = 0.0f, int verticalMoveDir = 0) { Rectangle dockedBorders = GetDockedBorders(); - Vector2 diffFromDockedBorders = + Vector2 diffFromDockedBorders = new Vector2(dockedBorders.Center.X, dockedBorders.Y - dockedBorders.Height / 2) - new Vector2(Borders.Center.X, Borders.Y - Borders.Height / 2); @@ -506,7 +501,7 @@ namespace Barotrauma (e.Point1.Y > refPos.Y + minHeight * 0.5f && e.Point2.Y > refPos.Y + minHeight * 0.5f)) { continue; - } + } if (cell.Site.Coord.X < refPos.X) { @@ -553,7 +548,7 @@ namespace Barotrauma //walls found at both sides, use their midpoint spawnPos.X = (limits.X + limits.Y) / 2 + subDockingPortOffset; } - + spawnPos.Y = MathHelper.Clamp(spawnPos.Y, dockedBorders.Height / 2 + 10, Level.Loaded.Size.Y - dockedBorders.Height / 2 - padding * 2); return spawnPos - diffFromDockedBorders; } @@ -587,18 +582,31 @@ namespace Barotrauma { if (e is Item item) { + if (item.GetComponent() != null) { return false; } if (item.body != null && !item.body.Enabled) { return true; } } + if (e.HiddenInGame) { return true; } return false; }); - if (entities.Count == 0) return Rectangle.Empty; + if (entities.Count == 0) { return Rectangle.Empty; } float minX = entities[0].Rect.X, minY = entities[0].Rect.Y - entities[0].Rect.Height; float maxX = entities[0].Rect.Right, maxY = entities[0].Rect.Y; for (int i = 1; i < entities.Count; i++) { + if (entities[i] is Item item) + { + var turret = item.GetComponent(); + if (turret != null) + { + minX = Math.Min(minX, entities[i].Rect.X + turret.TransformedBarrelPos.X * 2f); + minY = Math.Min(minY, entities[i].Rect.Y - entities[i].Rect.Height - turret.TransformedBarrelPos.Y * 2f); + maxX = Math.Max(maxX, entities[i].Rect.Right + turret.TransformedBarrelPos.X * 2f); + maxY = Math.Max(maxY, entities[i].Rect.Y - turret.TransformedBarrelPos.Y * 2f); + } + } minX = Math.Min(minX, entities[i].Rect.X); minY = Math.Min(minY, entities[i].Rect.Y - entities[i].Rect.Height); maxX = Math.Max(maxX, entities[i].Rect.Right); @@ -607,7 +615,7 @@ namespace Barotrauma return new Rectangle((int)minX, (int)minY, (int)(maxX - minX), (int)(maxY - minY)); } - + public static Rectangle AbsRect(Vector2 pos, Vector2 size) { if (size.X < 0.0f) @@ -620,7 +628,7 @@ namespace Barotrauma pos.Y -= size.Y; size.Y = -size.Y; } - + return new Rectangle((int)pos.X, (int)pos.Y, (int)size.X, (int)size.Y); } @@ -674,7 +682,7 @@ namespace Barotrauma closestFraction = 0.0f; closestNormal = Vector2.Normalize(rayEnd - rayStart); - if (fixture.Body != null) closestBody = fixture.Body; + if (fixture.Body != null) closestBody = fixture.Body; return false; }, ref aabb); if (closestFraction <= 0.0f) @@ -685,7 +693,7 @@ namespace Barotrauma return closestBody; } } - + GameMain.World.RayCast((fixture, point, normal, fraction) => { if (!CheckFixtureCollision(fixture, ignoredBodies, collisionCategory, ignoreSensors, customPredicate)) { return -1; } @@ -702,7 +710,7 @@ namespace Barotrauma lastPickedPosition = rayStart + (rayEnd - rayStart) * closestFraction; lastPickedFraction = closestFraction; lastPickedNormal = closestNormal; - + return closestBody; } @@ -827,13 +835,13 @@ namespace Barotrauma lastPickedPosition = rayEnd; return null; } - + GameMain.World.RayCast((fixture, point, normal, fraction) => { if (fixture == null) { return -1; } if (ignoreSensors && fixture.IsSensor) { return -1; } if (ignoreLevel && fixture.CollisionCategories.HasFlag(Physics.CollisionLevel)) { return -1; } - if (!fixture.CollisionCategories.HasFlag(Physics.CollisionLevel) + if (!fixture.CollisionCategories.HasFlag(Physics.CollisionLevel) && !fixture.CollisionCategories.HasFlag(Physics.CollisionWall) && !fixture.CollisionCategories.HasFlag(Physics.CollisionRepair)) { return -1; } if (ignoreSubs && fixture.Body.UserData is Submarine) { return -1; } @@ -880,7 +888,7 @@ namespace Barotrauma parents.Add(this); flippedX = !flippedX; - + Item.UpdateHulls(); List bodyItems = Item.ItemList.FindAll(it => it.Submarine == this && it.body != null); @@ -1049,6 +1057,7 @@ namespace Barotrauma } steering.MaintainPos = true; + steering.PosToMaintain = WorldPosition; steering.AutoPilot = true; #if SERVER steering.UnsentChanges = true; @@ -1137,7 +1146,7 @@ namespace Barotrauma { if (ConnectedDockingPorts.TryGetValue(dockedSub, out DockingPort port)) { - port.Undock(); + port.Undock(applyEffects: false); continue; } } @@ -1164,7 +1173,8 @@ namespace Barotrauma subBody.SetPosition(subBody.Position + amount); } - public static Submarine FindClosest(Vector2 worldPosition, bool ignoreOutposts = false, bool ignoreOutsideLevel = true) + /// If has value, the sub must match the team type. + public static Submarine FindClosest(Vector2 worldPosition, bool ignoreOutposts = false, bool ignoreOutsideLevel = true, bool ignoreRespawnShuttle = false, CharacterTeamType? teamType = null) { Submarine closest = null; float closestDist = 0.0f; @@ -1172,6 +1182,11 @@ namespace Barotrauma { if (ignoreOutposts && sub.Info.IsOutpost) { continue; } if (ignoreOutsideLevel && Level.Loaded != null && sub.WorldPosition.Y > Level.Loaded.Size.Y) { continue; } + if (ignoreRespawnShuttle) + { + if (sub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle) { continue; } + } + if (teamType.HasValue && sub.TeamID != teamType) { continue; } float dist = Vector2.DistanceSquared(worldPosition, sub.WorldPosition); if (closest == null || dist < closestDist) { @@ -1334,14 +1349,19 @@ namespace Barotrauma PhysicsBody.FarseerBody.BodyType = BodyType.Static; TeamID = CharacterTeamType.FriendlyNPC; + bool indestructible = + GameMain.NetworkMember != null && + !GameMain.NetworkMember.ServerSettings.DestructibleOutposts && + !(info.OutpostGenerationParams?.AlwaysDestructible ?? false); + foreach (MapEntity me in MapEntity.mapEntityList) { if (me.Submarine != this) { continue; } if (me is Item item) { - item.SpawnedInOutpost = true; - if (item.GetComponent() != null && - (GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.DestructibleOutposts)) + item.SpawnedInOutpost = info.OutpostGenerationParams != null; + item.AllowStealing = info.OutpostGenerationParams?.AllowStealing ?? true; + if (item.GetComponent() != null && indestructible) { item.Indestructible = true; } @@ -1350,7 +1370,10 @@ namespace Barotrauma if (ic is ConnectionPanel connectionPanel) { //prevent rewiring - connectionPanel.Locked = true; + if (info.OutpostGenerationParams != null && !info.OutpostGenerationParams.AlwaysRewireable) + { + connectionPanel.Locked = true; + } } else if (ic is Holdable holdable && holdable.Attached && item.GetComponent() == null) { @@ -1363,9 +1386,9 @@ namespace Barotrauma } } } - else if (me is Structure structure && structure.Prefab.IndestructibleInOutposts) + else if (me is Structure structure && structure.Prefab.IndestructibleInOutposts && indestructible) { - structure.Indestructible = GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.DestructibleOutposts; + structure.Indestructible = true; } } } @@ -1415,7 +1438,7 @@ namespace Barotrauma //halve the brightness of the lights to make them look (almost) right on the new lighting formula if (showWarningMessages && !string.IsNullOrEmpty(Info.FilePath) && - Screen.Selected != GameMain.SubEditorScreen && + Screen.Selected != GameMain.SubEditorScreen && (Info.GameVersion == null || Info.GameVersion < new Version("0.8.9.0"))) { DebugConsole.ThrowError("The submarine \"" + Info.Name + "\" was made using an older version of the Barotrauma that used a different formula to calculate the lighting. " @@ -1428,6 +1451,7 @@ namespace Barotrauma if (lightComponent != null) lightComponent.LightColor = new Color(lightComponent.LightColor, lightComponent.LightColor.A / 255.0f * 0.5f); } } + GenerateOutdoorNodes(); } protected override ushort DetermineID(ushort id, Submarine submarine) @@ -1483,7 +1507,7 @@ namespace Barotrauma element.Add(new XAttribute("recommendedcrewsizemax", Info.RecommendedCrewSizeMax)); element.Add(new XAttribute("recommendedcrewexperience", Info.RecommendedCrewExperience ?? "")); element.Add(new XAttribute("requiredcontentpackages", string.Join(", ", Info.RequiredContentPackages))); - + if (Info.Type == SubmarineType.OutpostModule) { Info.OutpostModuleInfo?.Save(element); @@ -1589,7 +1613,7 @@ namespace Barotrauma PhysicsBody.RemoveAll(); - GameMain.World.Clear(); + GameMain.World.Clear(); Unloading = false; } @@ -1634,12 +1658,18 @@ namespace Barotrauma { if (outdoorNodes == null) { - outdoorNodes = PathNode.GenerateNodes(WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Path && wp.Submarine == this && wp.CurrentHull == null)); + GenerateOutdoorNodes(); } return outdoorNodes; } } + private void GenerateOutdoorNodes() + { + var waypoints = WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Path && wp.Submarine == this && wp.CurrentHull == null); + outdoorNodes = PathNode.GenerateNodes(waypoints, removeOrphans: false); + } + private readonly Dictionary> obstructedNodes = new Dictionary>(); /// @@ -1707,7 +1737,6 @@ namespace Barotrauma } } } - node.Waypoint.FindHull(); } } @@ -1722,7 +1751,8 @@ namespace Barotrauma nodes.Clear(); obstructedNodes.Remove(otherSub); } - OutdoorNodes.ForEach(n => n.Waypoint.FindHull()); } + + public void RefreshOutdoorNodes() => OutdoorNodes.ForEach(n => n?.Waypoint?.FindHull()); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index 19bf9df68..6b789bec1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -1,4 +1,5 @@ using Barotrauma.Extensions; +using Barotrauma.Items.Components; using Barotrauma.Networking; using FarseerPhysics; using FarseerPhysics.Collision; @@ -449,12 +450,21 @@ namespace Barotrauma if (GameMain.GameSession?.GameMode is TestGameMode) { return; } #endif if (Level.Loaded == null) { return; } - float submarineDepth = submarine.RealWorldDepth; - if (!Submarine.AtDamageDepth) { return; } + + //camera shake and sounds start playing 500 meters before crush depth + float depthEffectThreshold = 500.0f; + if (Submarine.RealWorldDepth < Level.Loaded.RealWorldCrushDepth - depthEffectThreshold || Submarine.RealWorldDepth < Submarine.RealWorldCrushDepth - depthEffectThreshold) + { + return; + } depthDamageTimer -= deltaTime; if (depthDamageTimer > 0.0f) { return; } +#if CLIENT + SoundPlayer.PlayDamageSound("pressure", Rand.Range(0.0f, 100.0f), submarine.WorldPosition + Rand.Vector(Rand.Range(0.0f, Math.Min(submarine.Borders.Width, submarine.Borders.Height))), 20000.0f); +#endif + foreach (Structure wall in Structure.WallList) { if (wall.Submarine != submarine) { continue; } @@ -462,12 +472,14 @@ namespace Barotrauma float wallCrushDepth = wall.CrushDepth; if (submarine.Info.SubmarineClass == SubmarineClass.DeepDiver) { wallCrushDepth *= 1.2f; } float pastCrushDepth = submarine.RealWorldDepth - wallCrushDepth; - if (pastCrushDepth < 0) { return; } - Explosion.RangedStructureDamage(wall.WorldPosition, 100.0f, pastCrushDepth * 0.1f, levelWallDamage: 0.0f); + if (pastCrushDepth > 0) + { + Explosion.RangedStructureDamage(wall.WorldPosition, 100.0f, pastCrushDepth * 0.1f, levelWallDamage: 0.0f); + } if (Character.Controlled != null && Character.Controlled.Submarine == submarine) { - GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, Math.Min(pastCrushDepth * 0.001f, 50.0f)); - } + GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, MathHelper.Clamp(pastCrushDepth * 0.001f, 1.0f, 50.0f)); + } } depthDamageTimer = 10.0f; @@ -558,19 +570,29 @@ namespace Barotrauma { if (limb?.body?.FarseerBody == null || limb.character == null) { return; } - if (limb.Mass > MinImpactLimbMass) + float impactMass = limb.Mass; + var enemyAI = limb.character.AIController as EnemyAIController; + float attackMultiplier = 1.0f; + if (enemyAI?.ActiveAttack != null) + { + impactMass = Math.Max(Math.Max(limb.Mass, limb.character.AnimController.MainLimb.Mass), limb.character.AnimController.Collider.Mass); + attackMultiplier = enemyAI.ActiveAttack.SubmarineImpactMultiplier; + } + + if (impactMass * attackMultiplier > MinImpactLimbMass) { Vector2 normal = Vector2.DistanceSquared(Body.SimPosition, limb.SimPosition) < 0.0001f ? Vector2.UnitY : Vector2.Normalize(Body.SimPosition - limb.SimPosition); - float impact = Math.Min(Vector2.Dot(collision.Velocity, -normal), 50.0f) * Math.Min(limb.Mass / 100.0f, 1); + float impact = Math.Min(Vector2.Dot(collision.Velocity, -normal), 50.0f) * Math.Min(impactMass / 300.0f, 1); + impact *= attackMultiplier; - ApplyImpact(impact, -normal, collision.ImpactPos, applyDamage: false); + ApplyImpact(impact, normal, collision.ImpactPos, applyDamage: false); foreach (Submarine dockedSub in submarine.DockedTo) { - dockedSub.SubBody.ApplyImpact(impact, -normal, collision.ImpactPos, applyDamage: false); + dockedSub.SubBody.ApplyImpact(impact, normal, collision.ImpactPos, applyDamage: false); } } @@ -803,7 +825,7 @@ namespace Barotrauma #if CLIENT if (Character.Controlled != null && Character.Controlled.Submarine == submarine) { - GameMain.GameScreen.Cam.Shake = impact * 2.0f; + GameMain.GameScreen.Cam.Shake = impact * 10.0f; if (submarine.Info.Type == SubmarineType.Player && !submarine.DockedTo.Any(s => s.Info.Type != SubmarineType.Player)) { float angularVelocity = @@ -817,34 +839,41 @@ namespace Barotrauma foreach (Character c in Character.CharacterList) { if (c.Submarine != submarine) { continue; } - + if (c.KnockbackCooldownTimer > 0.0f) { continue; } + + c.KnockbackCooldownTimer = Character.KnockbackCooldown; + foreach (Limb limb in c.AnimController.Limbs) { if (limb.IsSevered) { continue; } limb.body.ApplyLinearImpulse(limb.Mass * impulse, 10.0f); } - c.AnimController.Collider.ApplyLinearImpulse(c.AnimController.Collider.Mass * impulse, 10.0f); bool holdingOntoSomething = false; if (c.SelectedConstruction != null) { - var controller = c.SelectedConstruction.GetComponent(); - holdingOntoSomething = controller != null && controller.LimbPositions.Any(); + holdingOntoSomething = + c.SelectedConstruction.GetComponent() != null || + (c.SelectedConstruction.GetComponent()?.LimbPositions.Any() ?? false); } - //stun for up to 1 second if the impact equal or higher to the maximum impact - if (impact >= MaxCollisionImpact && !holdingOntoSomething) + if (!holdingOntoSomething) { - c.SetStun(Math.Min(impulse.Length() * 0.2f, 1.0f)); + c.AnimController.Collider.ApplyLinearImpulse(c.AnimController.Collider.Mass * impulse, 10.0f); + //stun for up to 2 second if the impact equal or higher to the maximum impact + if (impact >= MaxCollisionImpact) + { + c.AddDamage(impactPos, AfflictionPrefab.ImpactDamage.Instantiate(3.0f).ToEnumerable(), stun: Math.Min(impulse.Length() * 0.2f, 2.0f), playSound: true); + } } } foreach (Item item in Item.ItemList) { - if (item.Submarine != submarine || item.CurrentHull == null || - item.body == null || !item.body.Enabled) continue; + if (item.Submarine != submarine || item.CurrentHull == null || item.body == null || !item.body.Enabled) { continue; } item.body.ApplyLinearImpulse(item.body.Mass * impulse, 10.0f); + item.PositionUpdateInterval = 0.0f; } float dmg = applyDamage ? impact * ImpactDamageMultiplier : 0.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index 3c056fbbc..97c714f73 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -95,8 +95,9 @@ namespace Barotrauma public OutpostModuleInfo OutpostModuleInfo { get; set; } - public bool IsOutpost => Type == SubmarineType.Outpost; + public bool IsOutpost => Type == SubmarineType.Outpost || Type == SubmarineType.OutpostModule; public bool IsWreck => Type == SubmarineType.Wreck; + public bool IsBeacon => Type == SubmarineType.BeaconStation; public bool IsPlayer => Type == SubmarineType.Player; public bool IsCampaignCompatible => IsPlayer && !HasTag(SubmarineTag.Shuttle) && !HasTag(SubmarineTag.HideInMenus) && SubmarineClass != SubmarineClass.Undefined; @@ -274,7 +275,7 @@ namespace Barotrauma OutpostModuleInfo = new OutpostModuleInfo(original.OutpostModuleInfo); } #if CLIENT - PreviewImage = original.PreviewImage != null ? new Sprite(original.PreviewImage.Texture, null, null) : null; + PreviewImage = original.PreviewImage != null ? new Sprite(original.PreviewImage) : null; #endif } @@ -463,6 +464,49 @@ namespace Barotrauma } } + /// + /// Calculated from . Can be used when the sub hasn't been loaded and we can't access . + /// + public float GetRealWorldCrushDepth() + { + if (SubmarineElement == null) { return Level.DefaultRealWorldCrushDepth; } + bool structureCrushDepthsDefined = false; + float realWorldCrushDepth = float.PositiveInfinity; + foreach (var structureElement in SubmarineElement.GetChildElements("structure")) + { + string name = structureElement.Attribute("name")?.Value ?? ""; + string identifier = structureElement.GetAttributeString("identifier", ""); + var structurePrefab = Structure.FindPrefab(name, identifier); + if (structurePrefab == null || !structurePrefab.Body) { continue; } + if (!structureCrushDepthsDefined && structureElement.Attribute("crushdepth") != null) + { + structureCrushDepthsDefined = true; + } + float structureCrushDepth = structureElement.GetAttributeFloat("crushdepth", float.PositiveInfinity); + realWorldCrushDepth = Math.Min(structureCrushDepth, realWorldCrushDepth); + } + if (!structureCrushDepthsDefined) + { + realWorldCrushDepth = Level.DefaultRealWorldCrushDepth; + } + realWorldCrushDepth *= GetRealWorldCrushDepthMultiplier(); + return realWorldCrushDepth; + } + + /// + /// Based on + /// + public float GetRealWorldCrushDepthMultiplier() + { + if (SubmarineClass == SubmarineClass.DeepDiver) + { + return 1.2f; + } + else + { + return 1.0f; + } + } //saving/loading ---------------------------------------------------- public bool SaveAs(string filePath, System.IO.MemoryStream previewImage = null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index 633aec372..0d3bf3e32 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -11,7 +11,9 @@ using Barotrauma.Extensions; namespace Barotrauma { - public enum SpawnType { Path = 0, Human = 1, Enemy = 2, Cargo = 3, Corpse = 4 }; + [Flags] + public enum SpawnType { Path = 0, Human = 1, Enemy = 2, Cargo = 4, Corpse = 8 }; + partial class WayPoint : MapEntity { public static List WayPointList = new List(); @@ -142,7 +144,7 @@ namespace Barotrauma DebugConsole.Log("Created waypoint (" + ID + ")"); - CurrentHull = Hull.FindHull(WorldPosition); + FindHull(); } public override MapEntity Clone() @@ -784,12 +786,19 @@ namespace Barotrauma public void FindHull() { CurrentHull = Hull.FindHull(WorldPosition, CurrentHull); +#if CLIENT + //we may not be able to find the hull with the optimized method in the sub editor if new hulls have been added, use the unoptimized method + if (Screen.Selected == GameMain.SubEditorScreen) + { + CurrentHull ??= Hull.FindHullUnoptimized(WorldPosition); + } +#endif } public override void OnMapLoaded() { InitializeLinks(); - CurrentHull = Hull.FindHull(WorldPosition, CurrentHull); + FindHull(); FindStairs(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs index d4c48430a..c60de64dd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs @@ -8,7 +8,18 @@ namespace Barotrauma.Networking { public enum ChatMessageType { - Default, Error, Dead, Server, Radio, Private, Console, MessageBox, Order, ServerLog, ServerMessageBox, ServerMessageBoxInGame + Default = 0, + Error = 1, + Dead = 2, + Server = 3, + Radio = 4, + Private = 5, + Console = 6, + MessageBox = 7, + Order = 8, + ServerLog = 9, + ServerMessageBox = 10, + ServerMessageBoxInGame = 11 } public enum PlayerConnectionChangeType { None = 0, Joined = 1, Kicked = 2, Disconnected = 3, Banned = 4 } @@ -82,7 +93,7 @@ namespace Barotrauma.Networking { get { - return string.IsNullOrWhiteSpace(SenderName) ? TranslatedText : SenderName + ": " + TranslatedText; + return string.IsNullOrWhiteSpace(SenderName) ? TranslatedText : NetworkMember.ClientLogName(SenderClient, SenderName) + ": " + TranslatedText; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs index 9a4c83272..332de65d0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs @@ -13,6 +13,7 @@ namespace Barotrauma.Networking public string Name; public UInt16 NameID; public byte ID; public UInt64 SteamID; + public UInt64 OwnerSteamID; public string Language; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs index 5fca74a45..ddf794d29 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs @@ -14,15 +14,6 @@ namespace Barotrauma.Networking public static string MasterServerUrl = GameMain.Config.MasterServerUrl; - //if a Character is further than this from the sub and the players, the server will disable it - //(in display units) - public const float DisableCharacterDist = 22000.0f; - public const float DisableCharacterDistSqr = DisableCharacterDist * DisableCharacterDist; - - //the character needs to get this close to be re-enabled - public const float EnableCharacterDist = 20000.0f; - public const float EnableCharacterDistSqr = EnableCharacterDist * EnableCharacterDist; - public const float MaxPhysicsBodyVelocity = 64.0f; public const float MaxPhysicsBodyAngularVelocity = 16.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEvent.cs index 09506cf41..33ef8bec8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEvent.cs @@ -18,7 +18,8 @@ namespace Barotrauma.Networking Combine, ExecuteAttack, Upgrade, - AssignCampaignInteraction + AssignCampaignInteraction, + ObjectiveManagerOrderState, } public readonly Entity Entity; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs index 709c4d002..d60b084ea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -30,7 +30,8 @@ namespace Barotrauma.Networking ERROR, //tell the server that an error occurred CREW, - READY_CHECK + READY_CHECK, + READY_TO_SPAWN } enum ClientNetObject @@ -172,7 +173,7 @@ namespace Barotrauma.Networking #endif protected ServerSettings serverSettings; - + protected TimeSpan updateInterval; protected DateTime updateTimer; @@ -236,9 +237,9 @@ namespace Barotrauma.Networking return radioComponent.HasRequiredContainedItems(sender, addMessage: false); } - public void AddChatMessage(string message, ChatMessageType type, string senderName = "", Character senderCharacter = null, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None) + public void AddChatMessage(string message, ChatMessageType type, string senderName = "", Client senderClient = null, Character senderCharacter = null, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None) { - AddChatMessage(ChatMessage.Create(senderName, message, type, senderCharacter, changeType: changeType)); + AddChatMessage(ChatMessage.Create(senderName, message, type, senderCharacter, senderClient, changeType: changeType)); } public virtual void AddChatMessage(ChatMessage message) @@ -251,6 +252,18 @@ namespace Barotrauma.Networking } } + public static string ClientLogName(Client client, string name = null) + { + if (client == null) { return name; } + string retVal = "‖"; + if (client.Karma < 40.0f) + { + retVal += "color:#ff9900;"; + } + retVal += "metadata:" + (client.SteamID != 0 ? client.SteamID.ToString() : client.ID.ToString()) + "‖" + (name ?? client.Name).Replace("‖", "") + "‖end‖"; + return retVal; + } + public virtual void KickPlayer(string kickedName, string reason) { } public virtual void BanPlayer(string kickedName, string reason, bool range = false, TimeSpan? duration = null) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs index acee8540e..47233788a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs @@ -1,4 +1,6 @@ -namespace Barotrauma.Networking +using System; + +namespace Barotrauma.Networking { partial class OrderChatMessage : ChatMessage { @@ -13,26 +15,87 @@ //additional instructions (power up, fire at will, etc) public readonly string OrderOption; + public readonly int OrderPriority; + /// /// Used when the order targets a wall /// public int? WallSectionIndex { get; set; } - public OrderChatMessage(Order order, string orderOption, ISpatialEntity targetEntity, Character targetCharacter, Character sender) - : this(order, orderOption, + public OrderChatMessage(Order order, string orderOption, int priority, ISpatialEntity targetEntity, Character targetCharacter, Character sender) + : this(order, orderOption, priority, order?.GetChatMessage(targetCharacter?.Name, sender?.CurrentHull?.DisplayName, givingOrderToSelf: targetCharacter == sender, orderOption: orderOption), targetEntity, targetCharacter, sender) { } - public OrderChatMessage(Order order, string orderOption, string text, ISpatialEntity targetEntity, Character targetCharacter, Character sender) + public OrderChatMessage(Order order, string orderOption, int priority, string text, ISpatialEntity targetEntity, Character targetCharacter, Character sender) : base(sender?.Name, text, ChatMessageType.Order, sender, GameMain.NetworkMember.ConnectedClients.Find(c => c.Character == sender)) { Order = order; OrderOption = orderOption; + OrderPriority = priority; TargetCharacter = targetCharacter; TargetEntity = targetEntity; } + + private void WriteOrder(IWriteMessage msg) + { + msg.Write((byte)Order.PrefabList.IndexOf(Order.Prefab)); + msg.Write(TargetCharacter == null ? (UInt16)0 : TargetCharacter.ID); + msg.Write(TargetEntity is Entity ? (TargetEntity as Entity).ID : (UInt16)0); + + // The option of a Dismiss order is written differently so we know what order we target + // now that the game supports multiple current orders simultaneously + if (Order.Prefab.Identifier != "dismissed") + { + msg.Write((byte)Array.IndexOf(Order.Prefab.Options, OrderOption)); + } + else + { + if (!string.IsNullOrEmpty(OrderOption)) + { + msg.Write(true); + string[] dismissedOrder = OrderOption.Split('.'); + msg.Write((byte)dismissedOrder.Length); + if (dismissedOrder.Length > 0) + { + string dismissedOrderIdentifier = dismissedOrder[0]; + var orderPrefab = Order.GetPrefab(dismissedOrderIdentifier); + msg.Write((byte)Order.PrefabList.IndexOf(orderPrefab)); + if (dismissedOrder.Length > 1) + { + string dismissedOrderOption = dismissedOrder[1]; + msg.Write((byte)Array.IndexOf(orderPrefab.Options, dismissedOrderOption)); + } + } + } + else + { + // If the order option is not specified for a Dismiss order, + // we dismiss all current orders for the character + msg.Write(false); + } + } + + msg.Write((byte)OrderPriority); + msg.Write((byte)Order.TargetType); + if (Order.TargetType == Order.OrderTargetType.Position && TargetEntity is OrderTarget orderTarget) + { + msg.Write(true); + msg.Write(orderTarget.Position.X); + msg.Write(orderTarget.Position.Y); + msg.Write(orderTarget.Hull == null ? (UInt16)0 : orderTarget.Hull.ID); + } + else + { + msg.Write(false); + if (Order.TargetType == Order.OrderTargetType.WallSection) + { + msg.Write((byte)(WallSectionIndex ?? Order.WallSectionIndex ?? 0)); + } + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs index ddc1107a2..a84520c56 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs @@ -136,8 +136,7 @@ namespace Barotrauma.Networking EnsureBufferSize(ref buf, bitPos + 64); byte[] bytes = BitConverter.GetBytes(val); - WriteBytes(ref buf, ref bitPos, bytes, 0, bytes.Length); - bitPos += 64; + WriteBytes(ref buf, ref bitPos, bytes, 0, 8); } internal static void Write(ref byte[] buf, ref int bitPos, string val) { @@ -155,14 +154,14 @@ namespace Barotrauma.Networking internal static int WriteVariableUInt32(ref byte[] buf, ref int bitPos, uint value) { int retval = 1; - uint num1 = (uint)value; - while (num1 >= 0x80) + uint remainingValue = (uint)value; + while (remainingValue >= 0x80) { - Write(ref buf, ref bitPos, (byte)(num1 | 0x80)); - num1 = num1 >> 7; + Write(ref buf, ref bitPos, (byte)(remainingValue | 0x80)); + remainingValue = remainingValue >> 7; retval++; } - Write(ref buf, ref bitPos, (byte)num1); + Write(ref buf, ref bitPos, (byte)remainingValue); return retval; } @@ -304,19 +303,19 @@ namespace Barotrauma.Networking { int bitLength = buf.Length * 8; - int num1 = 0; - int num2 = 0; + int result = 0; + int shift = 0; while (bitLength - bitPos >= 8) { - byte num3 = ReadByte(buf, ref bitPos); - num1 |= (num3 & 0x7f) << num2; - num2 += 7; - if ((num3 & 0x80) == 0) - return (uint)num1; + byte chunk = ReadByte(buf, ref bitPos); + result |= (chunk & 0x7f) << shift; + shift += 7; + if ((chunk & 0x80) == 0) + return (uint)result; } // ouch; failed to find enough bytes; malformed variable length number? - return (uint)num1; + return (uint)result; } internal static String ReadString(byte[] buf, ref int bitPos) @@ -329,7 +328,7 @@ namespace Barotrauma.Networking if ((ulong)(bitLength - bitPos) < ((ulong)byteLen * 8)) { // not enough data - return null; + return null; } if ((bitPos & 7) == 0) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs index 4ddf29479..0c1569709 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs @@ -21,6 +21,12 @@ namespace Barotrauma.Networking protected set; } + public UInt64 OwnerSteamID + { + get; + protected set; + } + public string EndPointString { get; @@ -44,5 +50,15 @@ namespace Barotrauma.Networking //is received by the server. return false; } + + public bool SetOwnerSteamIDIfUnknown(UInt64 id) + { + //we know that for both Lidgren and SteamP2P, the + //owner id isn't known until the auth ticket is + //processed, so this method is the same for both + if (OwnerSteamID != 0) { return false; } + OwnerSteamID = id; + return true; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/SteamP2PConnection.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/SteamP2PConnection.cs index a65506196..7e70ad98b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/SteamP2PConnection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/SteamP2PConnection.cs @@ -10,6 +10,7 @@ namespace Barotrauma.Networking public SteamP2PConnection(string name, UInt64 steamId) { SteamID = steamId; + OwnerSteamID = 0; EndPointString = SteamManager.SteamIDUInt64ToString(SteamID); Name = name; Heartbeat(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs index 183c48e38..c5be46c81 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs @@ -24,7 +24,7 @@ namespace Barotrauma.Networking //items created during respawn //any respawn items left in the shuttle are removed when the shuttle despawns - private List respawnItems = new List(); + private readonly List respawnItems = new List(); public bool UsingShuttle { @@ -55,6 +55,14 @@ namespace Barotrauma.Networking public State CurrentState { get; private set; } + public bool UseRespawnPrompt + { + get + { + return GameMain.GameSession?.GameMode is CampaignMode && Level.Loaded != null && Level.Loaded?.Type != LevelData.LevelType.Outpost; + } + } + private float maxTransportTime; private float updateReturnTimer; @@ -70,6 +78,8 @@ namespace Barotrauma.Networking { RespawnShuttle = new Submarine(shuttleInfo, true); RespawnShuttle.PhysicsBody.FarseerBody.OnCollision += OnShuttleCollision; + //set crush depth slightly deeper than the main sub's + RespawnShuttle.RealWorldCrushDepth = Math.Max(RespawnShuttle.RealWorldCrushDepth, Submarine.MainSub.RealWorldCrushDepth * 1.2f); //prevent wifi components from communicating between the respawn shuttle and other subs List wifiComponents = new List(); @@ -293,10 +303,10 @@ namespace Barotrauma.Networking RespawnShuttle.Velocity = Vector2.Zero; } - partial void RespawnCharactersProjSpecific(); - public void RespawnCharacters() + partial void RespawnCharactersProjSpecific(Vector2? shuttlePos); + public void RespawnCharacters(Vector2? shuttlePos) { - RespawnCharactersProjSpecific(); + RespawnCharactersProjSpecific(shuttlePos); } public Vector2 FindSpawnPos() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index 8196a5495..79f9df744 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -628,6 +628,13 @@ namespace Barotrauma.Networking set; } + [Serialize(false, true)] + public bool LockAllDefaultWires + { + get; + set; + } + [Serialize(true, true)] public bool AllowFriendlyFire { @@ -872,6 +879,13 @@ namespace Barotrauma.Networking private set; } + [Serialize(true, true)] + public bool RadiationEnabled + { + get; + set; + } + public void SetPassword(string password) { if (string.IsNullOrEmpty(password)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs index 5da1564b8..5e236219e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs @@ -139,7 +139,21 @@ namespace Barotrauma.Steam } return success; } - + + public static bool StoreStats() + { + if (!isInitialized || !Steamworks.SteamClient.IsValid) { return false; } + DebugConsole.Log("Storing Steam stats..."); + bool success = Steamworks.SteamUserStats.StoreStats(); + if (!success) + { +#if DEBUG + DebugConsole.NewMessage("Failed to store Steam stats."); +#endif + } + return success; + } + public static void Update(float deltaTime) { if (!isInitialized) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voip/VoipConfig.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voip/VoipConfig.cs index 0c76824fe..ab92eb553 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voip/VoipConfig.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voip/VoipConfig.cs @@ -6,8 +6,8 @@ namespace Barotrauma.Networking { static partial class VoipConfig { - public const int MAX_COMPRESSED_SIZE = 120; //amount of bytes we expect each 60ms of audio to fit in + public const int MAX_COMPRESSED_SIZE = 40; //amount of bytes we expect each 20ms of audio to fit in - public static readonly TimeSpan SEND_INTERVAL = new TimeSpan(0,0,0,0,120); + public static readonly TimeSpan SEND_INTERVAL = new TimeSpan(0,0,0,0,20); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs index aeb5d17ed..d09effa0e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs @@ -713,7 +713,7 @@ namespace Barotrauma public void SetPrevTransform(Vector2 simPosition, float rotation) { -#if DEBUG || UNSTABLE +#if DEBUG if (!IsValidValue(simPosition, "position", -1e10f, 1e10f)) { return; } if (!IsValidValue(rotation, "rotation")) { return; } #endif @@ -756,12 +756,12 @@ namespace Barotrauma Vector2 vel = FarseerBody.LinearVelocity; Vector2 deltaPos = simPosition - (Vector2)pullPos; +#if DEBUG if (deltaPos.LengthSquared() > 100.0f * 100.0f) { -#if DEBUG || UNSTABLE DebugConsole.ThrowError("Attempted to move a physics body to an invalid position.\n" + Environment.StackTrace.CleanupStackTrace()); -#endif } +#endif deltaPos *= force; ApplyLinearImpulse((deltaPos - vel * 0.5f) * FarseerBody.Mass, (Vector2)pullPos); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IPrefab.cs index fb3e5f630..d2b2e9d27 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IPrefab.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Security.Cryptography; using System.Text; namespace Barotrauma @@ -11,4 +12,28 @@ namespace Barotrauma string FilePath { get; } ContentPackage ContentPackage { get; } } + + public interface IHasUintIdentifier + { + uint UIntIdentifier { get; set; } + } + + public static class PrefabExtensions + { + public static void CalculatePrefabUIntIdentifier(this T prefab, PrefabCollection prefabs) where T : class, IPrefab, IHasUintIdentifier, IDisposable + { + using (MD5 md5 = MD5.Create()) + { + prefab.UIntIdentifier = ToolBox.StringToUInt32Hash(prefab.Identifier, md5); + + //it's theoretically possible for two different values to generate the same hash, but the probability is astronomically small + var collision = prefabs.Find(p => p != prefab && p.UIntIdentifier == prefab.UIntIdentifier); + if (collision != null) + { + DebugConsole.ThrowError($"Hashing collision when generating uint identifiers for {typeof(T).Name}: {prefab.Identifier} has the same identifier as {collision.Identifier} ({prefab.UIntIdentifier})"); + collision.UIntIdentifier++; + } + } + } + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs index 62839c061..86eababf0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs @@ -93,7 +93,7 @@ namespace Barotrauma //Handle bad overrides and duplicates if (basePrefabExists && !isOverride) { - DebugConsole.ThrowError($"Error registering \"{prefab.OriginalName}\", \"{prefab.Identifier}\" ({typeof(T).ToString()}): base already exists; try overriding"); + DebugConsole.ThrowError($"Error registering \"{prefab.OriginalName}\", \"{prefab.Identifier}\" ({typeof(T).ToString()}): base already exists; try overriding\n{Environment.StackTrace}"); return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ProcGen/Voronoi.cs b/Barotrauma/BarotraumaShared/SharedSource/ProcGen/Voronoi.cs index 46b5e5c10..4442823aa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ProcGen/Voronoi.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ProcGen/Voronoi.cs @@ -987,5 +987,28 @@ namespace Voronoi2 return generateVoronoi(xVal, yVal, 0, width, 0, height); } + public List MakeVoronoiGraph(double[] xVal, double[] yVal, Rectangle area) + { + for (int i = 0; i sites = new HashSet(); + foreach (var graphEdge in graphEdges) + { + graphEdge.Point1 += area.Location.ToVector2(); + graphEdge.Point2 += area.Location.ToVector2(); + sites.Add(graphEdge.Site1); + sites.Add(graphEdge.Site2); + } + foreach (Site site in sites) + { + site.Coord = new DoubleVector2(site.Coord.X + area.Location.X, site.Coord.Y + area.Location.Y); + } + return graphEdges; + } + } // Voronoi Class End } // namespace Voronoi2 End \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs index def402528..f506479fd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework; using System.Threading; using FarseerPhysics.Dynamics; #if DEBUG && CLIENT +using System; using Microsoft.Xna.Framework.Input; #endif @@ -115,6 +116,37 @@ namespace Barotrauma } } } + +#if LINUX + // disgusting + if (PlayerInput.KeyDown(Keys.RightShift) && Character.Controlled is { CharacterHealth: { } health } && PlayerInput.MouseSpeed != Vector2.Zero) + { + AfflictionPrefab radiationPrefab = AfflictionPrefab.RadiationSickness; + float afflictionAmount = (PlayerInput.MousePosition.X / GameMain.GraphicsWidth) * radiationPrefab.MaxStrength; + Affliction affliction = health.GetAffliction(radiationPrefab.Identifier, true); + + if (affliction == null) + { + health.ApplyAffliction(null, new Affliction(radiationPrefab, Math.Abs(afflictionAmount))); + } + else + { + float diff = affliction.Strength - afflictionAmount; + + if (!MathUtils.NearlyEqual(diff, 0)) + { + if (diff > 0) + { + health.ReduceAffliction(null, radiationPrefab.Identifier, Math.Abs(diff)); + } + else if (diff < 0) + { + health.ApplyAffliction(null, new Affliction(radiationPrefab, Math.Abs(diff))); + } + } + } + } +#endif #endif #if CLIENT @@ -192,7 +224,7 @@ namespace Barotrauma if (Character.Controlled != null && Lights.LightManager.ViewTarget != null) { - Vector2 targetPos = Lights.LightManager.ViewTarget.DrawPosition; + Vector2 targetPos = Lights.LightManager.ViewTarget.WorldPosition; if (Lights.LightManager.ViewTarget == Character.Controlled && (CharacterHealth.OpenHealthWindow != null || CrewManager.IsCommandInterfaceOpen || ConversationAction.IsDialogOpen)) { @@ -212,7 +244,7 @@ namespace Barotrauma cam.TargetPos = targetPos; } - cam.MoveCamera((float)deltaTime); + cam.MoveCamera((float)deltaTime, allowZoom: GUI.MouseOn == null); #endif foreach (Submarine sub in Submarine.Loaded) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs index 4d5cfa806..e7c3e5da5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs @@ -36,6 +36,23 @@ namespace Barotrauma levelDifficultyScrollBar.OnMoved(levelDifficultyScrollBar, levelDifficultyScrollBar.BarScroll); #endif } + + public void SetRadiationEnabled(bool enabled) + { +#if CLIENT + if (radiationEnabledTickBox == null) { return; } + radiationEnabledTickBox.Selected = enabled; +#endif + } + + public bool IsRadiationEnabled() + { +#if CLIENT + return radiationEnabledTickBox != null && radiationEnabledTickBox.Selected; +#elif SERVER + return GameMain.Server.ServerSettings.RadiationEnabled; +#endif + } public void ToggleTraitorsEnabled(int dir) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/Screen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/Screen.cs index 3cfa66057..8ddb1ca8c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/Screen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/Screen.cs @@ -24,6 +24,7 @@ { selected.Deselect(); #if CLIENT + GUIContextMenu.CurrentContextMenu = null; GUI.ClearCursorWait(); //make sure any textbox in the previously selected screen doesn't stay selected if (GUI.KeyboardDispatcher.Subscriber != DebugConsole.TextBox) @@ -31,6 +32,13 @@ GUI.KeyboardDispatcher.Subscriber = null; GUI.ScreenChanged = true; } + SubmarinePreview.Close(); + + // Make sure the saving indicator is disabled when returning to main menu or lobby + if (this == GameMain.MainMenuScreen || this == GameMain.NetLobbyScreen) + { + GUI.DisableSavingIndicatorDelayed(); + } #endif } selected = this; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index 0a21bcdce..fb24f1a63 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -570,6 +570,25 @@ namespace Barotrauma public static Color ParseColor(string stringColor, bool errorMessages = true) { + if (stringColor.StartsWith("gui.", StringComparison.OrdinalIgnoreCase)) + { +#if CLIENT + if (GUI.Style != null) + { + string colorName = stringColor.Substring(4); + var property = GUI.Style.GetType().GetProperties().FirstOrDefault( + p => p.PropertyType == typeof(Color) && + p.Name.Equals(colorName, StringComparison.OrdinalIgnoreCase)); + if (property != null) + { + return (Color)property?.GetValue(GUI.Style); + } + } +#endif + return Color.White; + } + + string[] strComponents = stringColor.Split(','); Color color = Color.White; diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 4ba9b0e7a..52a1e2aa7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -39,6 +39,66 @@ namespace Barotrauma } } + class AITrigger : ISerializableEntity + { + public string Name => "ai trigger"; + + public Dictionary SerializableProperties { get; set; } + + [Serialize(AIState.Idle, false)] + public AIState State { get; private set; } + + [Serialize(0f, false)] + public float Duration { get; private set; } + + [Serialize(1f, false)] + public float Probability { get; private set; } + + [Serialize(0f, false)] + public float MinDamage { get; private set; } + + [Serialize(true, false)] + public bool AllowToOverride { get; private set; } + + [Serialize(true, false)] + public bool AllowToBeOverridden { get; private set; } + + public bool IsTriggered { get; private set; } + + public float Timer { get; private set; } = -1; + + public bool IsActive { get; private set; } + + public void Launch() + { + IsTriggered = true; + IsActive = true; + Timer = Duration; + } + + public void Reset() + { + IsTriggered = false; + IsActive = false; + Timer = 0; + } + + public void UpdateTimer(float deltaTime) + { + Timer -= deltaTime; + if (Timer < 0) + { + Timer = 0; + IsActive = false; + } + } + + public AITrigger(XElement element) + { + SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + } + } + partial class StatusEffect { [Flags] @@ -53,7 +113,8 @@ namespace Barotrauma UseTarget = 64, Hull = 128, Limb = 256, - AllLimbs = 512 + AllLimbs = 512, + LastLimb = 1024 } class ItemSpawnInfo @@ -198,10 +259,11 @@ namespace Barotrauma public readonly ActionType type = ActionType.OnActive; - private readonly List explosions; + public readonly List Explosions; private readonly List spawnItems; private readonly List spawnCharacters; + private readonly List aiTriggers; private readonly List triggeredEvents; private readonly string triggeredEventTargetTag = "statuseffecttarget", @@ -219,12 +281,16 @@ namespace Barotrauma public readonly bool OnlyInside; public readonly bool OnlyOutside; + // Currently only used for OnDamaged. TODO: is there a better, more generic way to do this? + public readonly bool OnlyPlayerTriggered; public HashSet TargetIdentifiers { get { return targetIdentifiers; } } + public HashSet AllowedAfflictions { get; private set; } + public List Afflictions { get; @@ -236,7 +302,9 @@ namespace Barotrauma get { return spawnCharacters; } } - private readonly List> reduceAffliction; + public readonly List> ReduceAffliction; + + public float Duration => duration; //only applicable if targeting NearbyCharacters or NearbyItems public float Range @@ -279,13 +347,15 @@ namespace Barotrauma requiredItems = new List(); spawnItems = new List(); spawnCharacters = new List(); + aiTriggers = new List(); Afflictions = new List(); - explosions = new List(); + Explosions = new List(); triggeredEvents = new List(); - reduceAffliction = new List>(); + ReduceAffliction = new List>(); tags = new HashSet(element.GetAttributeString("tags", "").Split(',')); OnlyInside = element.GetAttributeBool("onlyinside", false); OnlyOutside = element.GetAttributeBool("onlyoutside", false); + OnlyPlayerTriggered = element.GetAttributeBool("onlyplayertriggered", false); Range = element.GetAttributeFloat("range", 0.0f); Offset = element.GetAttributeVector2("offset", Vector2.Zero); @@ -295,7 +365,7 @@ namespace Barotrauma List targetLimbs = new List(); foreach (string targetLimbName in targetLimbNames) { - if (Enum.TryParse(targetLimbName, out LimbType targetLimb)) { targetLimbs.Add(targetLimb); } + if (Enum.TryParse(targetLimbName, ignoreCase: true, out LimbType targetLimb)) { targetLimbs.Add(targetLimb); } } if (targetLimbs.Count > 0) { this.targetLimbs = targetLimbs.ToArray(); } } @@ -353,6 +423,14 @@ namespace Barotrauma targetIdentifiers.Add(identifiers[i].Trim().ToLowerInvariant()); } break; + case "allowedafflictions": + string[] types = attribute.Value.Split(','); + AllowedAfflictions = new HashSet(); + for (int i = 0; i < types.Length; i++) + { + AllowedAfflictions.Add(types[i].Trim().ToLowerInvariant()); + } + break; case "duration": duration = attribute.GetAttributeFloat(0.0f); break; @@ -419,7 +497,7 @@ namespace Barotrauma switch (subElement.Name.ToString().ToLowerInvariant()) { case "explosion": - explosions.Add(new Explosion(subElement, parentDebugName)); + Explosions.Add(new Explosion(subElement, parentDebugName)); break; case "fire": FireSize = subElement.GetAttributeFloat("size", 10.0f); @@ -494,7 +572,7 @@ namespace Barotrauma if (subElement.Attribute("name") != null) { DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - define afflictions using identifiers or types instead of names."); - reduceAffliction.Add(new Pair( + ReduceAffliction.Add(new Pair( subElement.GetAttributeString("name", "").ToLowerInvariant(), subElement.GetAttributeFloat(1.0f, "amount", "strength", "reduceamount"))); } @@ -505,7 +583,7 @@ namespace Barotrauma if (AfflictionPrefab.List.Any(ap => ap.Identifier == name || ap.AfflictionType == name)) { - reduceAffliction.Add(new Pair( + ReduceAffliction.Add(new Pair( name, subElement.GetAttributeFloat(1.0f, "amount", "strength", "reduceamount"))); } @@ -529,7 +607,6 @@ namespace Barotrauma triggeredEvents.Add(prefab); } } - foreach (XElement eventElement in subElement.Elements()) { if (!eventElement.Name.ToString().Equals("ScriptedEvent", StringComparison.OrdinalIgnoreCase)) { continue; } @@ -540,6 +617,9 @@ namespace Barotrauma var newSpawnCharacter = new CharacterSpawnInfo(subElement, parentDebugName); if (!string.IsNullOrWhiteSpace(newSpawnCharacter.SpeciesName)) { spawnCharacters.Add(newSpawnCharacter); } break; + case "aitrigger": + aiTriggers.Add(new AITrigger(subElement)); + break; } } InitProjSpecific(element, parentDebugName); @@ -977,7 +1057,7 @@ namespace Barotrauma } } - foreach (Explosion explosion in explosions) + foreach (Explosion explosion in Explosions) { explosion.Explode(position, damageSource: entity, attacker: user); } @@ -990,12 +1070,11 @@ namespace Barotrauma foreach (Affliction affliction in Afflictions) { if (Rand.Value(Rand.RandSync.Unsynced) > affliction.Probability) { continue; } - Affliction multipliedAffliction = affliction; - if (!disableDeltaTime) + Affliction newAffliction = affliction; + if (!disableDeltaTime && !setValue) { - multipliedAffliction = affliction.CreateMultiplied(deltaTime); + newAffliction = affliction.CreateMultiplied(deltaTime); } - if (target is Character character) { if (character.Removed) { continue; } @@ -1005,7 +1084,7 @@ namespace Barotrauma if (limb.Removed) { continue; } if (limb.IsSevered) { continue; } if (targetLimbs != null && !targetLimbs.Contains(limb.type)) { continue; } - AttackResult result = limb.character.DamageLimb(position, limb, multipliedAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source); + AttackResult result = limb.character.DamageLimb(position, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source, allowStacking: !setValue); limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability, disableDeltaTime ? result.Damage : result.Damage / deltaTime, allowBeheading: true); //only apply non-limb-specific afflictions to the first limb if (!affliction.Prefab.LimbSpecific) { break; } @@ -1015,21 +1094,21 @@ namespace Barotrauma { if (limb.IsSevered) { continue; } if (limb.character.Removed || limb.Removed) { continue; } - AttackResult result = limb.character.DamageLimb(position, limb, multipliedAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source); + AttackResult result = limb.character.DamageLimb(position, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source, allowStacking: !setValue); limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability, disableDeltaTime ? result.Damage : result.Damage / deltaTime, allowBeheading: true); } } - foreach (Pair reduceAffliction in reduceAffliction) + foreach (Pair reduceAffliction in ReduceAffliction) { - float reduceAmount = disableDeltaTime ? reduceAffliction.Second : reduceAffliction.Second * deltaTime; + float reduceAmount = disableDeltaTime || setValue ? reduceAffliction.Second : reduceAffliction.Second * deltaTime; Limb targetLimb = null; Character targetCharacter = null; if (target is Character character) { targetCharacter = character; } - else if (target is Limb limb) + else if (target is Limb limb && !limb.Removed) { targetLimb = limb; targetCharacter = limb.character; @@ -1050,6 +1129,32 @@ namespace Barotrauma #endif } } + + if (aiTriggers.Any()) + { + Character targetCharacter = target as Character; + if (targetCharacter == null) + { + if (target is Limb targetLimb && !targetLimb.Removed) + { + targetCharacter = targetLimb.character; + } + } + if (targetCharacter != null && !targetCharacter.Removed && !targetCharacter.IsPlayer) + { + if (targetCharacter.AIController is EnemyAIController enemyAI) + { + foreach (AITrigger trigger in aiTriggers) + { + if (Rand.Value(Rand.RandSync.Unsynced) > trigger.Probability) { continue; } + if (target is Limb targetLimb && targetCharacter.LastDamage.HitLimb != targetLimb) { continue; } + if (targetCharacter.LastDamage.Damage < trigger.MinDamage) { continue; } + enemyAI.LaunchTrigger(trigger); + break; + } + } + } + } } if (FireSize > 0.0f && entity != null) @@ -1290,7 +1395,7 @@ namespace Barotrauma foreach (Affliction affliction in element.Parent.Afflictions) { Affliction multipliedAffliction = affliction; - if (!element.Parent.disableDeltaTime) { multipliedAffliction = affliction.CreateMultiplied(deltaTime); } + if (!element.Parent.disableDeltaTime && !element.Parent.setValue) { multipliedAffliction = affliction.CreateMultiplied(deltaTime); } if (target is Character character) { @@ -1304,7 +1409,7 @@ namespace Barotrauma } } - foreach (Pair reduceAffliction in element.Parent.reduceAffliction) + foreach (Pair reduceAffliction in element.Parent.ReduceAffliction) { Limb targetLimb = null; Character targetCharacter = null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs index 7622342b7..c0c645e9b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs @@ -307,7 +307,7 @@ namespace Barotrauma public static void OnRoundEnded(GameSession gameSession) { //made it to the destination - if (gameSession?.Submarine != null && Level.Loaded != null && gameSession.Submarine.AtEndPosition) + if (gameSession?.Submarine != null && Level.Loaded != null && gameSession.Submarine.AtEndExit) { float levelLengthMeters = Physics.DisplayToRealWorldRatio * Level.Loaded.Size.X; float levelLengthKilometers = levelLengthMeters / 1000.0f; @@ -331,32 +331,35 @@ namespace Barotrauma } } + //make sure changed stats (kill count, kms traveled) get stored + SteamManager.StoreStats(); + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } - if (gameSession.Mission != null) + foreach (Mission mission in gameSession.Missions) { - if (gameSession.Mission is CombatMission combatMission && GameMain.GameSession.WinningTeam.HasValue) + if (mission is CombatMission combatMission && GameMain.GameSession.WinningTeam.HasValue) { //all characters that are alive and in the winning team get an achievement - UnlockAchievement(gameSession.Mission.Prefab.AchievementIdentifier + (int)GameMain.GameSession.WinningTeam, true, + UnlockAchievement(mission.Prefab.AchievementIdentifier + (int)GameMain.GameSession.WinningTeam, true, c => c != null && !c.IsDead && !c.IsUnconscious && combatMission.IsInWinningTeam(c)); } - else if (gameSession.Mission.Completed) + else if (mission.Completed) { //all characters get an achievement if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { - UnlockAchievement(gameSession.Mission.Prefab.AchievementIdentifier, true, c => c != null); + UnlockAchievement(mission.Prefab.AchievementIdentifier, true, c => c != null); } else { - UnlockAchievement(gameSession.Mission.Prefab.AchievementIdentifier); + UnlockAchievement(mission.Prefab.AchievementIdentifier); } } } //made it to the destination - if (gameSession.Submarine.AtEndPosition) + if (gameSession.Submarine.AtEndExit) { bool noDamageRun = !roundData.SubWasDamaged && !roundData.Casualties.Any(c => !(c.AIController is EnemyAIController)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs b/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs index b9ae5a165..c7aa407ea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs @@ -4,6 +4,7 @@ using Barotrauma.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; +using Barotrauma.Extensions; namespace Barotrauma { @@ -202,6 +203,8 @@ namespace Barotrauma return false; } + private static readonly List availableTexts = new List(); + public static string Get(string textTag, bool returnNull = false, string fallBackTag = null, bool useEnglishAsFallBack = true) { lock (mutex) @@ -228,11 +231,19 @@ namespace Barotrauma return textTag; } #endif - + availableTexts.Clear(); foreach (TextPack textPack in textPacks[Language]) { - string text = textPack.Get(textTag); - if (text != null) { return text; } + var texts = textPack.GetAll(textTag); + if (texts != null) + { + availableTexts.AddRange(texts); + } + } + + if (availableTexts.Any()) + { + return availableTexts.GetRandom().Replace(@"\n", "\n"); } if (!string.IsNullOrEmpty(fallBackTag)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/TextPack.cs b/Barotrauma/BarotraumaShared/SharedSource/TextPack.cs index 06526e2ab..395e5ffa4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/TextPack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/TextPack.cs @@ -87,6 +87,8 @@ namespace Barotrauma public List GetAll(string textTag) { + if (textTag is null) { return null; } + if (!texts.TryGetValue(textTag.ToLowerInvariant(), out List textList) || !textList.Any()) { return null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/MTRandom.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/MTRandom.cs index 8da34cff0..9b634f90a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/MTRandom.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/MTRandom.cs @@ -84,6 +84,22 @@ namespace Barotrauma return retval; } + public override int Next(int minValue, int maxValue) + { + int range = maxValue - minValue; + return minValue + Next(range); + } + + public override void NextBytes(byte[] buffer) + { + throw new NotImplementedException(); + } + + public override void NextBytes(Span buffer) + { + throw new NotImplementedException(); + } + /// /// Returns a random value is greater or equal than 0 and less than maxValue /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs index 47c0cb08b..299e6d6dd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using Barotrauma.Extensions; +using System.Linq; namespace Barotrauma { @@ -723,7 +724,7 @@ namespace Barotrauma return wrappedPoints; } - public static List GenerateJaggedLine(Vector2 start, Vector2 end, int iterations, float offsetAmount) + public static List GenerateJaggedLine(Vector2 start, Vector2 end, int iterations, float offsetAmount, Rectangle? bounds = null) { List segments = new List { @@ -745,6 +746,26 @@ namespace Barotrauma normal = new Vector2(-normal.Y, normal.X); midPoint += normal * Rand.Range(-offsetAmount, offsetAmount, Rand.RandSync.Server); + if (bounds.HasValue) + { + if (midPoint.X < bounds.Value.X) + { + midPoint.X = bounds.Value.X + (bounds.Value.X - midPoint.X); + } + else if (midPoint.X > bounds.Value.Right) + { + midPoint.X = bounds.Value.Right - (midPoint.X - bounds.Value.Right); + } + if (midPoint.Y < bounds.Value.Y) + { + midPoint.Y = bounds.Value.Y + (bounds.Value.Y - midPoint.Y); + } + else if (midPoint.Y > bounds.Value.Bottom) + { + midPoint.Y = bounds.Value.Bottom - (midPoint.Y - bounds.Value.Bottom); + } + } + segments.Insert(i, new Vector2[] { startSegment, midPoint }); segments.Insert(i + 1, new Vector2[] { midPoint, endSegment }); @@ -900,6 +921,8 @@ namespace Barotrauma return (float)Math.Pow(f, p); } + public static float Pow2(float f) => f * f; + /// /// Converts the alignment to a vector where -1,-1 is the top-left corner, 0,0 the center and 1,1 bottom-right /// @@ -930,12 +953,10 @@ namespace Barotrauma /// Modified from: /// http://www.gamefromscratch.com/post/2012/11/24/GameDev-math-recipes-Rotating-one-point-around-another-point.aspx /// - public static Vector2 RotatePointAroundTarget(Vector2 point, Vector2 target, float degrees, bool clockWise = true) + public static Vector2 RotatePointAroundTarget(Vector2 point, Vector2 target, float radians, bool clockWise = true) { - // (Math.PI / 180) * degrees - var angle = MathHelper.ToRadians(degrees); - var sin = Math.Sin(angle); - var cos = Math.Cos(angle); + var sin = Math.Sin(radians); + var cos = Math.Cos(radians); if (!clockWise) { sin = -sin; @@ -1046,6 +1067,16 @@ namespace Barotrauma if (diff == 0) { return v >= max ? 1f : 0f; } return MathHelper.Clamp((v - min) / diff, 0f, 1f); } + + public static float Min(params float[] vals) + { + return vals.Min(); + } + + public static float Max(params float[] vals) + { + return vals.Max(); + } } class CompareCCW : IComparer diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/RichTextData.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/RichTextData.cs index 07064ae46..fd7cb014d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/RichTextData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/RichTextData.cs @@ -9,6 +9,8 @@ namespace Barotrauma public Color? Color; public string Metadata; + public float Alpha = 1.0f; + private const char definitionIndicator = '‖'; private const char attributeSeparator = ';'; private const char keyValueSeparator = ':'; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs index fb5ae54bc..201c9d7e9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs @@ -7,7 +7,10 @@ namespace Barotrauma.IO { static readonly string[] unwritableDirs = new string[] { "Content", "Data/ContentPackages" }; - public static bool DevException; + /// + /// When set to true, the game is allowed to modify the vanilla content in debug builds. Has no effect in non-debug builds. + /// + public static bool SkipValidationInDebugBuilds; public static bool CanWrite(string path) { @@ -20,7 +23,7 @@ namespace Barotrauma.IO if (path.StartsWith(dir, StringComparison.InvariantCultureIgnoreCase)) { #if DEBUG - return DevException; + return SkipValidationInDebugBuilds; #else return false; #endif @@ -37,7 +40,7 @@ namespace Barotrauma.IO { if (!Validation.CanWrite(path)) { - DebugConsole.ThrowError($"Cannot save XML document to \"{path}\": failed validation"); + DebugConsole.ThrowError($"Cannot save XML document to \"{path}\": modifying the files in the folder is not allowed."); return; } doc.Save(path); @@ -47,7 +50,7 @@ namespace Barotrauma.IO { if (!Validation.CanWrite(path)) { - DebugConsole.ThrowError($"Cannot save XML element to \"{path}\": failed validation"); + DebugConsole.ThrowError($"Cannot save XML element to \"{path}\": modifying the files in the folder is not allowed."); return; } element.Save(path); @@ -72,7 +75,7 @@ namespace Barotrauma.IO { if (!Validation.CanWrite(path)) { - DebugConsole.ThrowError($"Cannot write XML document to \"{path}\": failed validation"); + DebugConsole.ThrowError($"Cannot write XML document to \"{path}\": modifying the files in the folder is not allowed."); Writer = null; return; } @@ -222,7 +225,7 @@ namespace Barotrauma.IO { if (!Validation.CanWrite(path)) { - DebugConsole.ThrowError($"Cannot create directory \"{path}\": failed validation"); + DebugConsole.ThrowError($"Cannot create directory \"{path}\": modifying the contents of the folder is not allowed."); return null; } return System.IO.Directory.CreateDirectory(path); @@ -232,7 +235,7 @@ namespace Barotrauma.IO { if (!Validation.CanWrite(path)) { - DebugConsole.ThrowError($"Cannot delete directory \"{path}\": failed validation"); + DebugConsole.ThrowError($"Cannot delete directory \"{path}\": modifying the contents of the folder is not allowed."); return; } //TODO: validate recursion? @@ -251,7 +254,7 @@ namespace Barotrauma.IO { if (!Validation.CanWrite(dest)) { - DebugConsole.ThrowError($"Cannot copy \"{src}\" to \"{dest}\": failed validation"); + DebugConsole.ThrowError($"Cannot copy \"{src}\" to \"{dest}\": modifying the contents of the folder is not allowed."); return; } System.IO.File.Copy(src, dest, overwrite); @@ -261,12 +264,12 @@ namespace Barotrauma.IO { if (!Validation.CanWrite(src)) { - DebugConsole.ThrowError($"Cannot move \"{src}\" to \"{dest}\": src failed validation"); + DebugConsole.ThrowError($"Cannot move \"{src}\" to \"{dest}\": modifying the contents of the source folder is not allowed."); return; } if (!Validation.CanWrite(dest)) { - DebugConsole.ThrowError($"Cannot move \"{src}\" to \"{dest}\": dest failed validation"); + DebugConsole.ThrowError($"Cannot move \"{src}\" to \"{dest}\": modifying the contents of the destination folder is not allowed"); return; } System.IO.File.Move(src, dest); @@ -276,7 +279,7 @@ namespace Barotrauma.IO { if (!Validation.CanWrite(path)) { - DebugConsole.ThrowError($"Cannot delete file \"{path}\": failed validation"); + DebugConsole.ThrowError($"Cannot delete file \"{path}\": modifying the contents of the folder is not allowed."); return; } System.IO.File.Delete(path); @@ -298,7 +301,7 @@ namespace Barotrauma.IO case System.IO.FileMode.Truncate: if (!Validation.CanWrite(path)) { - DebugConsole.ThrowError($"Cannot open \"{path}\" in {mode} mode: failed validation"); + DebugConsole.ThrowError($"Cannot open \"{path}\" in {mode} mode: modifying the contents of the folder is not allowed."); return null; } break; @@ -328,7 +331,7 @@ namespace Barotrauma.IO { if (!Validation.CanWrite(path)) { - DebugConsole.ThrowError($"Cannot write all bytes to \"{path}\": failed validation"); + DebugConsole.ThrowError($"Cannot write all bytes to \"{path}\": modifying the files in the folder is not allowed."); return; } System.IO.File.WriteAllBytes(path, contents); @@ -338,7 +341,7 @@ namespace Barotrauma.IO { if (!Validation.CanWrite(path)) { - DebugConsole.ThrowError($"Cannot write all text to \"{path}\": failed validation"); + DebugConsole.ThrowError($"Cannot write all text to \"{path}\": modifying the files in the folder is not allowed."); return; } System.IO.File.WriteAllText(path, contents, encoding ?? System.Text.Encoding.UTF8); @@ -348,7 +351,7 @@ namespace Barotrauma.IO { if (!Validation.CanWrite(path)) { - DebugConsole.ThrowError($"Cannot write all lines to \"{path}\": failed validation"); + DebugConsole.ThrowError($"Cannot write all lines to \"{path}\": modifying the files in the folder is not allowed."); return; } System.IO.File.WriteAllLines(path, contents, encoding ?? System.Text.Encoding.UTF8); @@ -420,7 +423,7 @@ namespace Barotrauma.IO } else { - DebugConsole.ThrowError($"Cannot write to file \"{fileName}\": failed validation"); + DebugConsole.ThrowError($"Cannot write to file \"{fileName}\": modifying the files in the folder is not allowed."); } } @@ -487,7 +490,7 @@ namespace Barotrauma.IO { if (!Validation.CanWrite(innerInfo.FullName)) { - DebugConsole.ThrowError($"Cannot delete directory \"{Name}\": failed validation"); + DebugConsole.ThrowError($"Cannot delete directory \"{Name}\": modifying the contents of the folder is not allowed."); return; } innerInfo.Delete(); @@ -523,7 +526,7 @@ namespace Barotrauma.IO { if (!Validation.CanWrite(innerInfo.FullName)) { - DebugConsole.ThrowError($"Cannot set read-only to {value} for \"{Name}\": failed validation"); + DebugConsole.ThrowError($"Cannot set read-only to {value} for \"{Name}\": modifying the files in the folder is not allowed."); return; } innerInfo.IsReadOnly = value; @@ -534,7 +537,7 @@ namespace Barotrauma.IO { if (!Validation.CanWrite(dest)) { - DebugConsole.ThrowError($"Cannot copy \"{Name}\" to \"{dest}\": failed validation"); + DebugConsole.ThrowError($"Cannot copy \"{Name}\" to \"{dest}\": modifying the contents of the destination folder is not allowed."); return; } innerInfo.CopyTo(dest, overwriteExisting); @@ -544,7 +547,7 @@ namespace Barotrauma.IO { if (!Validation.CanWrite(innerInfo.FullName)) { - DebugConsole.ThrowError($"Cannot delete file \"{Name}\": failed validation"); + DebugConsole.ThrowError($"Cannot delete file \"{Name}\": modifying the files in the folder is not allowed."); return; } innerInfo.Delete(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs index 7f23fec99..1d68cccc5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -224,6 +224,38 @@ namespace Barotrauma return inputType; } + /// + /// Convert a HSV value into a RGB value. + /// + /// Value between 0 and 360 + /// Value between 0 and 1 + /// Value between 0 and 1 + /// Reference + /// + public static Color HSVToRGB(float hue, float saturation, float value) + { + float c = value * saturation; + + float h = Math.Clamp(hue, 0, 360) / 60f; + + float x = c * (1 - Math.Abs(h % 2 - 1)); + + float r = 0, + g = 0, + b = 0; + + if (0 <= h && h <= 1) { r = c; g = x; b = 0; } + else if (1 < h && h <= 2) { r = x; g = c; b = 0; } + else if (2 < h && h <= 3) { r = 0; g = c; b = x; } + else if (3 < h && h <= 4) { r = 0; g = x; b = c; } + else if (4 < h && h <= 5) { r = x; g = 0; b = c; } + else if (5 < h && h <= 6) { r = c; g = 0; b = x; } + + float m = value - c; + + return new Color(r + m, g + m, b + m); + } + /// /// Returns either a green [x] or a red [o] /// @@ -449,6 +481,7 @@ namespace Barotrauma return key; } + /// /// Returns a new instance of the class with all properties and fields copied. /// @@ -490,6 +523,30 @@ namespace Barotrauma return destination; } + public static void SiftElement(this List list, int from, int to) + { + if (from < 0 || from >= list.Count) { throw new ArgumentException($"from parameter out of range (from={from}, range=[0..{list.Count - 1}])"); } + if (to < 0 || to >= list.Count) { throw new ArgumentException($"to parameter out of range (to={to}, range=[0..{list.Count - 1}])"); } + + T elem = list[from]; + if (from > to) + { + for (int i = from; i > to; i--) + { + list[i] = list[i - 1]; + } + list[to] = elem; + } + else if (from < to) + { + for (int i = from; i < to; i++) + { + list[i] = list[i + 1]; + } + list[to] = elem; + } + } + public static string ByteArrayToString(byte[] ba) { StringBuilder hex = new StringBuilder(ba.Length * 2); diff --git a/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub b/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub index 3e6ffa14a..38d4e7f67 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub and b/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Berilia.sub b/Barotrauma/BarotraumaShared/Submarines/Berilia.sub index 7111e046d..b2208771e 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Berilia.sub and b/Barotrauma/BarotraumaShared/Submarines/Berilia.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Dugong.sub b/Barotrauma/BarotraumaShared/Submarines/Dugong.sub index 92b63ca88..8c1105f88 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Dugong.sub and b/Barotrauma/BarotraumaShared/Submarines/Dugong.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Hemulen.sub b/Barotrauma/BarotraumaShared/Submarines/Hemulen.sub index d307e0ca5..3c5994800 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Hemulen.sub and b/Barotrauma/BarotraumaShared/Submarines/Hemulen.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Humpback.sub b/Barotrauma/BarotraumaShared/Submarines/Humpback.sub index f18f2889c..c1640d13f 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Humpback.sub and b/Barotrauma/BarotraumaShared/Submarines/Humpback.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub b/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub index f59877ef3..5e14feb89 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub and b/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub b/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub index 6bf6eb494..28b1c9410 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub and b/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Orca.sub b/Barotrauma/BarotraumaShared/Submarines/Orca.sub index 901e9decc..a59bad499 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Orca.sub and b/Barotrauma/BarotraumaShared/Submarines/Orca.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/R-29.sub b/Barotrauma/BarotraumaShared/Submarines/R-29.sub index d3f62f6fd..be89e7cf0 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/R-29.sub and b/Barotrauma/BarotraumaShared/Submarines/R-29.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Remora.sub b/Barotrauma/BarotraumaShared/Submarines/Remora.sub index 5637a8ce6..c70b638d3 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Remora.sub and b/Barotrauma/BarotraumaShared/Submarines/Remora.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub b/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub index 539211663..943887519 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub and b/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Selkie.sub b/Barotrauma/BarotraumaShared/Submarines/Selkie.sub index c989a3fa9..4075081a5 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Selkie.sub and b/Barotrauma/BarotraumaShared/Submarines/Selkie.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Typhon.sub b/Barotrauma/BarotraumaShared/Submarines/Typhon.sub index c3cda313c..b0fb3144b 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Typhon.sub and b/Barotrauma/BarotraumaShared/Submarines/Typhon.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub b/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub index 179ad01de..4518bcd0f 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub and b/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Venture.sub b/Barotrauma/BarotraumaShared/Submarines/Venture.sub index f014bdfec..0ce3c7daa 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Venture.sub and b/Barotrauma/BarotraumaShared/Submarines/Venture.sub differ diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 0c799d62e..c6d0ec1f7 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,246 @@ +--------------------------------------------------------------------------------------------------------- +v0.13.0.11 +--------------------------------------------------------------------------------------------------------- + +Campaign changes: +- Replaced "Lair" locations with "hunting grounds" in the connections between levels. Inhabited locations next to hunting grounds have a chance of getting abandoned and habitation can't spread through those levels to adjacent locations. The hunting grounds can be cleared by killing a boss monster in the level. +- Beacon stations are shown on the campaign map. +- Visual changes to the map to make it a bit more intuitive. +- When you enter a level with a beacon station, you always get an optional side objective to restore it even if you haven't selected a beacon mission. +- Added radiation on the campaign map: the intensity of the radiation around Jupiter is slowly increasing, which is forcing Europans to delve deeper under the ice. In practice, the radiation gradually destroys the outposts starting from the left side of the map, making it more dangerous and costly to stay in these areas. The intention behind this is to prevent players from farming resources indefinitely in the low-difficulty areas of the game before proceeding further. +- Gating progress between biomes: you need a certain amount of money or reputation before you can enter the next biome. +- Reworked exit points at uninhabited locations: there's a hole/tunnel above the start/end of the level the sub needs to enter to leave. + +Abyss: +- Reintroduced Endworm and Charybdis. +- Added floating islands that contain caves and rare minerals to the Abyss. + +New player experience: +- Added in-game hints, designed to help with new player onboarding. Can be disabled in the settings. +- Added text highlighting to mission descriptions. +- Player-controlled characters get automatically assigned an appropriate order when the game starts to guide the player on what they should do and to make it easier to find the relevant device(s). +- Changed the camera animation at the start of the campaign to show the entire outpost. +- Display mission difficulty in the available missions list, the info tab, and the round summary. +- Show an indicator when the campaign is saved. + +Changes and additions: +- Added abandoned outposts. +- New mission types for abandoned outposts: destroying the outpost, clearing a nest, hostage rescue and assassination. +- Added entity subcategories to the submarine editor. +- Added interactive submarine previews to the server lobby and "New Game" screen. +- Added respawning mid-round in the multiplayer campaign mode. Respawning gives you an affliction that can be healed for a cost at outposts. You can also choose not to respawn mid-round, and instead wait for the next round to spawn normally without the affliction, just like before. +- Reworked the impacts to the sub when it's hit by monsters. Increased the screen shake and stunning. There's now a 30 second cooldown for getting knocked down. Fixed the impact not triggering if the colliding limb is not big enough. +- Reworked attacks and effects for the following creatures: Moloch, Black Moloch, Hammerhead, Hammerhead Matriarch, and Golden Hammerhead. They now have a bigger impact on the sub when they hit it. Black Moloch's emp damage is halved. +- Added Abyss Diving Suits which slows down your movement speed but offers more protection against pressure and consumes oxygen more slowly than the normal suits. +- Added Combat Diving Suits which slow you down less than normal suits and offer more protection against damage. +- Added black moloch mission variants. +- Moloch's shell now always breaks when shot with a railgun. +- Increased the audio ranges for Watcher and Hammerhead Matriarch. +- Overhauled tab menu: improved layout, added submarine and reputation tabs, current funds are displayed in the campaign mode. +- Recreated waypoints for the vanilla subs. +- Bots can now be renamed through the outpost crew management interface. +- Certain large monsters' (endworm, charybdis, boss variants of molochs and hammerheads) health bars become visible at the top of the screen when you damage them. +- Added EMP effect to nuclear shells. Increased the damage a bit. +- Added a right click context menu option to copy debug console errors to clipboard. +- Railgun shells now explode instead of piercing armor. Physicorium shells still go through armor. Make all railgun ammunition slightly more likely to break limb joints. +- Added multiple current orders: you can assign up to three simultaneous orders for characters and drag the icons to change their priority. +- NPCs (non-bots) speak report related lines less than they used to. +- The bleeding particles are now emitted from the last matching limb instead of the first. Readjusted particle emit frequency and scale. +- Reduced Moloch's bleeding reductions. +- Trying to pick up a diving suit when you're already wearing one swaps the suits. +- Made monster eggs glow to make them easier to spot in caves and abandoned outposts. +- Exploding oxygen/fuel tanks don't make other tanks explode. +- Added a delay to oxygen/fuel tank explosions. +- Disconnect wires from beacon stations instead of deleting the wires completely. +- The probability of disconnected wires in beacon stations is relative to the level's difficulty, and no wires are removed if the difficulty is less than 20. +- Improved server filters: instead of only being able to search for servers that have a certain setting enabled/disabled, the filter can be set to "Any" (thanks someone972!). +- Show whether a server is player-hosted or dedicated in the server list. +- Show whether a server is public or private in the server lobby. +- Added a server setting for locking all default wiring in subs. +- Added favorite button to the server lobby. +- Banning a player who's using Steam Family Sharing will now also ban the owner's account, solving ban evasion using this method. +- Disabled NPC conversations in sub editor test mode. +- Beacon stations' reactors don't deteriorate or consume fuel to prevent the station from going back down when the player is travelling out from the level. +- The output length of all signal components is now restricted to 200 characters by default to prevent performance and networking issues if the output is set to an excessively long value (such as the entire dialog of the Bee Movie). The limit can be increased in the sub editor. +- Stunning still works on friendly characters even if friendly fire has been disabled on a server. +- Made radiation sickness's icon appear before it starts causing burns (instead of working the other way around). +- Made radiation sickness cause nausea. +- Added a screen distortion effect for radiation sickness. +- Added a checkbox to color component that toggles between RGBA and HSV input. +- Fabricators now display remaining time when fabricating. +- Revisited husk animations, movement speeds, and general balance. Husks regenerate a lot and are a bit tougher than they used to be. Human husks can and will easily rise again, unless properly killed. +- Adjustments and fixes on the thresher attacks. +- Revisit mudraptors' attacks. Fixes unarmored mudraptors and mudraptor hatchlings acting weird when attacking characters in water. +- Lower the overall damage of all the hatchlings. +- Added a link to the wiki to the main menu. +- Drop empty/used oxygen tank from diving suit when trying to swap in a new one when the inventory is full. +- When using an equipped item from a stack (e.g. a medical item), another item from the same stack is automatically equipped. +- The mid-round mission messages (e.g. "the monster is dead, navigate out...") are shown in the tab menu's mission tab. +- Make characters grabbable and the inventories accessible after a half a second delay. Fixes abusing the toy hammer to access NPC inventories. +- Allow fabricating fresh fuel rods, welding fuel and coilgun ammunition using depleted ones. +- Restrict the length of the text that can be entered as a sonar beacon's sonar label. + +Fixes: +- Major improvements to the voice chat: higher audio quality, less intrusive radio effect, fixed clicks/distortion. +- Attempt to fix crashes with the error message "failed to generate OpenAL/stream buffer" on Mac. +- Fixed crashing on startup with the error message "unable to load shared library 'freetype6' or one of its dependencies" on some Linux systems. +- Removed outdated steamclient.so file from the dedicated server (no longer needed because it's automatically installed by Steamworks). Should fix inability to host dedicated servers on Linux. +- Fixed clients' job preferences not being respected in all cases where they should, resulting in some clients getting a job that should be give to another client. +- Items can't be stacked in the equip slots (e.g. you can't hold and throw a stack of grenades). +- Trying to pick up a stack of items with a full inventory doesn't pick up the item in both hand slots. +- Fixes/improvements to item position syncing. When trying to pick up an item whose position has gotten desynced, the server should correct the item's position immediately. +- Fixed turrets getting cropped by the "wikiimage" console command. +- Fixed items with nothing but a holdable component (e.g. medical items) being hidden when hiding wires in the sub editor. +- Fixed ban list not displaying ban reasons or the duration of the ban. +- Fixed items appearing misplaced when switching to the sub editor mid-round with console commands. +- Fixed shuttles spawning undocked in certain custom subs (e.g. DOMA Prototype Mk 8). +- Fixed "tried to add a dead character to CrewManager" console error when reviving a character who died of high pressure. +- Fixed skill levels not getting updated in the "my character" tab mid-round. +- Fixed clients not calculating eventmanager intensity, preventing any high-intensity tracks from playing in multiplayer. +- Fixed campaign map being closed with RMB even if the user has inverted their mouse buttons. +- Fixed damage sometimes desyncing when the sub hits walls/spires: happened because damage was also applied client-side, which sometimes caused the clients to damage walls that weren't actually hit server-side. +- Fixed physics bodies between docking ports not getting mirrored in mirrored subs, sometimes causing impassable areas in spots where the docking port used to be. +- Fixed status monitor in a docked shuttle sometimes not displaying the main sub. +- Fixed campaign stores sometimes displaying prices for a previous location. +- Fixed various scaling issues on higher resolutions. +- Fixed monsters fluttering while pursuing other creatures. Only happened while swimming and was not notable on all monsters). +- Fixed mouse cursor being switched to hand even when the spritesheet is not shown in the character editor. +- Fixed monsters with low head/torso torques moving backwards when they try to turn around 180 degrees. +- Fixed regular Moloch not bleeding correctly. +- Fixed Workshop mod download prompt looping and freezing when there's a hash mismatch after a mod has already been installed. +- Fixed ability to walk through the heavy doors in mining outposts. +- Fixed nav terminal ignoring velocity_in signals when the terminal hasn't been operated by anyone during the round. +- Fixed one of the engineer variants having too many items in the toolbelt. +- Fixed "hide incompatible" checkbox in the server browser. +- Fixed characters getting impact damage from collisions with sensors. Caused characters to get stunned when they're thrown around by a monster while touching a level trigger (for instance, the branches in forest caves). +- Fixed bioluminescent cave's hallucination effect never going fully away and made it treatable with haloperidol. +- Fixed bots treating radiation sickness too eagerly (leading to wasted antirad). +- Added missing platforms to abandoned outpost modules. +- Fixed non-interactable items being counted as owned in the store interface. +- Fixed husk's mouth tentacles rendering on top of the left hand. Also fixed a minor texture bleeding on the waist. +- Fixed possible desync when multiple players use the crew management interface simultaneously. +- Fixed a crash when no valid limb was found for an affliction. Probably only happened with characters that already had some limbs severed. +- Fixed non-multiplied damage to limbs effectively always being clamped to 100, which means that no attack could do more than 100 damage per hit unless there's some damage modifier defined on the target limb. +- Monsters now stop targeting characters that are far enough behind level geometry. Fixes them getting stuck while trying to reach targets that are not easily reachable. +- Fixed players getting prompted to download subs when the server has disabled file transfers. +- Fixed command-spawned characters not getting the "job" tag on their id cards. +- Fixed coilguns consuming ammo faster than they should when linked to multiple loaders. +- Fixed wiring interface labels overlapping when using a high text scale. +- Allow engine propellers to do damage inside the sub. +- Fixed new lights being displayed as off in the sub editor until you toggle the lighting. +- Fixed non-ruin artifacts sometimes spawning on top of the sub. +- Fixed terminal's welcome message getting cleared when using the test mode in the sub editor. +- Fixed fabricator displaying the "not enough room in the input inventory" error even if the items fit in the input inventory by stacking. +- Fixed signal check components being unable to output whitespace. +- Fixed inability to edit smoke detector's parameters in-game. +- Fixed smoke detectors outputting empty signals. +- Fixed "engineers are special" outpost event not giving a price discount. +- Fixed clients being unable to vote for subs they don't have. +- Fixed a bunch of non-suitable items (such as devices that can't be deattached) being displayed in the "extra cargo" menu in the server lobby. +- Fixed crashing if you try to press the server log button in the lobby when connection to the server has been lost. +- Fixed item disappearing from the character's hand when you combine it with an item in a container in a way that doesn't fully deplete/remove it. +- Fixed gaps in the "backwall pipes" sprite. +- Fixed dedicated servers restricting player count to 1 less than the MaxPlayer setting. +- Fixed Kastrull's drone airlock not flooding correctly. +- Replaced legacy small pumps with the new ones in Orca. +- Fixed monsters sometimes spawning very close (or even inside) the respawn shuttle. +- Fixed "attempted to set SoundRange to NaN" error when a reactor's maximum power output is 0. +- Fixed wall damage particles sometimes spawning at an incorrect position. +- Fixed black moloch doing only 62.5% of the structure damage compared to the regular moloch. +- Fixes to R-29: fixed top docking port's control circuit, adjustments to pre-placed supplies, adjusted discharge coil power consumption, minor visual fixes. +- Reduced the amount of clown gear in the "Praise the Honkmother" mission to fit everything in the crate. +- Fixed non-repeatable outpost events starting to repeat once all of them have been triggered. +- Fixed inconsistency in the way vertical velocity in/out signals are handled by the nav terminals. Previously "velocity_in" would interpret positive y as upwards, even though "velocity_y_out" outputs a negative value when going up. Now positive is always down (= corresponds with the "descent velocity" value displayed on the terminal). +- Fixed Hammerhead Spawns not targeting decoys. +- Fixed the abyss monsters not targeting "provocative" items, like flares, glowsticks, scooters etc. +- Fixed Hammerhead Matriarch constantly fleeing from divers. It's intentional that the matriarch avoids the divers, but not as much. Now they should flee briefly only when shot by the player, making it possible for the divers to reach it (#5449). +- Fixed camera shaking/vibrating when moving at high speed. +- Fixed monsters being able to attack through ruin walls. +- Fixed overlapping vending machine and light switch in CrewModule_02. +- Fixed respawn shuttle maintaining an incorrect position after getting dispatched. +- Fixed inventory tooltip not changing when the cursor is hovered on the slot and the condition of the item changes. +- Fixed waypoints not being able to find hulls that have been added since the sub was last saved in the sub editor, causing them to appear blue. +- Fixed resizable outpost hallway modules not having waypoints and thus causing navigation issues. +- Fixed water particle effects not showing up when water flows between certain rooms in Remora. +- Addressed the lag spikes occasionally caused by Spineling's spikes when they hit/get stuck to something. +- Fixed logbooks being empty in the wreck salvage missions. + +Bots: +- Bots now warn when you are running low / out of oxygen or welding fuel tanks, turret ammunition, or reactor fuel. +- Bots no longer report dead monsters on the sonar. +- Disabled the dialogue lines about not finding any targets for the given order, since they don't really improve the feedback. Instead they can give a wrong impression that the bot is not following the order when they actually are. +- Bots no longer speak about not being able to reach the diving suit when they fail to get it because it was taken by someone else. They will target another suit like they used to. +- Bots don't extinguish fires when there's ballast flora in the hull. +- Bots only speak about getting attacked when attacked by players. Fixes security bots throwing ultimatums on other bots that accidentally damage them (e.g. while welding). +- Bots that operate an item (reactor/turret) now drop the empty items on the floor instead of placing them back to a container. This saves time and the empty items should be taken care of by the idle bots. +- Fixed bots not seeking more oxygen before the remaining oxygen level drops to zero when they already have diving gear equipped and when they are staying stationary, leading to suffocation when the bot is repairing/operating while already wearing the diving gear and running out of oxygen tanks. +- Fixed bots trying to swap the oxygen tank too early when they are outside of the sub, which in some cases might lead to bugs where they try to get back inside the submarine. +- Improved the feedback by adding dialogues for swapping the oxygen and for not being able to find more oxygen. +- Fixed bots trying to return the diving suit when it's not a reasonable thing to do, but when they actually don't need it. +- Fixed bots failing to swap an oxygen tank from a diving mask to a diving suit when both items are equipped. +- Fixed bots getting confused when they have a mask without a valid oxygen tank inside it and when there's not enough oxygen in the room to be without the mask. +- Bots now react faster in general to all enemies. +- Bots now automatically attack enemies outside of the main sub, if they have a weapon. While docked to an enemy outpost or submarine, the "fight intruders" order now acts as an offensive order to attack the enemies on the connected submarine/outpost. +- Fixed bots following the player when the player is controlling a monster. +- You can now escape from NPCs by going far enough from them while they are pursuing you. +- Fixed bots reacting to attackers that are outside of the submarine. +- Fixed the "reportrange" parameter not working when the character is attacked. In practice only has effect on NPCs. +- Fixed report icons being shown also for other teams instead of just one's own team. +- Bots now target the closest limb instead of the main collider when they aim with the turret. With small creatures, this should now make any difference. With long creatures, it allows the bots to target the extremities of the body instead of always shooting at the main body. +- Fixed non-security bots fleeing the enemy (in defensive combat state) even when they are ordered to fight intruders. +- Bots no longer automatically unequip weapons after combat if they are outside of a friendly submarine. +- Fixed bots having difficulties in leaving the ruins via gaps. +- Allow bots to use gaps also to exit the sub, but only when following a player character. +- Fixed bots always trying to reach the closest item even when it's unreachable. +- Make bots find items faster. +- Fixed bots sometimes continuing their movement (e.g. walking towards a wall) when they fail to find the diving gear they need to continue with the go to objective (e.g. follow). +- Fixed security officers standing idle next to the stunned target and doing nothing when they don't have handcuffs. Only happened while trying to arrest the target. +- Fixed multiple bots occasionally trying to operate the reactor at the same time. +- Fixed bots "stealing" stuff while cleaning up. Happened when the objective changed (= they decided or were ordered to do something else while cleaning up). Idle bots are no longer allowed to take items from purchased containers. +- Fixed bots running headlessly around while trying to find the container for items while following the clean up order. Now they should walk around instead and only run when the target container is found. +- Fixes and adjustments to security officer reactions for damage done by friendlies. +- Fixed bots sometimes yelling that they can't enter an airlock when the sub is docked. +- Fixed bots taking items from fabricators and deconstructors. +- Fixed bots saying "Can't reach target [name]". + +Modding: +- Fixed custom loading screen tips not showing up if the vanilla content package is enabled. +- Fixed crashing when loading a save where a stack contains more items than the maximum stack size for the item/container. +- The hit impact of a monster's attack can now be adjusted with the new "submarineimpactmultiplier" defined in the attack block. Note that this is a multiplier to the actual impact, hence also the force applied on the attacking monster affects the final impact. +- Explosions now have three new parameters: ignorecover, onlyinside, and onlyoutside. +- Fixed crashing/disconnects when preloading content at the start of a round if random events contain character variants. In practice, mods that used character variants in random events occasionally prevented rounds from starting. +- Fixed crashing when attempting to play a music clip that isn't a valid ogg file or if the file is not found. +- Fixed crashing when trying to spawn a character variant with custom inventory contents. +- Renamed the ai parameter "Threshold" as "DamageThreshold". +- Replaced "spawndeep" with "abyss" spawn type (defined in random events). +- New parameters StayInAbyss and StayInsideLevel that define the area where the creature tries to keep inside while not attacking. +- The creature disable distance, which totally disables the creature, is now exposed in the character parameters. The distance for triggering simple physics, which disables all the limbs and keeps only the main collider updated, is half of this distance. Increased the default from 22 000 to 25 000 (pixels). +- New attack pattern: Circle (around the target). Used by the abyss creatures. +- The aim speed and accuracy of NPC characters can now be adjusted in the npc (spawn) definition. The Aim speed also affects melee attack speed. +- Fixed OnSevered status effects launching also on the limb that the severed limb was attached to. +- Added "bleedingnonstop" affliction, which is just the same as normal bleeding but it never wears off. +- Added and option to target the last matching limb instead of the first (StatusEffect.TargetType.Limb). Implemented targeting other limbs even when the status effect is triggered from the limb, which was previously only implemented for status effects that targeted character. See Endworm for an example. +- Fixed hidden limbs not being ignored in many cases where they should, which potentially could cause issues with some custom monsters. +- Added "bleedparticlemultiplier" parameter in character definition, which can be used to increase/decrease the general amount of bleeding for the character in question. +- Added an option to always ignore an ai target if it's not inside the same sub as the character. +- Attacks can now "blink" limbs when they attack (Endworm). Blinking is a generic way to rotate limbs so that they "animate" (see Watcher's eye). +- Added AITrigger that can be used to trigger an ai state using the status effects (See Charybdis). +- Turned IsTraitor into a property so it can be accessed by status effects. +- Changed husk affliction's type to "alieninfection" because using "huskinfection" as both the identifier and type makes it impossible to add custom husk infections and reference just the custom one in conditionals or statuseffects. +- Fixed affliction statuseffects targeting NearbyItems or NearbyCharacters causing a crash. +- Corpses don't spawn in wrecks that contain no waypoints/spawnpoints. +- Monster events don't spawn monsters in wrecks that don't contain enemy spawnpoints. +- Fixed afflictions caused by status effects being multiplied by deltatime even when setvalue="true". The only vanilla statuseffect affected by this was incremental stun, which would stun the player for 1 frame instead of 1 second, but this may have affected some mods as well. +- Fixed errors when trying to load a beacon station with no reactor. +- Fixed level editor crashing when it tries to place a wreck that contains linked subs in the level. +- Added "onlyplayertriggered" condition for status effects. Currently only implemented for OnDamaged. +- Fixed crashing if you try to create a decal with incorrect casing. +- Fixed affliction's periodic effects being unable to reference afflictions defined later in the affliction xml. +- Fixed crashing if you save a campaign with a large map and try to load it with map generation parameters where the map is smaller. +- Fixed explosions not damaging level walls if their structure damage is set to 0. +- Fixed inability to load more than 5 wires per connection even if the connection is set to allow more. +- Allow to define afflictions (types or identifiers) to trigger the OnDamaged status effects only from certain afflictions and not all that do the damage. If the afflictions property is not defined, no restrictions are used (works as it used to). + --------------------------------------------------------------------------------------------------------- v0.12.0.3 --------------------------------------------------------------------------------------------------------- diff --git a/Barotrauma/BarotraumaShared/hintmanager.xml b/Barotrauma/BarotraumaShared/hintmanager.xml new file mode 100644 index 000000000..ed6c346cc --- /dev/null +++ b/Barotrauma/BarotraumaShared/hintmanager.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/steamclient.so b/Barotrauma/BarotraumaShared/steamclient.so deleted file mode 100644 index 214fabf4d..000000000 Binary files a/Barotrauma/BarotraumaShared/steamclient.so and /dev/null differ