diff --git a/.github/DISCUSSION_TEMPLATE/bug-reports.yml b/.github/DISCUSSION_TEMPLATE/bug-reports.yml index d10175604..ecd1e3753 100644 --- a/.github/DISCUSSION_TEMPLATE/bug-reports.yml +++ b/.github/DISCUSSION_TEMPLATE/bug-reports.yml @@ -73,8 +73,7 @@ body: label: Version description: Which version of the game did the bug happen in? You can see the current version number in the bottom left corner of your screen in the main menu. options: - - v1.4.6.0 (Blood in the Water Update, hotfix 2) - - v1.5.4.0 (unstable) + - v1.5.7.0 (Summer Update) - Other validations: required: true diff --git a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs index 1858f969c..c497c157f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs @@ -66,6 +66,8 @@ namespace Barotrauma public Vector2 ShakePosition { get; private set; } private float shakeTimer; + public float MovementLockTimer; + private float globalZoomScale = 1.0f; //used to smooth out the movement when in freecam @@ -257,13 +259,15 @@ namespace Barotrauma float moveSpeed = 20.0f / zoom; + MovementLockTimer -= deltaTime; + Vector2 moveCam = Vector2.Zero; if (TargetPos == Vector2.Zero) { Vector2 moveInput = Vector2.Zero; if (allowMove && !Freeze) { - if (GUI.KeyboardDispatcher.Subscriber == null && allowInput) + if (GUI.KeyboardDispatcher.Subscriber == null && allowInput && MovementLockTimer <= 0.0f) { if (PlayerInput.KeyDown(Keys.LeftShift)) { moveSpeed *= 2.0f; } if (PlayerInput.KeyDown(Keys.LeftControl)) { moveSpeed *= 0.5f; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 3b5ff2c41..e35f32c4e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -558,14 +558,17 @@ namespace Barotrauma if (GameMain.Client != null) { chatMessage += " " + TextManager.Get("DeathChatNotification"); } - GameMain.NetworkMember.RespawnManager?.ShowRespawnPromptIfNeeded(); + RespawnManager.ShowDeathPromptIfNeeded(); GameMain.NetworkMember.AddChatMessage(chatMessage.Value, ChatMessageType.Dead); GameMain.LightManager.LosEnabled = false; controlled = null; - if (!(Screen.Selected?.Cam is null)) + if (Screen.Selected?.Cam is Camera cam) { - Screen.Selected.Cam.TargetPos = Vector2.Zero; + cam.TargetPos = Vector2.Zero; + //briefly lock moving the camera with arrow keys + //(it's annoying to have the camera fly off when you die while trying to move to safety) + cam.MovementLockTimer = 2.0f; Lights.LightManager.ViewTarget = null; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index 1bee8cd84..633630298 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -289,7 +289,7 @@ namespace Barotrauma if (character.Params.CanInteract && character.SelectedCharacter != null && character.SelectedCharacter.Inventory != null) { - if (character.SelectedCharacter.CanInventoryBeAccessed) + if (character.SelectedCharacter.IsInventoryAccessibleTo(character)) { character.SelectedCharacter.Inventory.Update(deltaTime, cam); } @@ -677,7 +677,7 @@ namespace Barotrauma { if (character.Params.CanInteract && character.SelectedCharacter != null && character.SelectedCharacter.Inventory != null) { - if (character.SelectedCharacter.CanInventoryBeAccessed) + if (character.SelectedCharacter.IsInventoryAccessibleTo(character)) { character.SelectedCharacter.Inventory.Locked = false; character.SelectedCharacter.Inventory.CurrentLayout = CharacterInventory.Layout.Left; @@ -759,7 +759,7 @@ namespace Barotrauma textPos.Y += largeTextSize.Y; } - if (character.FocusedCharacter.CanBeDragged) + if (character.FocusedCharacter.CanBeDraggedBy(character)) { string text = character.CanEat ? "EatHint" : "GrabHint"; GUI.DrawString(spriteBatch, textPos, GetCachedHudText(text, InputType.Grab), @@ -767,11 +767,7 @@ namespace Barotrauma textPos.Y += largeTextSize.Y; } - if (!character.DisableHealthWindow && - character.IsFriendly(character.FocusedCharacter) && - character.FocusedCharacter.CharacterHealth.UseHealthWindow && - character.CanInteractWith(character.FocusedCharacter, 160f, false) && - !character.IsClimbing) + if (character.FocusedCharacter.CanBeHealedBy(character)) { GUI.DrawString(spriteBatch, textPos, GetCachedHudText("HealHint", InputType.Health), GUIStyle.Green, Color.Black, 2, GUIStyle.SmallFont); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index 0b41bcf3d..0fea47a53 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -141,7 +141,7 @@ 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 }; + var skillName = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillsArea.RectTransform), skill.DisplayName, textColor: textColor, font: font) { Padding = Vector4.Zero }; float modifiedSkillLevel = skill.Level; if (Character != null) @@ -582,6 +582,7 @@ namespace Barotrauma ch.ExperiencePoints = inc.ReadInt32(); ch.AdditionalTalentPoints = inc.ReadRangedInteger(0, MaxAdditionalTalentPoints); + ch.PermanentlyDead = inc.ReadBoolean(); return ch; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 08f944f2c..3ade24b3d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -723,11 +723,19 @@ namespace Barotrauma character.ReadStatus(inc); } - if (character.IsHuman && character.TeamID != CharacterTeamType.FriendlyNPC && character.TeamID != CharacterTeamType.None && !character.IsDead) + if (character.IsHuman && character.TeamID != CharacterTeamType.FriendlyNPC && character.TeamID != CharacterTeamType.None) { CharacterInfo duplicateCharacterInfo = GameMain.GameSession.CrewManager.GetCharacterInfos().FirstOrDefault(c => c.ID == info.ID); GameMain.GameSession.CrewManager.RemoveCharacterInfo(duplicateCharacterInfo); - GameMain.GameSession.CrewManager.AddCharacter(character); + if (character.isDead) + { + //just add the info if dead (displayed in the round summary, and crew list if the character is revived) + GameMain.GameSession.CrewManager.AddCharacterInfo(character.info); + } + else + { + GameMain.GameSession.CrewManager.AddCharacter(character); + } } if (GameMain.Client.SessionId == ownerId) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index e9798be70..01a89a762 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -152,8 +152,7 @@ namespace Barotrauma if (value == null && Character.Controlled?.SelectedCharacter?.CharacterHealth != null && - Character.Controlled.SelectedCharacter.CharacterHealth == prevOpenHealthWindow/* && - !Character.Controlled.SelectedCharacter.CanInventoryBeAccessed*/) + Character.Controlled.SelectedCharacter.CharacterHealth == prevOpenHealthWindow) { Character.Controlled.DeselectCharacter(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index 519b12eed..04fb57758 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -289,18 +289,19 @@ namespace Barotrauma spriteAnimState.Add(decorativeSprite, new SpriteState()); } TintMask = null; + float sourceRectScale = ragdoll.RagdollParams.SourceRectScale; foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "sprite": - Sprite = new Sprite(subElement, file: GetSpritePath(subElement, Params.normalSpriteParams, ref _texturePath)); + Sprite = new Sprite(subElement, file: GetSpritePath(subElement, Params.normalSpriteParams, ref _texturePath), sourceRectScale: sourceRectScale); break; case "damagedsprite": - DamagedSprite = new Sprite(subElement, file: GetSpritePath(subElement, Params.damagedSpriteParams, ref _damagedTexturePath)); + DamagedSprite = new Sprite(subElement, file: GetSpritePath(subElement, Params.damagedSpriteParams, ref _damagedTexturePath), sourceRectScale: sourceRectScale); break; case "conditionalsprite": - var conditionalSprite = new ConditionalSprite(subElement, GetConditionalTarget(), file: GetSpritePath(subElement, null, ref _texturePath)); + var conditionalSprite = new ConditionalSprite(subElement, GetConditionalTarget(), file: GetSpritePath(subElement, null, ref _texturePath), sourceRectScale: sourceRectScale); ConditionalSprites.Add(conditionalSprite); if (conditionalSprite.DeformableSprite != null) { @@ -310,7 +311,7 @@ namespace Barotrauma } break; case "deformablesprite": - _deformSprite = new DeformableSprite(subElement, filePath: GetSpritePath(subElement, Params.deformSpriteParams, ref _texturePath)); + _deformSprite = new DeformableSprite(subElement, filePath: GetSpritePath(subElement, Params.deformSpriteParams, ref _texturePath), sourceRectScale: sourceRectScale); var deformations = CreateDeformations(subElement); Deformations.AddRange(deformations); NonConditionalDeformations.AddRange(deformations); @@ -339,7 +340,7 @@ namespace Barotrauma ContentPath tintMaskPath = subElement.GetAttributeContentPath("texture"); if (!tintMaskPath.IsNullOrWhiteSpace()) { - TintMask = new Sprite(subElement, file: GetSpritePath(tintMaskPath)); + TintMask = new Sprite(subElement, file: GetSpritePath(tintMaskPath), sourceRectScale: sourceRectScale); TintHighlightThreshold = subElement.GetAttributeFloat("highlightthreshold", 0.6f); TintHighlightMultiplier = subElement.GetAttributeFloat("highlightmultiplier", 0.8f); } @@ -348,7 +349,7 @@ namespace Barotrauma ContentPath huskMaskPath = subElement.GetAttributeContentPath("texture"); if (!huskMaskPath.IsNullOrWhiteSpace()) { - HuskMask = new Sprite(subElement, file: GetSpritePath(huskMaskPath)); + HuskMask = new Sprite(subElement, file: GetSpritePath(huskMaskPath), sourceRectScale: sourceRectScale); } break; } @@ -700,31 +701,34 @@ namespace Barotrauma if (spriteParams == null || Alpha <= 0) { return; } float burn = spriteParams.IgnoreTint ? 0 : burnOverLayStrength; float brightness = Math.Max(1.0f - burn, 0.2f); - Color clr = spriteParams.Color; + Color tintedColor = spriteParams.Color; if (!spriteParams.IgnoreTint) { - clr = clr.Multiply(ragdoll.RagdollParams.Color); + tintedColor = tintedColor.Multiply(ragdoll.RagdollParams.Color); if (character.Info != null) { - clr = clr.Multiply(character.Info.Head.SkinColor); + tintedColor = tintedColor.Multiply(character.Info.Head.SkinColor); } if (character.CharacterHealth.FaceTint.A > 0 && type == LimbType.Head) { - clr = Color.Lerp(clr, character.CharacterHealth.FaceTint.Opaque(), character.CharacterHealth.FaceTint.A / 255.0f); + tintedColor = Color.Lerp(tintedColor, character.CharacterHealth.FaceTint.Opaque(), character.CharacterHealth.FaceTint.A / 255.0f); } if (character.CharacterHealth.BodyTint.A > 0) { - clr = Color.Lerp(clr, character.CharacterHealth.BodyTint.Opaque(), character.CharacterHealth.BodyTint.A / 255.0f); + tintedColor = Color.Lerp(tintedColor, character.CharacterHealth.BodyTint.Opaque(), character.CharacterHealth.BodyTint.A / 255.0f); } } - Color color = new Color((byte)(clr.R * brightness), (byte)(clr.G * brightness), (byte)(clr.B * brightness), clr.A); + Color color = new Color(tintedColor.Multiply(brightness), tintedColor.A); + Color colorWithoutTint = new Color(spriteParams.Color.Multiply(brightness), spriteParams.Color.A); Color blankColor = new Color(brightness, brightness, brightness, 1); if (deadTimer > 0) { color = Color.Lerp(color, spriteParams.DeadColor, MathUtils.InverseLerp(0, spriteParams.DeadColorTime, deadTimer)); + colorWithoutTint = Color.Lerp(colorWithoutTint, spriteParams.DeadColor, MathUtils.InverseLerp(0, spriteParams.DeadColorTime, deadTimer)); } color = overrideColor ?? color; + colorWithoutTint = overrideColor ?? colorWithoutTint; blankColor = overrideColor ?? blankColor; color *= Alpha; blankColor *= Alpha; @@ -739,6 +743,7 @@ namespace Barotrauma else if (severedFadeOutTimer > SeveredFadeOutTime - 1.0f) { color *= SeveredFadeOutTime - severedFadeOutTimer; + colorWithoutTint *= SeveredFadeOutTime - severedFadeOutTimer; } } @@ -956,7 +961,7 @@ namespace Barotrauma { DamagedSprite.Draw(spriteBatch, new Vector2(body.DrawPosition.X, -body.DrawPosition.Y), - color * damageOverlayStrength, activeSprite.Origin, + colorWithoutTint * damageOverlayStrength, activeSprite.Origin, -body.DrawRotation, Scale, spriteEffect, activeSprite.Depth - depthStep * Math.Max(1, WearingItems.Count * 2)); // Multiply by 2 to get rid of z-fighting with some clothing combos } diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxComponent.cs index 34d62728d..bdfc98c6e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxComponent.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System; using System.Linq; @@ -75,7 +75,7 @@ namespace Barotrauma foreach (ItemComponent ic in Item.Components) { if (ic is Holdable) { continue; } - if (!ic.AllowInGameEditing) { continue; } + if (!ic.AllowInGameEditing && Screen.Selected is not { IsEditor: true }) { continue; } if (SerializableProperty.GetProperties(ic).Count == 0 && !SerializableProperty.GetProperties(ic).Any(p => p.GetAttribute().IsEditable(ic))) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxConnection.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxConnection.cs index dfe8278e5..10bec598e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxConnection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxConnection.cs @@ -29,6 +29,12 @@ namespace Barotrauma Length = Rect.Width + Padding + Label.Size.X; } + public void SetLabel(LocalizedString label, CircuitBoxNode node) + { + Label = new CircuitBoxLabel(label, GUIStyle.SubHeadingFont); + Length = Rect.Width + Padding + Label.Size.X; + } + public void Draw(SpriteBatch spriteBatch, Vector2 drawPos, Vector2 parentPos, Color color) { if (CircuitBox.UI is not { } circuitBoxUi) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxInputOutputNode.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxInputOutputNode.cs new file mode 100644 index 000000000..d9658086f --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxInputOutputNode.cs @@ -0,0 +1,84 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + internal sealed partial class CircuitBoxInputOutputNode + { + private const string PromptUserData = "InputOutputEditPrompt"; + + public void PromptEdit(GUIComponent parent) + { + CircuitBox.UI?.SetMenuVisibility(false); + GUIFrame backgroundBlocker = new(new RectTransform(Vector2.One, parent.RectTransform), style: "GUIBackgroundBlocker") + { + UserData = PromptUserData + }; + + GUILayoutGroup mainLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 0.8f), backgroundBlocker.RectTransform, Anchor.Center), isHorizontal: false, childAnchor: Anchor.TopCenter); + GUIFrame labelArea = new(new RectTransform(new Vector2(1f, 0.8f), mainLayout.RectTransform, Anchor.Center)); + + GUILayoutGroup labelLayout = new GUILayoutGroup(new RectTransform(Vector2.One, labelArea.RectTransform), childAnchor: Anchor.Center); + GUIListBox labelList = new GUIListBox(new RectTransform(ToolBox.PaddingSizeParentRelative(labelLayout.RectTransform, 0.9f), labelLayout.RectTransform)); + + Dictionary textBoxes = new(); + + foreach (var conn in Connectors) + { + bool found = ConnectionLabelOverrides.TryGetValue(conn.Name, out string labelOverride); + + GUILayoutGroup connLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.12f), labelList.Content.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + new GUITextBlock(new RectTransform(new Vector2(0.4f, 1f), connLayout.RectTransform), text: conn.Connection.DisplayName, font: GUIStyle.SubHeadingFont); + GUITextBox box = GUI.CreateTextBoxWithPlaceholder(new RectTransform(new Vector2(0.6f, 1f), connLayout.RectTransform), text: found ? labelOverride : string.Empty, conn.Connection.DisplayName.Value); + box.MaxTextLength = MaxConnectionLabelLength; + + textBoxes.Add(conn.Name, box); + } + + new GUIButton(new RectTransform(new Vector2(0.5f, 0.1f), mainLayout.RectTransform), text: TextManager.Get("confirm")) + { + OnClicked = (_, _) => + { + var newOverrides = textBoxes.ToDictionary( + static pair => pair.Key, + static pair => pair.Value.Text); + + foreach (var (key, value) in newOverrides.ToImmutableDictionary()) + { + if (ConnectionLabelOverrides.TryGetValue(key, out string newValue)) + { + if (newValue == value) + { + newOverrides.Remove(key); + } + } + else if (string.IsNullOrWhiteSpace(value)) + { + newOverrides.Remove(key); + } + } + + CircuitBox.SetConnectionLabelOverrides(this, newOverrides); + RemoveEditPrompt(parent); + return true; + } + }; + new GUIButton(new RectTransform(new Vector2(0.5f, 0.1f), mainLayout.RectTransform), text: TextManager.Get("cancel")) + { + OnClicked = (_, _) => + { + RemoveEditPrompt(parent); + return true; + } + }; + } + + public void RemoveEditPrompt(GUIComponent parent) + { + if (parent.FindChild(PromptUserData) is not { } promptParent) { return; } + parent.RemoveChild(promptParent); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxLabelNode.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxLabelNode.cs index 498b61481..fdb664fe4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxLabelNode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxLabelNode.cs @@ -1,5 +1,6 @@ #nullable enable +using System; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; @@ -80,6 +81,18 @@ namespace Barotrauma return true; }; + var characterLimit = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), frame.RectTransform, Anchor.BottomRight) { RelativeOffset = new Vector2(0.03f, 0.02f) }, text: $"{bodyTextBox.Text.Length}/{NetLimitedString.MaxLength}", font: GUIStyle.SmallFont, textAlignment: Alignment.Right); + + bodyTextBox.OnTextChanged += (textBox, _) => + { + textBox.TextColor = textBox.TextBlock.SelectedTextColor = textBox.Text.Length > NetLimitedString.MaxLength + ? GUIStyle.Red + : GUIStyle.TextColorNormal; + + characterLimit.Text = $"{textBox.Text.Length}/{NetLimitedString.MaxLength}"; + return true; + }; + static void UpdateLabelColor(GUITextBox box) { bool found = TextManager.ContainsTag(box.Text); @@ -97,8 +110,8 @@ namespace Barotrauma } } - bodyTextBox.OnDeselected += (textBox, _) => UpdateLabelColor(textBox); - headerTextBox.OnDeselected += (textBox, _) => UpdateLabelColor(textBox); + bodyTextBox.OnDeselected += static (textBox, _) => UpdateLabelColor(textBox); + headerTextBox.OnDeselected += static (textBox, _) => UpdateLabelColor(textBox); UpdateLabelColor(bodyTextBox); UpdateLabelColor(headerTextBox); diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxMouseDragSnapshotHandler.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxMouseDragSnapshotHandler.cs index 59981ca5c..1acda77f9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxMouseDragSnapshotHandler.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxMouseDragSnapshotHandler.cs @@ -87,6 +87,16 @@ namespace Barotrauma SnapshotMoveAffectedNodes(); startClick = cursorPos; } + + public void ClearSnapshot() + { + lastNodesUnderCursor = ImmutableHashSet.Empty; + lastSelectedComponents = ImmutableHashSet.Empty; + moveAffectedComponents = ImmutableHashSet.Empty; + LastConnectorUnderCursor = Option.None; + LastWireUnderCursor = Option.None; + LastResizeAffectedNode = Option.None; + } /// /// Finds all connections and gathers them into a single list for easier iteration. @@ -168,38 +178,36 @@ namespace Barotrauma LastResizeAffectedNode = FindResizeBorderUnderCursor(lastNodesUnderCursor, cursorPos); } - private static Option<(CircuitBoxResizeDirection, CircuitBoxNode)> FindResizeBorderUnderCursor(ImmutableHashSet nodes, Vector2 cursorPos) + private Option<(CircuitBoxResizeDirection, CircuitBoxNode)> FindResizeBorderUnderCursor(ImmutableHashSet nodes, Vector2 cursorPos) { - foreach (var node in nodes) + if (!nodes.Any()) { return Option.None; } + + var node = circuitBoxUi.GetTopmostNode(nodes); + if (node is null || !node.IsResizable) { return Option.None; } + + const float borderSize = 32f; + + var rect = node.Rect; + RectangleF bottomBorder = new(rect.X, rect.Top, rect.Width, borderSize); + RectangleF rightBorder = new(rect.Right - borderSize, rect.Y, borderSize, rect.Height); + RectangleF leftBorder = new(rect.X, rect.Y, borderSize, rect.Height); + + bool hoverBottom = bottomBorder.Contains(cursorPos), + hoverRight = rightBorder.Contains(cursorPos), + hoverLeft = leftBorder.Contains(cursorPos); + + var dir = CircuitBoxResizeDirection.None; + + if (hoverBottom) { dir |= CircuitBoxResizeDirection.Down; } + if (hoverRight) { dir |= CircuitBoxResizeDirection.Right; } + if (hoverLeft) { dir |= CircuitBoxResizeDirection.Left; } + + if (dir is CircuitBoxResizeDirection.None) { - if (!node.IsResizable) { continue; } - - const float borderSize = 32f; - - var rect = node.Rect; - RectangleF bottomBorder = new(rect.X, rect.Top, rect.Width, borderSize); - RectangleF rightBorder = new(rect.Right - borderSize, rect.Y, borderSize, rect.Height); - RectangleF leftBorder = new(rect.X, rect.Y, borderSize, rect.Height); - - bool hoverBottom = bottomBorder.Contains(cursorPos), - hoverRight = rightBorder.Contains(cursorPos), - hoverLeft = leftBorder.Contains(cursorPos); - - var dir = CircuitBoxResizeDirection.None; - - if (hoverBottom) { dir |= CircuitBoxResizeDirection.Down; } - if (hoverRight) { dir |= CircuitBoxResizeDirection.Right; } - if (hoverLeft) { dir |= CircuitBoxResizeDirection.Left; } - - if (dir is CircuitBoxResizeDirection.None) - { - continue; - } - - return Option.Some((dir, node)); + return Option.None; } - return Option.None; + return Option.Some((dir, node)); } /// @@ -281,14 +289,14 @@ namespace Barotrauma if (circuitBoxUi.Locked) { return; } bool isDragThresholdExceeded = Vector2.DistanceSquared(startClick, cursorPos) > dragTreshold * dragTreshold; - if (LastResizeAffectedNode.IsSome()) - { - IsResizing |= isDragThresholdExceeded; - } - else if (LastConnectorUnderCursor.IsSome()) + if (LastConnectorUnderCursor.IsSome()) { IsWiring |= isDragThresholdExceeded; } + else if (LastResizeAffectedNode.IsSome()) + { + IsResizing |= isDragThresholdExceeded; + } else { IsDragging |= isDragThresholdExceeded; diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxUI.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxUI.cs index a595e7410..17f1e9215 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxUI.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -350,6 +350,10 @@ namespace Barotrauma { component.Sprite.Draw(spriteBatch, PlayerInput.MousePosition); } + if (PlayerInput.PrimaryMouseButtonHeld() && MouseSnapshotHandler.LastConnectorUnderCursor.IsSome()) + { + CircuitBoxWire.SelectedWirePrefab.Sprite.Draw(spriteBatch, PlayerInput.MousePosition, CircuitBoxWire.SelectedWirePrefab.SpriteColor, scale: camera.Zoom); + } foreach (var c in CircuitBox.Components) { @@ -360,11 +364,11 @@ namespace Barotrauma { n.DrawHUD(spriteBatch, camera); } - + if (Locked) { LocalizedString lockedText = TextManager.Get("CircuitBoxLocked") - .Fallback(TextManager.Get("ConnectionLocked")); + .Fallback(TextManager.Get("ConnectionLocked"), useDefaultLanguageIfFound: false); Vector2 size = GUIStyle.LargeFont.MeasureString(lockedText); Vector2 pos = new Vector2(screenRect.Center.X - size.X / 2, screenRect.Top + screenRect.Height * 0.05f); @@ -579,9 +583,25 @@ namespace Barotrauma if (isMouseOn) { - if (CircuitBox.HeldComponent.IsNone() && PlayerInput.PrimaryMouseButtonDown()) + if (PlayerInput.PrimaryMouseButtonDown()) { - MouseSnapshotHandler.StartDragging(); + if (CircuitBox.HeldComponent.IsNone()) + { + MouseSnapshotHandler.StartDragging(); + } + else + { + MouseSnapshotHandler.ClearSnapshot(); + } + } + + if (PlayerInput.DoubleClicked() && MouseSnapshotHandler.FindWireUnderCursor(cursorPos).IsNone()) + { + var topmostNode = GetTopmostNode(MouseSnapshotHandler.FindNodesUnderCursor(cursorPos)); + if (topmostNode is CircuitBoxLabelNode label && circuitComponent is not null) + { + label.PromptEditText(circuitComponent); + } } if (PlayerInput.MidButtonHeld() || (PlayerInput.IsAltDown() && PlayerInput.PrimaryMouseButtonHeld())) @@ -629,6 +649,7 @@ namespace Barotrauma if (PlayerInput.PrimaryMouseButtonClicked()) { + bool selectedNode = false; if (MouseSnapshotHandler.IsResizing && MouseSnapshotHandler.LastResizeAffectedNode.TryUnwrap(out var r)) { var (dir, node) = r; @@ -647,7 +668,7 @@ namespace Barotrauma } else if (!MouseSnapshotHandler.IsWiring) { - TrySelectComponentsUnderCursor(); + selectedNode = TrySelectComponentsUnderCursor(); } } @@ -658,8 +679,15 @@ namespace Barotrauma CircuitBox.AddWire(one, two); } } - - CircuitBox.SelectWires(MouseSnapshotHandler.LastWireUnderCursor.TryUnwrap(out var wire) ? ImmutableArray.Create(wire) : ImmutableArray.Empty, !PlayerInput.IsShiftDown()); + + if (MouseSnapshotHandler.LastWireUnderCursor.TryUnwrap(out var wire) && !MouseSnapshotHandler.IsDragging && !selectedNode) + { + CircuitBox.SelectWires(ImmutableArray.Create(wire), !PlayerInput.IsShiftDown()); + } + else if (CircuitBox.Wires.Any(static wire => wire.IsSelectedByMe)) + { + CircuitBox.SelectWires(ImmutableArray.Empty, !PlayerInput.IsShiftDown()); + } CircuitBox.HeldComponent = Option.None; MouseSnapshotHandler.EndDragging(); @@ -732,11 +760,17 @@ namespace Barotrauma } } - private void TrySelectComponentsUnderCursor() + private bool TrySelectComponentsUnderCursor() { CircuitBoxNode? foundNode = GetTopmostNode(MouseSnapshotHandler.GetLastComponentsUnderCursor()); + + if (foundNode is CircuitBoxLabelNode && MouseSnapshotHandler.LastWireUnderCursor.IsSome()) + { + foundNode = null; + } CircuitBox.SelectComponents(foundNode is null ? ImmutableArray.Empty : ImmutableArray.Create(foundNode), !PlayerInput.IsShiftDown()); + return foundNode is not null; } private void OpenContextMenu() @@ -767,17 +801,26 @@ namespace Barotrauma var editLabel = new ContextMenuOption(TextManager.Get("circuitboxeditlabel"), isEnabled: nodeOption is CircuitBoxLabelNode && !Locked, () => { - if (nodeOption is not CircuitBoxLabelNode label || circuitComponent is null) { return; } + if (circuitComponent is null) { return; } + if (nodeOption is not CircuitBoxLabelNode label) { return; } label.PromptEditText(circuitComponent); }); + var editConnections = new ContextMenuOption(TextManager.Get("circuitboxrenameconnections"), isEnabled: nodeOption is CircuitBoxInputOutputNode && !Locked, () => + { + if (circuitComponent is null) { return; } + if (nodeOption is not CircuitBoxInputOutputNode io) { return; } + + io.PromptEdit(circuitComponent); + }); + var addLabelOption = new ContextMenuOption(TextManager.Get("circuitboxaddlabel"), isEnabled: !Locked, () => { CircuitBox.AddLabel(cursorPos); }); - ContextMenuOption[] allOptions = { addLabelOption, editLabel, option }; + ContextMenuOption[] allOptions = { addLabelOption, editLabel, editConnections, option }; // show component name in the header to better indicate what is about to be deleted if (nodeOption is CircuitBoxComponent comp) diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index d6e738888..d6400ca91 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -114,7 +114,7 @@ namespace Barotrauma textBox.MaxTextLength = maxLength; textBox.OnKeyHit += (sender, key) => { - if (key != Keys.Tab) + if (key != Keys.Tab && key != Keys.LeftShift) { ResetAutoComplete(); } @@ -181,7 +181,8 @@ namespace Barotrauma if (PlayerInput.KeyHit(Keys.Tab) && !textBox.IsIMEActive) { - textBox.Text = AutoComplete(textBox.Text, increment: string.IsNullOrEmpty(currentAutoCompletedCommand) ? 0 : 1 ); + int increment = PlayerInput.KeyDown(Keys.LeftShift) ? -1 : 1; + textBox.Text = AutoComplete(textBox.Text, increment: string.IsNullOrEmpty(currentAutoCompletedCommand) ? 0 : increment ); } if (PlayerInput.KeyDown(Keys.LeftControl) || PlayerInput.KeyDown(Keys.RightControl)) @@ -634,6 +635,20 @@ namespace Barotrauma { NewMessage("Ready checks can only be commenced in multiplayer.", Color.Red); })); + + commands.Add(new Command("setsalary", "setsalary [0-100] [character/default]: Sets the salary of a certain character or the default salary to a percentage.", (string[] args) => + { + ThrowError("This command can only be used in multiplayer campaign."); + }, isCheat: true, getValidArgs: () => + { + return new[] + { + new[]{ "0", "100" }, + Enumerable.Union( + new string[] { "default" }, + Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n)).ToArray(), + }; + })); commands.Add(new Command("bindkey", "bindkey [key] [command]: Binds a key to a command.", (string[] args) => { @@ -793,6 +808,7 @@ namespace Barotrauma AssignRelayToServer("money", true); AssignRelayToServer("showmoney", true); AssignRelayToServer("setskill", true); + AssignRelayToServer("setsalary", true); AssignRelayToServer("readycheck", true); commands.Add(new Command("debugjobassignment", "", (string[] args) => { })); AssignRelayToServer("debugjobassignment", true); @@ -838,11 +854,8 @@ namespace Barotrauma AssignOnExecute("teleportcharacter|teleport", (string[] args) => { - Character tpCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args, false); - if (tpCharacter != null) - { - tpCharacter.TeleportTo(GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition)); - } + Vector2 cursorWorldPos = GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition); + TeleportCharacter(cursorWorldPos, Character.Controlled, args); }); AssignOnExecute("spawn|spawncharacter", (string[] args) => @@ -1425,6 +1438,9 @@ namespace Barotrauma AssignRelayToServer("water|editwater", false); AssignRelayToServer("fire|editfire", false); +#if DEBUG + AssignRelayToServer("debugvoip", true); +#endif commands.Add(new Command("mute", "mute [name]: Prevent the client from speaking to anyone through the voice chat. Using this command requires a permission from the server host.", null, @@ -2345,6 +2361,11 @@ namespace Barotrauma })); #if DEBUG + commands.Add(new Command("deathprompt", "Shows the death prompt for testing purposes.", (string[] args) => + { + DeathPrompt.Create(delay: 1.0f); + })); + commands.Add(new Command("listspamfilters", "Lists filters that are in the global spam filter.", (string[] args) => { if (!SpamServerFilters.GlobalSpamFilter.TryUnwrap(out var filter)) @@ -3093,12 +3114,12 @@ namespace Barotrauma { if (Screen.Selected == GameMain.GameScreen) { - ThrowError("Reloading the package while in GameScreen may break things; to do it anyway, type 'reloadcorepackage [name] force'"); + ThrowError("Reloading the package while in GameScreen may break things; to do it anyway, type 'reloadpackage [name] force'"); return; } if (Screen.Selected == GameMain.SubEditorScreen) { - ThrowError("Reloading the core package while in sub editor may break thingg; to do it anyway, type 'reloadcorepackage [name] force'"); + ThrowError("Reloading the core package while in sub editor may break things; to do it anyway, type 'reloadpackage [name] force'"); return; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AlienRuinMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/EliminateTargetsMission.cs similarity index 95% rename from Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AlienRuinMission.cs rename to Barotrauma/BarotraumaClient/ClientSource/Events/Missions/EliminateTargetsMission.cs index f4a737254..db730e374 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AlienRuinMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/EliminateTargetsMission.cs @@ -2,7 +2,7 @@ using Barotrauma.Networking; namespace Barotrauma { - partial class AlienRuinMission : Mission + partial class EliminateTargetsMission : Mission { public override bool DisplayAsCompleted => State > 0; public override bool DisplayAsFailed => false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs index 7f185b4d4..82b582124 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs @@ -403,9 +403,9 @@ namespace Barotrauma { senderName = (message.Type == ChatMessageType.Private ? "[PM] " : "") + message.SenderName; } - if (message.Sender?.Info?.Job != null) + if (message.SenderCharacter?.Info?.Job != null) { - senderColor = Color.Lerp(message.Sender.Info.Job.Prefab.UIColor, Color.White, 0.25f); + senderColor = Color.Lerp(message.SenderCharacter.Info.Job.Prefab.UIColor, Color.White, 0.25f); } var msgHolder = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.0f), chatBox.Content.RectTransform, Anchor.TopCenter), style: null, diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs index d43f789b7..4749ace03 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs @@ -14,6 +14,7 @@ namespace Barotrauma private readonly CampaignUI campaignUI; private readonly GUIComponent parentComponent; + private GUILayoutGroup pendingAndCrewGroup; private GUIListBox hireableList, pendingList, crewList; private GUIFrame characterPreviewFrame; private GUIDropDown sortingDropDown; @@ -24,7 +25,14 @@ namespace Barotrauma private PlayerBalanceElement? playerBalanceElement; private List PendingHires => campaign.Map?.CurrentLocation?.HireManager?.PendingHires; - private bool HasPermission => CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageHires); + + // Is the player hiring a new character for themselves instead of bots for the crew? + // The window can only be used for one of these purposes at the same time. + private static bool HiringNewCharacter => GameMain.NetworkMember?.ServerSettings is { RespawnMode: RespawnMode.Permadeath, IronmanMode: false } && + GameMain.Client?.CharacterInfo is { PermanentlyDead: true }; + + private static bool HasPermissionToHire => CampaignMode.AllowedToManageCampaign( + HiringNewCharacter ? ClientPermissions.ManageMoney : ClientPermissions.ManageHires); private Point resolutionWhenCreated; @@ -60,20 +68,20 @@ namespace Barotrauma RefreshCrewFrames(hireableList); RefreshCrewFrames(crewList); RefreshCrewFrames(pendingList); - if (clearAllButton != null) { clearAllButton.Enabled = HasPermission; } + if (clearAllButton != null) { clearAllButton.Enabled = HasPermissionToHire; } } private void RefreshCrewFrames(GUIListBox listBox) { if (listBox == null) { return; } - listBox.CanBeFocused = HasPermission; + listBox.CanBeFocused = HasPermissionToHire; foreach (GUIComponent child in listBox.Content.Children) { if (child.FindChild(c => c is GUIButton && c.UserData is CharacterInfo, true) is GUIButton buyButton) { CharacterInfo characterInfo = buyButton.UserData as CharacterInfo; - bool enoughReputationToHire = EnoughReputationToHire(characterInfo); - buyButton.Enabled = HasPermission && enoughReputationToHire; + bool enougMoneyToHire = !HiringNewCharacter || campaign.CanAfford(HireManager.GetSalaryFor(characterInfo)); + buyButton.Enabled = HasPermissionToHire && EnoughReputationToHire(characterInfo) && enougMoneyToHire; foreach (GUITextBlock text in child.GetAllChildren()) { text.TextColor = new Color(text.TextColor, buyButton.Enabled ? 1.0f : 0.6f); @@ -174,7 +182,7 @@ namespace Barotrauma playerBalanceElement = CampaignUI.AddBalanceElement(pendingAndCrewMainGroup, new Vector2(1.0f, 0.75f / 14.0f)); - var pendingAndCrewGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), anchor: Anchor.Center, + 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(panelMaxWidth, campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Rect.Height) @@ -222,7 +230,7 @@ namespace Barotrauma { ClickSound = GUISoundType.Cart, ForceUpperCase = ForceUpperCase.Yes, - Enabled = HasPermission, + Enabled = HasPermissionToHire, OnClicked = (b, o) => RemoveAllPendingHires() }; GUITextBlock.AutoScaleAndNormalize(validateHiresButton.TextBlock, clearAllButton.TextBlock); @@ -277,7 +285,7 @@ namespace Barotrauma { foreach (CharacterInfo c in hireableCharacters) { - if (c == null) { continue; } + if (c == null || PendingHires.Contains(c)) { continue; } CreateCharacterFrame(c, hireableList); } } @@ -289,8 +297,8 @@ namespace Barotrauma { HireManager hireManager = location.HireManager; if (hireManager == null) { return; } - int hireVal = hireManager.AvailableCharacters.Aggregate(0, (curr, hire) => curr + hire.GetIdentifier()); - int newVal = availableHires.Aggregate(0, (curr, hire) => curr + hire.GetIdentifier()); + int hireVal = hireManager.AvailableCharacters.Aggregate(0, (curr, hire) => curr + hire.ID); + int newVal = availableHires.Aggregate(0, (curr, hire) => curr + hire.ID); if (hireVal != newVal) { location.HireManager.AvailableCharacters = availableHires; @@ -371,7 +379,7 @@ namespace Barotrauma } } - private void CreateCharacterFrame(CharacterInfo characterInfo, GUIListBox listBox) + public GUIComponent CreateCharacterFrame(CharacterInfo characterInfo, GUIListBox listBox, bool hideSalary = false) { Skill skill = null; Color? jobColor = null; @@ -442,33 +450,41 @@ namespace Barotrauma CanBeFocused = false }; } - - if (listBox != crewList) + + if (!hideSalary) { - new GUITextBlock(new RectTransform(new Vector2(width, 1.0f), mainGroup.RectTransform), - TextManager.FormatCurrency(HireManager.GetSalaryFor(characterInfo)), - textAlignment: Alignment.Center) + if (listBox != crewList) { - 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 }; + new GUITextBlock(new RectTransform(new Vector2(width, 1.0f), mainGroup.RectTransform), + TextManager.FormatCurrency(HireManager.GetSalaryFor(characterInfo)), + 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) { var hireButton = new GUIButton(new RectTransform(new Vector2(width, 0.9f), mainGroup.RectTransform), style: "CrewManagementAddButton") { + ToolTip = TextManager.Get("hirebutton"), ClickSound = GUISoundType.Cart, UserData = characterInfo, - Enabled = CanHire(characterInfo), + Enabled = CanHire(characterInfo) && !HiringNewCharacter, OnClicked = (b, o) => AddPendingHire(o as CharacterInfo) }; hireButton.OnAddedToGUIUpdateList += (GUIComponent btn) => { + if (HiringNewCharacter) + { + return; + } if (PendingHires.Count + campaign.CrewManager.GetCharacterInfos().Count() >= CrewManager.MaxCrewSize) { if (btn.Enabled) @@ -483,6 +499,41 @@ namespace Barotrauma btn.Enabled = CanHire(characterInfo); } }; + + if (HiringNewCharacter) + { + bool canHire = CanHire(characterInfo) && campaign.CanAfford(HireManager.GetSalaryFor(characterInfo)); + var takeoverButton = new GUIButton(new RectTransform(new Vector2(width, 0.9f), mainGroup.RectTransform), style: "CrewManagementTakeControlButton") + { + ToolTip = canHire ? TextManager.Get("hireandtakecontrol") : TextManager.Get("hireandtakecontroldisabled"), + ClickSound = GUISoundType.ConfirmTransaction, + UserData = characterInfo, + Enabled = canHire, + OnClicked = (b, o) => + { + if (GameMain.Client is not GameClient gameClient) + { + return false; + } + Client client = gameClient.ConnectedClients.FirstOrDefault(c => c.SessionId == gameClient.SessionId); + if (!campaign.TryPurchase(client, HireManager.GetSalaryFor(characterInfo))) + { + return false; + } + gameClient.SendTakeOverBotRequest(characterInfo); + needsHireableRefresh = true; + campaign.ShowCampaignUI = false; + return true; + } + }; + takeoverButton.OnAddedToGUIUpdateList += (GUIComponent btn) => + { + bool canHireCurrently = HiringNewCharacter && CanHire(characterInfo) && campaign.CanAfford(HireManager.GetSalaryFor(characterInfo)); + btn.ToolTip = TextManager.Get(canHireCurrently ? "hireandtakecontrol" : "hireandtakecontroldisabled"); + btn.Visible = GameMain.GameSession is { AllowHrManagerBotTakeover: true }; + btn.Enabled = canHireCurrently; + }; + } } else if (listBox == pendingList) { @@ -501,7 +552,7 @@ namespace Barotrauma { UserData = characterInfo, //can't fire if there's only one character in the crew - Enabled = currentCrew.Contains(characterInfo) && currentCrew.Count() > 1 && HasPermission, + Enabled = currentCrew.Contains(characterInfo) && currentCrew.Count() > 1 && HasPermissionToHire, OnClicked = (btn, obj) => { var confirmDialog = new GUIMessageBox( @@ -534,11 +585,13 @@ namespace Barotrauma }; } - bool CanHire(CharacterInfo characterInfo) + bool CanHire(CharacterInfo thisCharacterInfo) { - if (!HasPermission) { return false; } - return EnoughReputationToHire(characterInfo); + if (!HasPermissionToHire) { return false; } + return EnoughReputationToHire(thisCharacterInfo); } + + return frame; } private bool EnoughReputationToHire(CharacterInfo characterInfo) @@ -709,10 +762,10 @@ namespace Barotrauma totalBlock.Text = TextManager.FormatCurrency(total); bool enoughMoney = campaign == null || campaign.CanAfford(total); totalBlock.TextColor = enoughMoney ? Color.White : Color.Red; - validateHiresButton.Enabled = enoughMoney && HasPermission && pendingList.Content.RectTransform.Children.Any(); + validateHiresButton.Enabled = enoughMoney && HasPermissionToHire && pendingList.Content.RectTransform.Children.Any(); } - public bool ValidateHires(List hires, bool takeMoney = true, bool createNetworkEvent = false) + public bool ValidateHires(List hires, bool takeMoney = true, bool createNetworkEvent = false, bool createNotification = true) { if (hires == null || hires.None()) { return false; } @@ -750,11 +803,14 @@ namespace Barotrauma { UpdateLocationView(campaign.Map.CurrentLocation, true); SelectCharacter(null, null, null); - var dialog = new GUIMessageBox( - TextManager.Get("newcrewmembers"), - TextManager.GetWithVariable("crewhiredmessage", "[location]", campaignUI?.Campaign?.Map?.CurrentLocation?.DisplayName), - new LocalizedString[] { TextManager.Get("Ok") }); - dialog.Buttons[0].OnClicked += dialog.Close; + if (createNotification) + { + var dialog = new GUIMessageBox( + TextManager.Get("newcrewmembers"), + TextManager.GetWithVariable("crewhiredmessage", "[location]", campaignUI?.Campaign?.Map?.CurrentLocation?.DisplayName), + new LocalizedString[] { TextManager.Get("Ok") }); + dialog.Buttons[0].OnClicked += dialog.Close; + } } if (createNetworkEvent) @@ -767,7 +823,7 @@ namespace Barotrauma private bool CreateRenamingComponent(GUIButton button, object userData) { - if (!HasPermission || userData is not CharacterInfo characterInfo) { return false; } + if (!HasPermissionToHire || userData is not 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) @@ -875,6 +931,9 @@ namespace Barotrauma { playerBalanceElement = CampaignUI.UpdateBalanceElement(playerBalanceElement); } + + // When showing this window to someone hiring a new character, the right side panels aren't needed + pendingAndCrewGroup.Visible = !HiringNewCharacter; if (needsHireableRefresh) { @@ -949,7 +1008,7 @@ namespace Barotrauma } } - public void SetPendingHires(List characterInfos, Location location) + public void SetPendingHires(List characterInfos, Location location) { List oldHires = PendingHires.ToList(); foreach (CharacterInfo pendingHire in oldHires) @@ -957,9 +1016,9 @@ namespace Barotrauma RemovePendingHire(pendingHire, createNetworkMessage: false); } PendingHires.Clear(); - foreach (int identifier in characterInfos) + foreach (UInt16 identifier in characterInfos) { - CharacterInfo match = location.HireManager.AvailableCharacters.Find(info => info.GetIdentifierUsingOriginalName() == identifier); + CharacterInfo match = location.HireManager.AvailableCharacters.Find(info => info.ID == identifier); if (match != null) { AddPendingHire(match, createNetworkMessage: false); @@ -992,7 +1051,7 @@ namespace Barotrauma msg.WriteUInt16((ushort)PendingHires.Count); foreach (CharacterInfo pendingHire in PendingHires) { - msg.WriteInt32(pendingHire.GetIdentifierUsingOriginalName()); + msg.WriteUInt16(pendingHire.ID); } } @@ -1002,17 +1061,16 @@ namespace Barotrauma msg.WriteBoolean(validRenaming); if (validRenaming) { - int identifier = renameCharacter.info.GetIdentifierUsingOriginalName(); - msg.WriteInt32(identifier); + msg.WriteUInt16(renameCharacter.info.ID); msg.WriteString(renameCharacter.newName); - bool existingCrewMember = campaign.CrewManager?.GetCharacterInfos().Any(ci => ci.GetIdentifierUsingOriginalName() == identifier) ?? false; + bool existingCrewMember = campaign.CrewManager?.GetCharacterInfos().Any(ci => ci.ID == renameCharacter.info.ID) ?? false; msg.WriteBoolean(existingCrewMember); } msg.WriteBoolean(firedCharacter != null); if (firedCharacter != null) { - msg.WriteInt32(firedCharacter.GetIdentifier()); + msg.WriteUInt16(firedCharacter.ID); } GameMain.Client.ClientPeer?.Send(msg, DeliveryMethod.Reliable); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/DeathPrompt.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/DeathPrompt.cs new file mode 100644 index 000000000..3042c060f --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/DeathPrompt.cs @@ -0,0 +1,529 @@ +#nullable enable +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma; + +internal class DeathPrompt +{ + private static CoroutineHandle? createPromptCoroutine; + + private GUIComponent? skillPanel; + private GUIComponent? newCharacterPanel; + private GUIComponent? takeOverBotPanel; + + private GUIComponent? content; + + public static GUIComponent? takeOverBotPanelFrame; + + /// + /// Private constructor, because these should only be created using the Show method + /// + private DeathPrompt() { } + + public static void Create(float delay) + { + if (!RespawnManager.UseDeathPrompt) { return; } + if (GameMain.GameSession.DeathPrompt != null) + { + return; + } + + if (createPromptCoroutine != null && CoroutineManager.IsCoroutineRunning(createPromptCoroutine)) { return; } + if ((GameMain.GameSession is not { IsRunning: true })) { return; } + + createPromptCoroutine = CoroutineManager.Invoke(() => + { + if (GameMain.GameSession != null) + { + GameMain.GameSession.DeathPrompt = new DeathPrompt(); + GameMain.GameSession.DeathPrompt.CreatePrompt(); + SoundPlayer.OverrideMusicType = "crewdead".ToIdentifier(); + SoundPlayer.OverrideMusicDuration = 25.0f; + } + }, delay); + } + + public void AddToGUIUpdateList() + { + content?.AddToGUIUpdateList(); + } + + private void CreatePrompt() + { + const float FadeInInterval = 1.0f; + const float FadeInDuration = 1.0f; + + bool permadeath = GameMain.NetworkMember is { ServerSettings.RespawnMode: RespawnMode.Permadeath }; + bool ironman = GameMain.NetworkMember is { ServerSettings: { RespawnMode: RespawnMode.Permadeath, IronmanMode: true } }; + + var background = new GUICustomComponent(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), onDraw: DrawBackground) + { + UserData = this + }; + background.FadeIn(wait: 0, duration: 5.0f); + + var foreground = new GUIImage(new RectTransform(new Vector2(1.0f, GUI.RelativeHorizontalAspectRatio), background.RectTransform, Anchor.BottomCenter) { AbsoluteOffset = new Point(0, GUI.IntScale(-20)) }, "DeathScreenForeground") + { + Color = Color.White + }; + foreground.FadeIn(wait: 0, duration: 5.0f); + foreground.Pulsate(startScale: Vector2.One, Vector2.One * 0.8f, duration: 25.0f); + + var frame = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.3f), background.RectTransform, Anchor.Center)) + { + UserData = this + }; + frame.FadeIn(wait: 0, duration: FadeInDuration); + + new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.1f), background.RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.2f) }, string.Empty, font: GUIStyle.LargeFont, textAlignment: Alignment.TopCenter) + { + TextGetter = () => + { + return GameMain.Client.EndRoundTimeRemaining > 0.0f ? + TextManager.GetWithVariable("endinground", "[time]", ToolBox.SecondsToReadableTime(GameMain.Client.EndRoundTimeRemaining)) + .Fallback(ToolBox.SecondsToReadableTime(GameMain.Client.EndRoundTimeRemaining), useDefaultLanguageIfFound: false) : + string.Empty; + } + }; + + var content = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.8f), frame.RectTransform, Anchor.Center)) + { + Stretch = true, + RelativeSpacing = 0.05f + }; + + //"you have died" header + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), TextManager.Get("deathprompt.header"), font: GUIStyle.LargeFont, textAlignment: Alignment.Center) + .FadeIn(wait: 0, duration: FadeInDuration); + + var causeOfDeath = GameMain.Client?.Character?.CauseOfDeath; + if (causeOfDeath != null && causeOfDeath.Type != CauseOfDeathType.Unknown) + { + var causeOfDeathDescription = causeOfDeath.Affliction != null ? + causeOfDeath.Affliction.SelfCauseOfDeathDescription : + TextManager.Get("Self_CauseOfDeathDescription." + causeOfDeath.Type.ToString(), "Self_CauseOfDeathDescription.Damage"); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), causeOfDeathDescription) + .FadeIn(wait: FadeInInterval * 2, duration: FadeInDuration); + } + + if (permadeath) + { + if (ironman) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), + TextManager.Get("deathprompt.permadeathnotification") + "\n\n" + TextManager.Get("deathprompt.ironmanexplanation"), wrap: true) + .FadeIn(wait: FadeInInterval * 3, duration: FadeInDuration); + } + else + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), + TextManager.Get("deathprompt.permadeathnotification") + '\n' + TextManager.Get("deathprompt.takeoverbotexplanation"), wrap: true) + .FadeIn(wait: FadeInInterval * 3, duration: FadeInDuration); + } + } + else if (RespawnManager.SkillLossPercentageOnDeath > 0) + { + string skillLossAmount = ((int)RespawnManager.SkillLossPercentageOnDeath).ToString(); + string skillLossText = $"‖color: { XMLExtensions.ToStringHex(GUIStyle.Red)}‖{skillLossAmount}‖end‖"; + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), + RichString.Rich(TextManager.GetWithVariable("respawnskillpenalty", "[percentage]", skillLossText))) + .FadeIn(wait: FadeInInterval * 3, duration: FadeInDuration); + }; + + //"what do you want to do" buttons in the middle + //------------------------------------------------------------------------------------------------------- + + var decisionButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), content.RectTransform), isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.05f + }; + + if (ironman) + { + // The only option is to spectate + var buttonContainerMiddle = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), decisionButtonContainer.RectTransform), childAnchor: Anchor.Center); + new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonContainerMiddle.RectTransform), TextManager.Get("spectatebutton")) + { + OnClicked = (btn, userdata) => + { + GameMain.Client?.SendRespawnPromptResponse(waitForNextRoundRespawn: true); + Close(); + return true; + } + }.FadeIn(wait: FadeInInterval * 4, duration: FadeInDuration, alsoChildren: true); + } + else + { + var buttonContainerLeft = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), decisionButtonContainer.RectTransform)); + var buttonContainerRight = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), decisionButtonContainer.RectTransform)); + + // The default "I'll wait" button + new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), buttonContainerLeft.RectTransform), TextManager.Get("respawnquestionpromptwait")) + { + OnClicked = (btn, userdata) => + { + GameMain.Client?.SendRespawnPromptResponse(waitForNextRoundRespawn: true); + Close(); + return true; + } + }.FadeIn(wait: FadeInInterval * 4, duration: FadeInDuration, alsoChildren: true); + + if (permadeath) + { + if (GameMain.Client != null && GameMain.Client.ServerSettings.AllowBotTakeoverOnPermadeath) + { + new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), buttonContainerRight.RectTransform), TextManager.Get("deathprompt.takeoverbot")) + { + Enabled = false, + OnAddedToGUIUpdateList = (component) => + { + component.Enabled = GetAvailableBots().Any(); + }, + OnClicked = (btn, userdata) => + { + if (takeOverBotPanel == null) + { + CreateTakeOverBotPanel(frame, this); + } + else + { + takeOverBotPanel.Parent?.RemoveChild(takeOverBotPanel); + takeOverBotPanel = null; + } + return true; + } + }.FadeIn(wait: FadeInInterval * 4, duration: FadeInDuration, alsoChildren: true); + } + } + else + { + new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), buttonContainerRight.RectTransform), TextManager.Get("deathprompt.respawnnow")) + { + OnClicked = (btn, userdata) => + { + GameMain.Client?.SendRespawnPromptResponse(waitForNextRoundRespawn: false); + Close(); + return true; + }, + Enabled = GameMain.NetworkMember is { ServerSettings.RespawnMode: RespawnMode.MidRound } + }.FadeIn(wait: FadeInInterval * 4, duration: FadeInDuration, alsoChildren: true); + } + + //"info buttons" at the bottom + //------------------------------------------------------------------------------------------------------- + + var infoButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.2f), content.RectTransform), childAnchor: Anchor.TopRight) + { + Stretch = true, + RelativeSpacing = 0.025f + }; + if (permadeath) + { + if (Level.IsLoadedFriendlyOutpost) + { + new GUIButton(new RectTransform(new Vector2(0.6f, 1.0f), infoButtonContainer.RectTransform), TextManager.Get("npctitle.hrmanager"), style: "GUIButtonSmall") + { + OnClicked = (btn, userdata) => + { + if (GameMain.GameSession?.Campaign is { } campaign) + { + campaign.ShowCampaignUI = true; + campaign.CampaignUI?.SelectTab(CampaignMode.InteractionType.Crew); + } + Close(); + return true; + } + }.FadeIn(wait: FadeInInterval * 5, duration: FadeInDuration, alsoChildren: true); + } + } + else + { + new GUIButton(new RectTransform(new Vector2(0.6f, 1.0f), infoButtonContainer.RectTransform), TextManager.Get("deathprompt.showskills"), style: "GUIButtonSmall") + { + OnClicked = (btn, userdata) => + { + if (skillPanel == null) + { + CreateSkillPanel(frame, GameMain.Client?.Character?.Info ?? GameMain.Client?.CharacterInfo); + } + else + { + skillPanel.Parent?.RemoveChild(skillPanel); + skillPanel = null; + } + return true; + } + }.FadeIn(wait: FadeInInterval * 5, duration: FadeInDuration, alsoChildren: true); + + new GUIButton(new RectTransform(new Vector2(0.6f, 1.0f), infoButtonContainer.RectTransform), TextManager.Get("deathprompt.newcharacter"), style: "GUIButtonSmall") + { + OnClicked = (btn, userdata) => + { + if (newCharacterPanel == null) + { + CreateNewCharacterPanel(frame); + } + else + { + newCharacterPanel.Parent?.RemoveChild(newCharacterPanel); + newCharacterPanel = null; + } + return true; + } + }.FadeIn(wait: FadeInInterval * 5, duration: FadeInDuration, alsoChildren: true); + } + } + + //TODO + /*new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), infoButtonContainer.RectTransform), "Respawn settings", style: "GUIButtonSmall") + { + OnClicked = (btn, userdata) => + { + return true; + } + }.FadeIn(wait: FadeInInterval * 5, duration: FadeInDuration, alsoChildren: true);*/ + + this.content = background; + } + + private void CreateSkillPanel(GUIComponent parent, CharacterInfo? characterInfo) + { + if (characterInfo == null) { return; } + var frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.0f), parent.RectTransform, Anchor.CenterRight, Pivot.CenterLeft)); + + var content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.8f), frame.RectTransform, Anchor.Center), isHorizontal: true) + { + Stretch = true + }; + + var leftColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), content.RectTransform)) + { + RelativeSpacing = 0.05f + }; + var middleColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1.0f), content.RectTransform)) + { + RelativeSpacing = 0.05f + }; + var rightColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1.0f), content.RectTransform)) + { + RelativeSpacing = 0.05f + }; + + var leftHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), leftColumn.RectTransform), TextManager.Get("Skills"), font: GUIStyle.SubHeadingFont, textColor: GUIStyle.TextColorBright); + var middleHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), middleColumn.RectTransform), TextManager.Get("deathprompt.SkillsLostHeader"), font: GUIStyle.SubHeadingFont, textColor: GUIStyle.TextColorBright); + var rightHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), rightColumn.RectTransform), TextManager.Get("deathprompt.respawnnow"), font: GUIStyle.SubHeadingFont, textColor: GUIStyle.TextColorBright); + + GUITextBlock.AutoScaleAndNormalize(leftHeader, middleHeader, rightHeader); + + foreach (var skill in characterInfo.Job.GetSkills().OrderByDescending(s => s.Level)) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), leftColumn.RectTransform), skill.DisplayName); + + int previousSkill = (int)skill.HighestLevelDuringRound; + int reducedSkill = (int)RespawnManager.GetReducedSkill(characterInfo, skill, RespawnManager.SkillLossPercentageOnDeath); + int reducedSkillOnImmediateRespawn = (int)RespawnManager.GetReducedSkill(characterInfo, skill, RespawnManager.SkillLossPercentageOnImmediateRespawn, currentSkillLevel: reducedSkill); + + int skillLoss = reducedSkill - previousSkill; + int skillLossOnImmediateRespawn = reducedSkillOnImmediateRespawn - previousSkill; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), middleColumn.RectTransform), + RichString.Rich($"{reducedSkill} (‖color:{XMLExtensions.ToStringHex(GUIStyle.Red)}‖{skillLoss}‖end‖)")); + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), rightColumn.RectTransform), + RichString.Rich($"{reducedSkillOnImmediateRespawn} (‖color:{XMLExtensions.ToStringHex(GUIStyle.Red)}‖{skillLossOnImmediateRespawn}‖end‖)")); + } + + new GUIButton(new RectTransform(new Vector2(1.0f, 0.15f), leftColumn.RectTransform, Anchor.BottomLeft), TextManager.Get("Close"), style: "GUIButtonSmall") + { + IgnoreLayoutGroups = true, + OnClicked = (btn, userdata) => + { + frame.Parent?.RemoveChild(frame); + skillPanel = null; + return true; + } + }; + + skillPanel = frame; + } + + private void CreateNewCharacterPanel(GUIComponent parent) + { + var frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.5f), parent.RectTransform, Anchor.CenterRight, Pivot.CenterLeft)); + + var content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), frame.RectTransform, Anchor.Center), isHorizontal: false) + { + Stretch = true, + RelativeSpacing = 0.05f + }; + GameMain.NetLobbyScreen.CreatePlayerFrame(content, alwaysAllowEditing: true, createPendingText: false); + + var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.15f), content.RectTransform), isHorizontal: true) + { + RelativeSpacing = 0.05f, + Stretch = true + }; + + new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonContainer.RectTransform, Anchor.BottomLeft), TextManager.Get("Cancel"), style: "GUIButtonSmall") + { + OnClicked = (btn, userdata) => + { + frame.Parent?.RemoveChild(frame); + newCharacterPanel = null; + return true; + } + }; + new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonContainer.RectTransform, Anchor.BottomLeft), TextManager.Get("ApplySettingsYes"), style: "GUIButtonSmall") + { + OnClicked = (btn, userdata) => + { + GameMain.NetLobbyScreen.TryDiscardCampaignCharacter(onYes: () => + { + frame.Parent?.RemoveChild(frame); + newCharacterPanel = null; + }); + return true; + } + }; + + newCharacterPanel = frame; + } + + public static void CreateTakeOverBotPanel() + { + var panelHolder = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.3f), GUI.Canvas, Anchor.Center)); + var takeOverBotPanel = CreateTakeOverBotPanel(panelHolder, deathPrompt: null); + if (takeOverBotPanel != null) + { + takeOverBotPanel.RectTransform.SetPosition(Anchor.Center); + GUIMessageBox.MessageBoxes.Add(panelHolder); + } + } + + /// + /// Static because the "take over bot" panel can be accessed outside the death prompt too + /// + private static GUIComponent? CreateTakeOverBotPanel(GUIComponent parent, DeathPrompt? deathPrompt) + { + if (GameMain.GameSession?.CrewManager == null) { return null; } + if (GameMain.GameSession?.Campaign is not MultiPlayerCampaign campaign) { return null; } + + if (campaign.CampaignUI == null) { campaign.InitCampaignUI(); } + + var frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.0f), parent.RectTransform, Anchor.CenterRight, Pivot.CenterLeft)); + takeOverBotPanelFrame = frame; + + var content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), frame.RectTransform, Anchor.Center), isHorizontal: false) + { + Stretch = true, + RelativeSpacing = 0.05f + }; + + var botList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.9f), content.RectTransform)); + foreach (CharacterInfo c in GetAvailableBots()) + { + var characterFrame = campaign.CampaignUI?.CrewManagement.CreateCharacterFrame(c, botList, hideSalary: true); + if (characterFrame != null) + { + characterFrame.UserData = c; + } + } + botList.UpdateScrollBarSize(); + + var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.15f), content.RectTransform), isHorizontal: true) + { + RelativeSpacing = 0.05f, + Stretch = true + }; + + new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonContainer.RectTransform, Anchor.BottomLeft), TextManager.Get("Cancel"), style: "GUIButtonSmall") + { + OnClicked = (btn, userdata) => + { + GUIMessageBox.MessageBoxes.Remove(frame.Parent); + frame.Parent?.RemoveChild(frame); + if (deathPrompt != null) + { + deathPrompt.takeOverBotPanel = null; + } + return true; + } + }; + new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonContainer.RectTransform, Anchor.BottomLeft), TextManager.Get("inputtype.select"), style: "GUIButtonSmall") + { + Enabled = false, + OnAddedToGUIUpdateList = (component) => + { + component.Enabled = botList.SelectedData is CharacterInfo; + }, + OnClicked = (btn, userdata) => + { + if (botList.SelectedData is CharacterInfo selectedCharacter && GameMain.Client is GameClient client) + { + client.SendTakeOverBotRequest(selectedCharacter); + GUIMessageBox.MessageBoxes.Remove(frame.Parent); + deathPrompt?.Close(); + return true; + } + else + { + DebugConsole.ThrowError($"Conditions for sending bot takeover request not met"); + return false; + } + } + }; + if (deathPrompt != null) + { + deathPrompt.takeOverBotPanel = frame; + } + return frame; + } + + private static IEnumerable GetAvailableBots() + { + if (GameMain.GameSession?.CrewManager is { } crewManager) + { + return crewManager.GetCharacterInfos().Where(c => + /*either an alive bot */ + c is { Character.IsBot: true, Character.IsDead: false } || + /* or a newly hired bot that hasn't spawned yet */ + (c.IsNewHire && c.Character == null)); + } + else + { + return Enumerable.Empty(); + } + } + + private void DrawBackground(SpriteBatch spriteBatch, GUICustomComponent guiCustomComponent) + { + var background = GUIStyle.GetComponentStyle("DeathScreenBackground"); + if (background != null) + { + GUI.DrawBackgroundSprite(spriteBatch, background.GetDefaultSprite(), Color.White * (guiCustomComponent.Color.A / 255.0f)); + } + } + + public void Close() + { + if (GameMain.GameSession != null) + { + GameMain.GameSession.DeathPrompt = null; + } + } + + public static void CloseBotPanel() + { + if (takeOverBotPanelFrame is GUIComponent frame) + { + GUIMessageBox.MessageBoxes.Remove(frame.Parent); + frame.Parent?.RemoveChild(frame); + } + takeOverBotPanelFrame = null; + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index a9cb78909..e03511a3e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -717,9 +717,9 @@ namespace Barotrauma private static readonly Queue removals = new Queue(); private static readonly Queue additions = new Queue(); // A helpers list for all elements that have a draw order less than 0. - private static readonly List first = new List(); + private static readonly List firstAdditions = new List(); // A helper list for all elements that have a draw order greater than 0. - private static readonly List last = new List(); + private static readonly List lastAdditions = new List(); /// /// Adds the component on the addition queue. @@ -737,11 +737,11 @@ namespace Barotrauma if (!component.Visible) { return; } if (component.UpdateOrder < 0) { - first.Add(component); + firstAdditions.Add(component); } else if (component.UpdateOrder > 0) { - last.Add(component); + lastAdditions.Add(component); } else { @@ -800,9 +800,9 @@ namespace Barotrauma RemoveFromUpdateList(component); } } - ProcessHelperList(first); + ProcessHelperList(firstAdditions); ProcessAdditions(); - ProcessHelperList(last); + ProcessHelperList(lastAdditions); ProcessRemovals(); } } @@ -897,7 +897,7 @@ namespace Barotrauma public static IEnumerable GetAdditions() { - return additions; + return additions.Union(firstAdditions).Union(lastAdditions); } #endregion @@ -2171,6 +2171,28 @@ namespace Barotrauma return frame; } + public static GUITextBox CreateTextBoxWithPlaceholder(RectTransform rectT, string text, LocalizedString placeholder) + { + var holder = new GUIFrame(rectT, style: null); + var textBox = new GUITextBox(new RectTransform(Vector2.One, holder.RectTransform, Anchor.CenterLeft), text, createClearButton: false); + var placeholderElement = new GUITextBlock(new RectTransform(Vector2.One, holder.RectTransform, Anchor.CenterLeft), + textColor: Color.DarkGray * 0.6f, + text: placeholder, + textAlignment: Alignment.CenterLeft) + { + CanBeFocused = false + }; + + new GUICustomComponent(new RectTransform(Vector2.Zero, holder.RectTransform), + onUpdate: delegate { placeholderElement.RectTransform.NonScaledSize = textBox.Frame.RectTransform.NonScaledSize; }); + + textBox.OnSelected += delegate { placeholderElement.Visible = false; }; + textBox.OnDeselected += delegate { placeholderElement.Visible = textBox.Text.IsNullOrWhiteSpace(); }; + + placeholderElement.Visible = string.IsNullOrWhiteSpace(text); + return textBox; + } + public static void NotifyPrompt(LocalizedString header, LocalizedString body) { GUIMessageBox msgBox = new GUIMessageBox(header, body, new[] { TextManager.Get("Ok") }, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index 0be5b3a17..8f58c3c11 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -815,7 +815,8 @@ namespace Barotrauma protected virtual void SetAlpha(float a) { color = new Color(color.R / 255.0f, color.G / 255.0f, color.B / 255.0f, a); - hoverColor = new Color(hoverColor.R / 255.0f, hoverColor.G / 255.0f, hoverColor.B / 255.0f, a);; + hoverColor = new Color(hoverColor.R / 255.0f, hoverColor.G / 255.0f, hoverColor.B / 255.0f, a); + disabledColor = new Color(disabledColor.R / 255.0f, disabledColor.G / 255.0f, disabledColor.B / 255.0f, a); } public virtual void Flash(Color? color = null, float flashDuration = 1.5f, bool useRectangleFlash = false, bool useCircularFlash = false, Vector2? flashRectInflate = null) @@ -835,15 +836,29 @@ namespace Barotrauma flashColor = (color == null) ? GUIStyle.Red : (Color)color; } - public void FadeOut(float duration, bool removeAfter, float wait = 0.0f, Action onRemove = null) + public void FadeOut(float duration, bool removeAfter, float wait = 0.0f, Action onRemove = null, bool alsoChildren = false) { CoroutineManager.StartCoroutine(LerpAlpha(0.0f, duration, removeAfter, wait, onRemove)); + if (alsoChildren) + { + foreach (var child in Children) + { + child.FadeOut(duration, removeAfter, wait, onRemove, alsoChildren); + } + } } - public void FadeIn(float wait, float duration) + public void FadeIn(float wait, float duration, bool alsoChildren = false) { SetAlpha(0.0f); CoroutineManager.StartCoroutine(LerpAlpha(1.0f, duration, false, wait)); + if (alsoChildren) + { + foreach (var child in Children) + { + child.FadeIn(wait, duration, alsoChildren); + } + } } public void SlideIn(float wait, float duration, int amount, SlideDirection direction) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs index ea44140e7..7f599ac11 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs @@ -201,7 +201,7 @@ namespace Barotrauma } else if (sprite?.Texture is { IsDisposed: false }) { - spriteBatch.Draw(sprite.Texture, Rect.Center.ToVector2(), sourceRect, currentColor * (currentColor.A / 255.0f), Rotation, origin, + spriteBatch.Draw(sprite.Texture, new Vector2(Rect.X + Rect.Width / 2.0f, Rect.Y + Rect.Height / 2.0f), sourceRect, currentColor * (currentColor.A / 255.0f), Rotation, origin, Scale, SpriteEffects, 0.0f); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs index c340cf1ad..45b64a5fb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs @@ -9,7 +9,6 @@ namespace Barotrauma { public class GUIMessageBox : GUIFrame { - #warning TODO: change this to List and fix incorrect uses of this list public readonly static List MessageBoxes = new List(); private static int DefaultWidth { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs index 019f31d92..c953565f5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs @@ -128,7 +128,7 @@ namespace Barotrauma } set { - if (MathUtils.NearlyEqual(value, floatValue)) { return; } + if (Math.Abs(value - floatValue) < 0.0001f && MathUtils.NearlyEqual(value, floatValue)) { return; } floatValue = value; ClampFloatValue(); float newValue = floatValue; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUISelectionCarousel.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUISelectionCarousel.cs index 069c50c80..af4f79ee9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUISelectionCarousel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUISelectionCarousel.cs @@ -1,5 +1,6 @@ #nullable enable +using System; using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Linq; @@ -25,6 +26,11 @@ namespace Barotrauma public delegate void OnValueChangedHandler(GUISelectionCarousel carousel); public OnValueChangedHandler? OnValueChanged; + + /// + /// Are there some conditions for selecting a particular element? + /// + public Func? ElementSelectionCondition { get; set; } public GUITextBlock TextBlock { get; private set; } @@ -89,35 +95,9 @@ namespace Barotrauma GUIStyle.Apply(TextBlock, "TextBlock", this); RightButton = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), layoutGroup.RectTransform), style: "GUIButtonToggleRight"); GUIStyle.Apply(RightButton, "RightButton", this); - - RightButton.OnClicked += (btn, userData) => - { - if (elements.Count < 2) { return false; } - if (SelectedElement == null) - { - SelectElement(elements.First()); - } - else - { - int newIndex = (elements.IndexOf(SelectedElement) + 1) % elements.Count; - SelectElement(elements[newIndex]); - } - return true; - }; - LeftButton.OnClicked += (btn, userData) => - { - if (elements.Count < 2) { return false; } - if (SelectedElement == null) - { - SelectElement(elements.First()); - } - else - { - int newIndex = MathUtils.PositiveModulo((elements.IndexOf(SelectedElement) - 1), elements.Count); - SelectElement(elements[newIndex]); - } - return true; - }; + + RightButton.OnClicked += (_, _) => SelectNextValidElement(); + LeftButton.OnClicked += (_, _) => SelectNextValidElement(directionLeft: true); if (newElements != null && newElements.Any()) { @@ -140,9 +120,11 @@ namespace Barotrauma SelectElement(null); return; } - if (elements.FirstOrDefault(e => value.Equals(e.value)) is { } element) + var matchingElement = elements.Where(e => value.Equals(e.value)) // selection is in the set of possible values + .FirstOrDefault(e => ElementSelectionCondition == null || ElementSelectionCondition(e.value)); // selection matches extra conditions, if any + if (matchingElement != null) { - SelectElement(element); + SelectElement(matchingElement); } } @@ -187,5 +169,42 @@ namespace Barotrauma SelectElement(newElement); } } + /// + /// Refresh the current selection, for example if there are conditions for which elements are valid, and those might have changed + /// + public void Refresh() + { + if (SelectedElement != null) + { + if (ElementSelectionCondition == null || ElementSelectionCondition(SelectedElement.value)) + { + return; + } + } + + SelectElement(elements.FirstOrDefault(e => ElementSelectionCondition == null || ElementSelectionCondition(e.value))); + } + + private bool SelectNextValidElement(bool directionLeft = false) + { + if (elements.Count < 2) { return false; } + + // Try to find a valid next/previous element + int currentIndex = SelectedElement == null ? -1 : elements.IndexOf(SelectedElement); + int newIndex = currentIndex; + for (int i = 0; i < elements.Count; i++) + { + newIndex = directionLeft ? MathUtils.PositiveModulo((newIndex - 1), elements.Count) : (newIndex + 1) % elements.Count; + if (ElementSelectionCondition == null || ElementSelectionCondition(elements[newIndex].value)) + { + SelectElement(elements[newIndex]); + return true; + } + } + + // No valid elements found + SelectElement(null); + return true; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs index 4aa48fd6d..6093924de 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs @@ -438,8 +438,11 @@ namespace Barotrauma protected override void SetAlpha(float a) { - // base.SetAlpha(a); - textColor = new Color(TextColor.R / 255.0f, TextColor.G / 255.0f, TextColor.B / 255.0f, a); + textColor = new Color(TextColor, a); + if (hoverTextColor.HasValue) + { + hoverTextColor = new Color(hoverTextColor.Value, a); + } } /// diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index cd303b3ce..372f1d499 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -2108,13 +2108,13 @@ namespace Barotrauma deliveryPrompt.Buttons[0].OnClicked = (btn, userdata) => { ConfirmPurchase(deliverImmediately: true); - deliveryPrompt.Close(); + deliveryPrompt?.Close(); return true; }; deliveryPrompt.Buttons[1].OnClicked = (btn, userdata) => { ConfirmPurchase(deliverImmediately: false); - deliveryPrompt.Close(); + deliveryPrompt?.Close(); return true; }; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs index 325b3d3f5..6fc380b99 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -742,8 +742,8 @@ namespace Barotrauma private (LocalizedString header, LocalizedString body) GetItemTransferWarningText() { - var header = TextManager.Get("itemtransferheader").Fallback("lowfuelheader"); - var body = TextManager.Get("itemtransferwarning").Fallback("lowfuelwarning"); + var header = TextManager.Get("itemtransferheader").Fallback("lowfuelheader", useDefaultLanguageIfFound: false); + var body = TextManager.Get("itemtransferwarning").Fallback("lowfuelwarning", useDefaultLanguageIfFound: false); return (header, body); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index b132d18f2..b71854188 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -320,7 +320,61 @@ namespace Barotrauma 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"); - GUITextBlock balanceText = new GUITextBlock(new RectTransform(Vector2.One, balanceFrame.RectTransform), string.Empty, textAlignment: Alignment.Right); + GUILayoutGroup salaryFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.66f, 1f), balanceFrame.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + + GUIScrollBar salaryScrollBar = null; + GUITextBlock salaryPercentage = null; + if (GameMain.GameSession?.GameMode is MultiPlayerCampaign) + { + float value = campaignMode.Bank.RewardDistribution; + GUITextBlock salaryText = new GUITextBlock(new RectTransform(new Vector2(0.25f, 1f), salaryFrame.RectTransform), TextManager.Get("defaultsalary"), textAlignment: Alignment.Center) + { + AutoScaleHorizontal = true + }; + salaryScrollBar = new GUIScrollBar(new RectTransform(new Vector2(0.4f, 1f), salaryFrame.RectTransform), barSize: 0.1f, style: "GUISlider") + { + Range = new Vector2(0, 1), + BarScrollValue = value / 100f, + Step = 0.01f, + BarSize = 0.1f, + }; + + salaryPercentage = new GUITextBlock(new RectTransform(new Vector2(0.15f, 1f), salaryFrame.RectTransform), "0", textAlignment: Alignment.Center) + { + Text = ValueToPercentage(RoundRewardDistribution(salaryScrollBar.BarScroll, salaryScrollBar.Step)) + }; + + salaryScrollBar.OnMoved = (scrollBar, value) => + { + salaryPercentage.Text = ValueToPercentage(RoundRewardDistribution(value, scrollBar.Step)); + return true; + }; + salaryScrollBar.OnReleased = (bar, scroll) => + { + int newRewardDistribution = RoundRewardDistribution(scroll, bar.Step); + SetRewardDistribution(Option.None, newRewardDistribution); + return true; + }; + + var resetButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), salaryFrame.RectTransform), TextManager.Get("ResetSalaries"), style: "GUIButtonSmall") + { + TextBlock = { AutoScaleHorizontal = true }, + ToolTip = TextManager.Get("resetsalaries.tooltip"), + OnClicked = (button, userData) => + { + GUI.AskForConfirmation(TextManager.Get("ResetSalaries"), TextManager.Get("ResetSalaries.Warning"), onConfirm: ResetRewardDistributions); + return true; + } + }; + + void UpdateSliderEnabled() + => salaryScrollBar.Enabled = resetButton.Enabled = CampaignMode.AllowedToManageWallets(); + UpdateSliderEnabled(); + + Identifier defaultSalaryEventIdentifier = "DefaultSalarySlider".ToIdentifier(); + GameMain.Client?.OnPermissionChanged?.RegisterOverwriteExisting(defaultSalaryEventIdentifier, _ => UpdateSliderEnabled()); + } + GUITextBlock balanceText = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1f), balanceFrame.RectTransform, Anchor.TopRight), string.Empty, textAlignment: Alignment.Right); if (GameMain.IsMultiplayer) { balanceText.ToolTip = TextManager.Get("bankdescription"); @@ -343,6 +397,13 @@ namespace Barotrauma { if (!e.Owner.IsNone()) { return; } SetBalanceText(balanceText, e.Wallet.Balance); + + if (salaryPercentage is not null && salaryScrollBar is not null) + { + float rewardDistribution = e.Wallet.RewardDistribution; + salaryScrollBar.BarScrollValue = rewardDistribution / 100f; + salaryPercentage.Text = ValueToPercentage(rewardDistribution); + } }); registeredEvents.Add(eventIdentifier); @@ -350,6 +411,9 @@ namespace Barotrauma { text.Text = TextManager.GetWithVariable("bankbalanceformat", "[money]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", balance)); } + + LocalizedString ValueToPercentage(float value) + => TextManager.GetWithVariable("percentageformat", "[value]", $"{(int)MathF.Round(value)}"); } var submarineButton = createTabButton(InfoFrameTab.Submarine, "submarine"); @@ -1037,11 +1101,10 @@ namespace Barotrauma { int newRewardDistribution = RoundRewardDistribution(scroll, bar.Step); if (newRewardDistribution == targetWallet.RewardDistribution) { return false; } - SetRewardDistribution(character, newRewardDistribution); + SetRewardDistribution(Option.Some(character), newRewardDistribution); return true; } }; - int RoundRewardDistribution(float scroll, float step) => (int)MathUtils.RoundTowardsClosest(scroll * 100, step * 100); SetRewardText(targetWallet.RewardDistribution, rewardBlock); @@ -1201,6 +1264,7 @@ namespace Barotrauma { moneyBlock.Text = TextManager.FormatCurrency(e.Info.Balance); salarySlider.BarScrollValue = e.Info.RewardDistribution / 100f; + SetRewardText(e.Info.RewardDistribution, rewardBlock); } UpdateAllInputs(); @@ -1311,20 +1375,29 @@ namespace Barotrauma transfer.Write(msg); GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable); } - - static void SetRewardDistribution(Character character, int newValue) - { - INetSerializableStruct transfer = new NetWalletSetSalaryUpdate - { - Target = character.ID, - NewRewardDistribution = newValue - }; - IWriteMessage msg = new WriteOnlyMessage().WithHeader(ClientPacketHeader.REWARD_DISTRIBUTION); - transfer.Write(msg); - GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable); - } } + static void SetRewardDistribution(Option character, int newValue) + { + INetSerializableStruct transfer = new NetWalletSetSalaryUpdate + { + Target = character.Select(c => c.ID), + NewRewardDistribution = newValue + }; + IWriteMessage msg = new WriteOnlyMessage().WithHeader(ClientPacketHeader.REWARD_DISTRIBUTION); + transfer.Write(msg); + GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable); + } + + static void ResetRewardDistributions() + { + IWriteMessage msg = new WriteOnlyMessage().WithHeader(ClientPacketHeader.RESET_REWARD_DISTRIBUTION); + GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable); + } + + static int RoundRewardDistribution(float scroll, float step) + => (int)MathUtils.RoundTowardsClosest(scroll * 100, step * 100); + private GUIComponent CreateClientInfoFrame(GUIFrame frame, Client client, Sprite permissionIcon = null) { GUIComponent paddedFrame; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs index aa8ab3f3a..f914358fa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs @@ -133,43 +133,47 @@ namespace Barotrauma GUIFrame containerFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.9f), characterLayout.RectTransform), style: null); GUILayoutGroup playerFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), containerFrame.RectTransform, Anchor.TopCenter)); GameMain.NetLobbyScreen.CreatePlayerFrame(playerFrame, alwaysAllowEditing: true, createPendingText: false); - - GUIButton newCharacterBox = new GUIButton(new RectTransform(new Vector2(0.5f, 0.2f), skillLayout.RectTransform, Anchor.BottomRight), - text: GameMain.NetLobbyScreen.CampaignCharacterDiscarded ? TextManager.Get("settings") : TextManager.Get("createnew"), style: "GUIButtonSmall") + + // TODO: What is CampaignCharacterDiscarded and can it be relevant in permadeath mode? + if (!GameMain.NetLobbyScreen.PermadeathMode) { - IgnoreLayoutGroups = false, - TextBlock = + GUIButton newCharacterBox = new GUIButton(new RectTransform(new Vector2(0.5f, 0.2f), skillLayout.RectTransform, Anchor.BottomRight), + text: GameMain.NetLobbyScreen.CampaignCharacterDiscarded ? TextManager.Get("settings") : TextManager.Get("createnew"), style: "GUIButtonSmall") { - AutoScaleHorizontal = true - } - }; - - newCharacterBox.OnClicked = (button, o) => - { - if (!GameMain.NetLobbyScreen.CampaignCharacterDiscarded) - { - GameMain.NetLobbyScreen.TryDiscardCampaignCharacter(() => + IgnoreLayoutGroups = false, + TextBlock = { - newCharacterBox.Text = TextManager.Get("settings"); - if (TabMenu.PendingChangesFrame != null) - { - NetLobbyScreen.CreateChangesPendingFrame(TabMenu.PendingChangesFrame); - } + AutoScaleHorizontal = true + } + }; - OpenMenu(); - }); - return true; - } - - OpenMenu(); - return true; - - void OpenMenu() + newCharacterBox.OnClicked = (button, o) => { - characterSettingsFrame!.Visible = true; - content.Visible = false; - } - }; + if (!GameMain.NetLobbyScreen.CampaignCharacterDiscarded) + { + GameMain.NetLobbyScreen.TryDiscardCampaignCharacter(() => + { + newCharacterBox.Text = TextManager.Get("settings"); + if (TabMenu.PendingChangesFrame != null) + { + NetLobbyScreen.CreateChangesPendingFrame(TabMenu.PendingChangesFrame); + } + + OpenMenu(); + }); + return true; + } + + OpenMenu(); + return true; + + void OpenMenu() + { + characterSettingsFrame!.Visible = true; + content.Visible = false; + } + }; + } GUILayoutGroup characterCloseButtonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), characterLayout.RectTransform), childAnchor: Anchor.BottomCenter); new GUIButton(new RectTransform(new Vector2(0.4f, 1f), characterCloseButtonLayout.RectTransform), TextManager.Get("ApplySettingsButton")) //TODO: Is this text appropriate for this circumstance for all languages? @@ -177,6 +181,7 @@ namespace Barotrauma OnClicked = (button, o) => { GameMain.Client?.SendCharacterInfo(GameMain.Client.PendingName); + GameMain.NetLobbyScreen.CampaignCharacterDiscarded = false; characterSettingsFrame.Visible = false; content.Visible = true; return true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index 192fbe7d0..ea933efde 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -773,7 +773,7 @@ namespace Barotrauma subItems ??= GetSubItems(); return subItems.Any(i => i.Prefab.SwappableItem != null && - !i.HiddenInGame && i.AllowSwapping && + !i.IsHidden && i.AllowSwapping && (i.Prefab.SwappableItem.CanBeBought || ItemPrefab.Prefabs.Any(ip => ip.SwappableItem?.ReplacementOnUninstall == i.Prefab.Identifier)) && Submarine.MainSub.IsEntityFoundOnThisSub(i, true) && category.ItemTags.Any(t => i.HasTag(t))); } @@ -876,7 +876,7 @@ namespace Barotrauma { parent.Content.ClearChildren(); currentUpgradeCategory = category; - var entitiesOnSub = submarine.GetItems(true).Where(i => submarine.IsEntityFoundOnThisSub(i, true) && !i.HiddenInGame && i.AllowSwapping && i.Prefab.SwappableItem != null && category.ItemTags.Any(t => i.HasTag(t))).ToList(); + var entitiesOnSub = submarine.GetItems(true).Where(i => submarine.IsEntityFoundOnThisSub(i, true) && !i.IsHidden && i.AllowSwapping && i.Prefab.SwappableItem != null && category.ItemTags.Any(t => i.HasTag(t))).ToList(); foreach (Item item in entitiesOnSub) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Widget.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Widget.cs index b1d25586c..9573a9bf1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Widget.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Widget.cs @@ -115,7 +115,6 @@ namespace Barotrauma if (IsMouseOver || (!RequireMouseOn && SelectedWidgets.Contains(this) && PlayerInput.PrimaryMouseButtonHeld())) { Hovered?.Invoke(); - System.Diagnostics.Debug.WriteLine("hovered"); if (RequireMouseOn || PlayerInput.PrimaryMouseButtonDown()) { if ((multiselect && !SelectedWidgets.Contains(this)) || SelectedWidgets.None()) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index ae9476740..fbfaca6a9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -1137,19 +1137,14 @@ namespace Barotrauma if (save) { GUI.SetSavingIndicatorState(true); + + GameSession.Campaign?.HandleSaveAndQuit(); if (GameSession.Submarine != null && !GameSession.Submarine.Removed) { GameSession.SubmarineInfo = new SubmarineInfo(GameSession.Submarine); } - if (GameSession.Campaign is CampaignMode campaign) - { - if (campaign is SinglePlayerCampaign spCampaign && Level.IsLoadedFriendlyOutpost) - { - spCampaign.UpdateStoreStock(); - } - GameSession.EventManager?.RegisterEventHistory(registerFinishedOnly: true); - campaign.End(); - } + GameSession.Campaign?.End(); + SaveUtil.SaveGame(GameSession.SavePath); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 68439bf0c..655f5f79f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -678,12 +678,12 @@ namespace Barotrauma /// /// Adds the message to the single player chatbox. /// - public void AddSinglePlayerChatMessage(LocalizedString senderName, LocalizedString text, ChatMessageType messageType, Character sender) + public void AddSinglePlayerChatMessage(LocalizedString senderName, LocalizedString text, ChatMessageType messageType, Entity sender) { AddSinglePlayerChatMessage(senderName.Value, text.Value, messageType, sender); } - public void AddSinglePlayerChatMessage(string senderName, string text, ChatMessageType messageType, Character sender) + public void AddSinglePlayerChatMessage(string senderName, string text, ChatMessageType messageType, Entity sender) { if (!IsSinglePlayer) { @@ -692,9 +692,13 @@ namespace Barotrauma } if (string.IsNullOrEmpty(text)) { return; } - if (sender != null) + if (sender is Character character) { - GameMain.GameSession.CrewManager.SetCharacterSpeaking(sender); + GameMain.GameSession.CrewManager?.SetCharacterSpeaking(character); + if (!character.IsBot) + { + character.TextChatVolume = 1f; + } } ChatBox.AddMessage(ChatMessage.Create(senderName, text, messageType, sender)); } @@ -708,9 +712,9 @@ namespace Barotrauma } if (string.IsNullOrEmpty(message.Text)) { return; } - if (message.Sender != null) + if (message.SenderCharacter != null) { - GameMain.GameSession.CrewManager.SetCharacterSpeaking(message.Sender); + GameMain.GameSession.CrewManager?.SetCharacterSpeaking(message.SenderCharacter); } ChatBox.AddMessage(message); } @@ -3688,6 +3692,9 @@ namespace Barotrauma crewList.ClearChildren(); } + /// + /// Saves the current crew. Note that this is client-only code (only used in the single player campaign) - saving in multiplayer is handled in the server-side code of . + /// public XElement Save(XElement parentElement) { var element = new XElement("crew"); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index 6571cebd7..591003e89 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -254,7 +254,7 @@ namespace Barotrauma buttonText = TextManager.Get("map"); } else if (prevCampaignUIAutoOpenType != availableTransition && - (availableTransition == TransitionType.ProgressToNextEmptyLocation || availableTransition == TransitionType.ReturnToPreviousEmptyLocation)) + availableTransition == TransitionType.ProgressToNextEmptyLocation) { 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 diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index f0b7616cb..eeeb558fd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -162,7 +162,7 @@ namespace Barotrauma }; } - private void InitCampaignUI() + public void InitCampaignUI() { campaignUIContainer = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: "InnerGlow", color: Color.Black); CampaignUI = new CampaignUI(this, campaignUIContainer) @@ -720,7 +720,7 @@ namespace Barotrauma } else { - campaign.UpgradeManager.PurchaseItemSwap(purchasedItemSwap.ItemToRemove, purchasedItemSwap.ItemToInstall, force: true); + campaign.UpgradeManager.PurchaseItemSwap(purchasedItemSwap.ItemToRemove, purchasedItemSwap.ItemToInstall, isNetworkMessage: true); } } foreach (Item item in Item.ItemList.ToList()) @@ -906,6 +906,8 @@ namespace Barotrauma public void ClientReadCrew(IReadMessage msg) { + bool createNotification = msg.ReadBoolean(); + ushort availableHireLength = msg.ReadUInt16(); List availableHires = new List(); for (int i = 0; i < availableHireLength; i++) @@ -916,10 +918,10 @@ namespace Barotrauma } ushort pendingHireLength = msg.ReadUInt16(); - List pendingHires = new List(); + List pendingHires = new List(); for (int i = 0; i < pendingHireLength; i++) { - pendingHires.Add(msg.ReadInt32()); + pendingHires.Add(msg.ReadUInt16()); } ushort hiredLength = msg.ReadUInt16(); @@ -934,30 +936,40 @@ namespace Barotrauma bool renameCrewMember = msg.ReadBoolean(); if (renameCrewMember) { - int renamedIdentifier = msg.ReadInt32(); + UInt16 renamedIdentifier = msg.ReadUInt16(); string newName = msg.ReadString(); - CharacterInfo renamedCharacter = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.GetIdentifierUsingOriginalName() == renamedIdentifier); + CharacterInfo renamedCharacter = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.ID == renamedIdentifier); if (renamedCharacter != null) { CrewManager.RenameCharacter(renamedCharacter, newName); } } bool fireCharacter = msg.ReadBoolean(); if (fireCharacter) { - int firedIdentifier = msg.ReadInt32(); - CharacterInfo firedCharacter = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.GetIdentifier() == firedIdentifier); + UInt16 firedIdentifier = msg.ReadUInt16(); + CharacterInfo firedCharacter = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.ID == 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); } } - if (map?.CurrentLocation?.HireManager != null && CampaignUI?.CrewManagement != null && - /*can't apply until we have the latest save file*/ - !NetIdUtils.IdMoreRecent(pendingSaveID, LastSaveID)) + if (map?.CurrentLocation?.HireManager != null && CampaignUI?.CrewManagement != null) { - CampaignUI.CrewManagement.SetHireables(map.CurrentLocation, availableHires); - if (hiredCharacters.Any()) { CampaignUI.CrewManagement.ValidateHires(hiredCharacters, takeMoney: false); } - CampaignUI.CrewManagement.SetPendingHires(pendingHires, map.CurrentLocation); - if (renameCrewMember || fireCharacter) { CampaignUI.CrewManagement.UpdateCrew(); } + //can't apply until we have the latest save file + if (!NetIdUtils.IdMoreRecent(pendingSaveID, LastSaveID)) + { + CampaignUI.CrewManagement.SetHireables(map.CurrentLocation, availableHires); + if (hiredCharacters.Any()) { CampaignUI.CrewManagement.ValidateHires(hiredCharacters, takeMoney: false, createNotification: createNotification); } + CampaignUI.CrewManagement.SetPendingHires(pendingHires, map.CurrentLocation); + if (renameCrewMember || fireCharacter) { CampaignUI.CrewManagement.UpdateCrew(); } + } } + else + { + //This is pretty nasty: setting hireables is handled through CrewManagement, + //which is part of the Campaign UI that might not exist when the client is still initializing the round. + //If that's the case, let's force the available hires here so they're available when the UI is created + CurrentLocation?.ForceHireableCharacters(availableHires); + } + } public void ClientReadMoney(IReadMessage inc) @@ -979,6 +991,7 @@ namespace Barotrauma else { Bank.Balance = info.Balance; + Bank.RewardDistribution = info.RewardDistribution; TryInvokeEvent(Bank, transaction.ChangedData, info); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs index ff5940862..728c8a7c7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs @@ -1,6 +1,8 @@ -using System; +using Barotrauma.Abilities; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using System; namespace Barotrauma { @@ -43,13 +45,19 @@ namespace Barotrauma private GUIButton crewListButton, commandButton, tabMenuButton; private GUIImage talentPointNotification; - private GUIComponent respawnInfoFrame, respawnButtonContainer; + private GUIComponent deathChoiceInfoFrame, deathChoiceButtonContainer; private GUITextBlock respawnInfoText; - private GUITickBox respawnTickBox; + private GUITickBox deathChoiceTickBox; + private GUIButton takeOverBotButton; + private GUIButton hrManagerButton; + public DeathPrompt DeathPrompt; private GUIImage eventLogNotification; private Point prevTopLeftButtonsResolution; + + public bool AllowHrManagerBotTakeover => GameMain.NetworkMember?.ServerSettings is { RespawnMode: RespawnMode.Permadeath, IronmanMode: false } + && Level.IsLoadedFriendlyOutpost; private void CreateTopLeftButtons() { @@ -96,30 +104,63 @@ namespace Barotrauma talentPointNotification = CreateNotificationIcon(tabMenuButton); eventLogNotification = CreateNotificationIcon(tabMenuButton); - - 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) + + // The visibility of the following contents of deathChoiceInfoFrame is controlled by SetRespawnInfo() + + deathChoiceInfoFrame = 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) + respawnInfoText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), deathChoiceInfoFrame.RectTransform), "", wrap: true); + deathChoiceButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), deathChoiceInfoFrame.RectTransform, Anchor.CenterRight), isHorizontal: true, childAnchor: Anchor.CenterLeft) { AbsoluteSpacing = HUDLayoutSettings.Padding, Stretch = true, Visible = false }; - respawnTickBox = new GUITickBox(new RectTransform(Vector2.One * 0.9f, respawnButtonContainer.RectTransform, Anchor.Center), TextManager.Get("respawnquestionpromptrespawn")) + + takeOverBotButton = new GUIButton(new RectTransform(Vector2.One * 0.9f, deathChoiceButtonContainer.RectTransform, Anchor.Center), + TextManager.Get("takeoverbotquestionprompttakeoverbot"), style: "GUIButtonSmall") { - ToolTip = TextManager.GetWithVariable( - "respawnquestionprompt", "[percentage]", - (Math.Round(Networking.RespawnManager.SkillLossPercentageOnImmediateRespawn).ToString())), - OnSelected = (tickbox) => + OnClicked = (btn, userdata) => { - GameMain.Client?.SendRespawnPromptResponse(waitForNextRoundRespawn: !tickbox.Selected); + DeathPrompt.CreateTakeOverBotPanel(); return true; } }; + takeOverBotButton.TextBlock.AutoScaleHorizontal = true; + + hrManagerButton = new GUIButton(new RectTransform(Vector2.One * 0.9f, deathChoiceButtonContainer.RectTransform, Anchor.Center), + TextManager.Get("npctitle.hrmanager"), style: "GUIButtonSmall") + { + OnClicked = (btn, userdata) => + { + if (GameMain.GameSession?.Campaign is { } campaign) + { + campaign.ShowCampaignUI = true; + campaign.CampaignUI?.SelectTab(CampaignMode.InteractionType.Crew); + } + return true; + } + }; + hrManagerButton.TextBlock.AutoScaleHorizontal = true; + + var questionText = + TextManager.GetWithVariable( + "respawnquestionprompt", "[percentage]", + ((int)Math.Round(RespawnManager.SkillLossPercentageOnImmediateRespawn)).ToString()); + deathChoiceTickBox = new GUITickBox(new RectTransform(Vector2.One * 0.9f, deathChoiceButtonContainer.RectTransform, Anchor.Center), + TextManager.Get("respawnquestionpromptrespawn")) + { + ToolTip = questionText, + OnSelected = (tickbox) => + { + GameMain.Client?.SendRespawnPromptResponse(waitForNextRoundRespawn: !tickbox.Selected); + return true; + } + }; + prevTopLeftButtonsResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); } @@ -150,6 +191,8 @@ namespace Barotrauma GameMain.NetLobbyScreen.CharacterAppearanceCustomizationMenu?.AddToGUIUpdateList(); GameMain.NetLobbyScreen?.JobSelectionFrame?.AddToGUIUpdateList(); } + + DeathPrompt?.AddToGUIUpdateList(); } public static GUIImage CreateNotificationIcon(GUIComponent parent, bool offset = true) @@ -230,16 +273,67 @@ namespace Barotrauma HintManager.Update(); ObjectiveManager.VideoPlayer.Update(); } - - public void SetRespawnInfo(bool visible, string text, Color textColor, bool buttonsVisible, bool waitForNextRoundRespawn) + + /// + /// This method controls the content and visibility logic of the respawn-related GUI elements at the top left of the game screen. + /// + /// Has the player chosen to wait until next round + /// Hide the respawn buttons even if they would otherwise be visible + public void SetRespawnInfo(string text, Color textColor, bool waitForNextRoundRespawn, bool hideButtons = false) { if (topLeftButtonGroup == null) { return; } - respawnInfoFrame.Visible = visible; - if (!visible) { return; } + + bool permadeathMode = GameMain.NetworkMember?.ServerSettings is { RespawnMode: RespawnMode.Permadeath }; + bool ironmanMode = GameMain.NetworkMember is { ServerSettings: { RespawnMode: RespawnMode.Permadeath, IronmanMode: true } }; + + bool hasRespawnOptions; + if (permadeathMode) + { + // In permadeath mode you can (in ironman, must) always at least wait, and possibly buy a new character from HR or take control of a bot + hasRespawnOptions = !ironmanMode && + GameMain.Client is GameClient client && (client.CharacterInfo == null || client.CharacterInfo.PermanentlyDead); + } + else // "classic" respawn modes + { + //can choose between midround respawning with a penalty or waiting + //if we're in a non-outpost level, and either don't have an existing character or have already spawned during the round + //(otherwise, e.g. when joining a campaign in which we have an existing character, we can respawn mid-round "for free" and there's no reason to make a choice) + hasRespawnOptions = Level.Loaded?.Type != LevelData.LevelType.Outpost && + (GameMain.Client is GameClient client && (client.CharacterInfo == null || client.HasSpawned)); + } + + // Are the death choice elements shown at all, at least with the text? + deathChoiceInfoFrame.Visible = !text.IsNullOrEmpty() || hasRespawnOptions; + if (!deathChoiceInfoFrame.Visible) { return; } respawnInfoText.Text = text; respawnInfoText.TextColor = textColor; - respawnButtonContainer.Visible = buttonsVisible; - respawnTickBox.Selected = !waitForNextRoundRespawn; + + // Determine if we even bother considering showing the buttons + if (GameMain.GameSession.GameMode is not CampaignMode || Character.Controlled != null) + { + // Disable the button container in case it was left visible earlier + deathChoiceButtonContainer.Visible = false; + return; + } + + deathChoiceButtonContainer.Visible = hasRespawnOptions && !hideButtons; + if (deathChoiceButtonContainer.Visible) + { + hrManagerButton.Visible = AllowHrManagerBotTakeover; + + if (permadeathMode && ironmanMode) + { + takeOverBotButton.Visible = false; + deathChoiceTickBox.Visible = false; + deathChoiceTickBox.Selected = false; + } + else + { + takeOverBotButton.Visible = permadeathMode && GameMain.NetworkMember?.ServerSettings is { AllowBotTakeoverOnPermadeath: true }; + deathChoiceTickBox.Visible = !permadeathMode; + deathChoiceTickBox.Selected = !waitForNextRoundRespawn; + } + } } public void Draw(SpriteBatch spriteBatch) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs index 796546c78..351d17f55 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System; using System.Collections.Generic; @@ -81,6 +81,22 @@ namespace Barotrauma public void Update(float deltaTime) { + processAfflictionChangesTimer -= deltaTime; + if (processAfflictionChangesTimer <= 0.0f) + { + foreach (var character in charactersWithAfflictionChanges) + { + if (GameMain.NetworkMember is null) + { + ImmutableArray afflictions = GetAllAfflictions(character.CharacterHealth); + ui?.UpdateAfflictions(new NetCrewMember(character.Info, afflictions)); + } + ui?.UpdateCrewPanel(); + } + charactersWithAfflictionChanges.Clear(); + processAfflictionChangesTimer = ProcessAfflictionChangesInterval; + } + DateTimeOffset now = DateTimeOffset.Now; UpdateQueue(afflictionRequests, now, onTimeout: static callback => { callback(new AfflictionRequest(RequestResult.Timeout, ImmutableArray.Empty)); }); UpdateQueue(pendingHealRequests, now, onTimeout: static callback => { callback(new PendingRequest(RequestResult.Timeout, NetCollection.Empty)); }); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 2b9ec4112..c2501c220 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -785,7 +785,7 @@ namespace Barotrauma { if (item.Container == null || character.Inventory.FindIndex(item.Container) == -1) // Not a subinventory in the character's inventory { - if (character.HeldItems.Any(i => i.OwnInventory != null && i.OwnInventory.CanBePut(item))) + if (character.HeldItems.Any(i => i.OwnInventory != null && i.OwnInventory.CanBePut(item) && character.CanAccessInventory(i.OwnInventory))) { return QuickUseAction.PutToEquippedItem; } @@ -843,13 +843,14 @@ namespace Barotrauma else if (character.HeldItems.FirstOrDefault(i => i.OwnInventory != null && i.OwnInventory.Container.DrawInventory && + character.CanAccessInventory(i.OwnInventory) && (i.OwnInventory.CanBePut(item) || ((i.OwnInventory.Capacity == 1 || i.OwnInventory.Container.HasSubContainers) && i.OwnInventory.AllowSwappingContainedItems && i.OwnInventory.Container.CanBeContained(item)))) is { } equippedContainer) { if (allowEquip) { if (!character.HasEquippedItem(item)) { - if (equippedContainer.GetComponent() is { QuickUseMovesItemsInside: false}) + if (equippedContainer.GetComponent() is { QuickUseMovesItemsInside: false }) { //put the item in a hand slot if that hand is free if ((item.AllowedSlots.Contains(InvSlotType.RightHand) && character.Inventory.GetItemInLimbSlot(InvSlotType.RightHand) == null) || diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index dfa27902a..e1658c4ef 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -79,7 +79,8 @@ namespace Barotrauma.Items.Components set; } - [Serialize(false, IsPropertySaveable.No, description: "If true, the contained state indicator calculates how full the item is based on the total amount of items that can be stacked inside it, as opposed to how many of the inventory slots are occupied.")] + [Serialize(false, IsPropertySaveable.No, description: "If true, the contained state indicator calculates how full the item is based on the total amount of items that can be stacked inside it, as opposed to how many of the inventory slots are occupied." + + " Note that only items in the main container or in the subcontainer are counted, depending on which container the first containable item match is found in. The item determining this can be defined with ContainedStateIndicatorSlot")] public bool ShowTotalStackCapacityInContainedStateIndicator { get; set; } [Serialize(false, IsPropertySaveable.No, description: "Should the inventory of this item be kept open when the item is equipped by a character.")] @@ -274,8 +275,14 @@ namespace Barotrauma.Items.Components } } - itemsPerSlot.Sort((i1, i2) => i1.First().Name.CompareTo(i2.First().Name)); - foreach (var items in itemsPerSlot) + var sortedItems = itemsPerSlot + .OrderBy(i => i.First().Name) + //if there's multiple items with the same name, sort largest stacks first + .ThenByDescending(i => i.Count) + //same name and stack size, sort items with most items inside first + .ThenByDescending(i => i.First().ContainedItems.Count()); + + foreach (var items in sortedItems) { int firstFreeSlot = -1; for (int i = 0; i < Inventory.Capacity; i++) @@ -591,7 +598,8 @@ namespace Barotrauma.Items.Components contained.Item.Scale, spriteEffects, depth: containedSpriteDepth); - contained.Item.DrawDecorativeSprites(spriteBatch, itemPos, flipX,flipY, (contained.Item.body == null ? 0.0f : contained.Item.body.DrawRotation), containedSpriteDepth); + contained.Item.DrawDecorativeSprites(spriteBatch, itemPos, flipX,flipY, (contained.Item.body == null ? 0.0f : contained.Item.body.DrawRotation), + containedSpriteDepth, overrideColor); foreach (ItemContainer ic in contained.Item.GetComponents()) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs index 8bbdc47c0..8e88d4189 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -40,7 +40,7 @@ namespace Barotrauma.Items.Components partial void SetLightSourceState(bool enabled, float brightness) { if (Light == null) { return; } - if (item.HiddenInGame) { enabled = false; } + if (item.IsHidden) { enabled = false; } Light.Enabled = enabled; lightColorMultiplier = brightness; if (enabled) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index dc0eeb6dd..1a1f2780e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -429,7 +429,7 @@ namespace Barotrauma.Items.Components { if (it?.Submarine == null) { return false; } if (item.Submarine == null || !item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true)) { return false; } - if (it.NonInteractable || it.HiddenInGame) { return false; } + if (it.NonInteractable || it.IsHidden) { return false; } if (it.GetComponent() == null) { return false; } var holdable = it.GetComponent(); @@ -470,10 +470,10 @@ namespace Barotrauma.Items.Components scissorComponent = new GUIScissorComponent(new RectTransform(Vector2.One, submarineContainer.RectTransform, Anchor.Center)); miniMapContainer = new GUIFrame(new RectTransform(Vector2.One, scissorComponent.Content.RectTransform, Anchor.Center), style: null) { CanBeFocused = false }; - ImmutableHashSet hullPointsOfInterest = Item.ItemList.Where(it => item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true) && !it.HiddenInGame && !it.NonInteractable && it.Prefab.ShowInStatusMonitor && (it.GetComponent() != null || it.GetComponent() != null)).ToImmutableHashSet(); + ImmutableHashSet hullPointsOfInterest = Item.ItemList.Where(it => item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true) && !it.IsHidden && !it.NonInteractable && it.Prefab.ShowInStatusMonitor && (it.GetComponent() != null || it.GetComponent() != null)).ToImmutableHashSet(); miniMapFrame = CreateMiniMap(item.Submarine, submarineContainer, MiniMapSettings.Default, hullPointsOfInterest, out hullStatusComponents); - IEnumerable electricalPointsOfInterest = Item.ItemList.Where(it => item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true) && !it.HiddenInGame && !it.NonInteractable && it.GetComponent() != null); + IEnumerable electricalPointsOfInterest = Item.ItemList.Where(it => item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true) && !it.IsHidden && !it.NonInteractable && it.GetComponent() != null); electricalFrame = CreateMiniMap(item.Submarine, miniMapContainer, new MiniMapSettings(createHullElements: false), electricalPointsOfInterest, out electricalMapComponents); Dictionary electricChildren = new Dictionary(); @@ -566,7 +566,7 @@ namespace Barotrauma.Items.Components displayedSubs.Add(item.Submarine); displayedSubs.AddRange(item.Submarine.DockedTo.Where(s => s.TeamID == item.Submarine.TeamID)); - subEntities = MapEntity.MapEntityList.Where(me => (item.Submarine is { } sub && sub.IsEntityFoundOnThisSub(me, includingConnectedSubs: true, allowDifferentType: false)) && !me.HiddenInGame).OrderByDescending(w => w.SpriteDepth).ToList(); + subEntities = MapEntity.MapEntityList.Where(me => (item.Submarine is { } sub && sub.IsEntityFoundOnThisSub(me, includingConnectedSubs: true, allowDifferentType: false)) && !me.IsHidden).OrderByDescending(w => w.SpriteDepth).ToList(); BakeSubmarine(item.Submarine, parentRect); elementSize = GuiFrame.Rect.Size; @@ -763,7 +763,7 @@ namespace Barotrauma.Items.Components worldBorders.Location += item.Submarine.WorldPosition.ToPoint(); foreach (Gap gap in Gap.GapList) { - if (gap.IsRoomToRoom || gap.linkedTo.Count == 0 || gap.Submarine != item.Submarine || gap.ConnectedDoor != null || gap.HiddenInGame) { continue; } + if (gap.IsRoomToRoom || gap.linkedTo.Count == 0 || gap.Submarine != item.Submarine || gap.ConnectedDoor != null || gap.IsHidden) { continue; } RectangleF entityRect = ScaleRectToUI(gap, miniMapFrame.Rect, worldBorders); Vector2 scale = new Vector2(entityRect.Size.X / spriteSize.X, entityRect.Size.Y / spriteSize.Y) * 2.0f; @@ -930,7 +930,7 @@ namespace Barotrauma.Items.Components if (DisplayAsSameItem(it.Prefab, searchedPrefab)) { // ignore items on players and hidden inventories - if (it.FindParentInventory(inv => inv is CharacterInventory || inv is ItemInventory { Owner: Item { HiddenInGame: true }}) is { }) { continue; } + if (it.FindParentInventory(inv => inv is CharacterInventory || inv is ItemInventory { Owner: Item { IsHidden: true }}) is { }) { continue; } if (it.FindParentInventory(inventory => inventory is ItemInventory { Owner: Item { ParentInventory: null } }) is ItemInventory parent) { @@ -1112,7 +1112,7 @@ namespace Barotrauma.Items.Components if (ShowHullIntegrity) { float amount = 1f + hullData.LinkedHulls.Count; - gapOpenSum = hull.ConnectedGaps.Concat(hullData.LinkedHulls.SelectMany(h => h.ConnectedGaps)).Where(g => g.linkedTo.Count == 1 && !g.HiddenInGame).Sum(g => g.Open) / amount; + gapOpenSum = hull.ConnectedGaps.Concat(hullData.LinkedHulls.SelectMany(h => h.ConnectedGaps)).Where(g => g.linkedTo.Count == 1 && !g.IsHidden).Sum(g => g.Open) / amount; borderColor = Color.Lerp(neutralColor, GUIStyle.Red, Math.Min(gapOpenSum, 1.0f)); } @@ -1557,7 +1557,7 @@ namespace Barotrauma.Items.Components { if (linkedEntity is Hull linkedHull) { - if (linkedHulls.Contains(linkedHull) || linkedHull.HiddenInGame) { continue; } + if (linkedHulls.Contains(linkedHull) || linkedHull.IsHidden) { continue; } linkedHulls.Add(linkedHull); GetLinkedHulls(linkedHull, linkedHulls); } @@ -1737,7 +1737,7 @@ namespace Barotrauma.Items.Components bool IsPartofSub(MapEntity entity) { - if (entity.Submarine != sub && !connectedSubs.Contains(entity.Submarine) || entity.HiddenInGame) { return false; } + if (entity.Submarine != sub && !connectedSubs.Contains(entity.Submarine) || entity.IsHidden) { return false; } return sub.IsEntityFoundOnThisSub(entity, true); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index 4f3f08ffe..cb5115422 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -1186,7 +1186,7 @@ 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.HiddenInGame) { continue; } + if (dockingPort.Item.IsHidden) { continue; } if (dockingPort.Item.Submarine == null) { continue; } if (dockingPort.Item.Submarine.Info.IsWreck) { continue; } // docking ports should be shown even if defined as not, if the submarine is the same as the sonar's diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs index a05f47a0f..5c790d576 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -60,7 +60,7 @@ namespace Barotrauma.Items.Components public override bool ShouldDrawHUD(Character character) { - if (item.HiddenInGame) { return false; } + if (item.IsHidden) { return false; } if (!HasRequiredItems(character, false) || character.SelectedItem != item) { return false; } if (character.IsTraitor && item.ConditionPercentage > MinSabotageCondition) { return true; } if (item.ConditionPercentageRelativeToDefaultMaxCondition < RepairThreshold) { return true; } @@ -224,7 +224,7 @@ namespace Barotrauma.Items.Components partial void UpdateProjSpecific(float deltaTime) { - if (item.HiddenInGame) { return; } + if (item.IsHidden) { return; } if (FakeBrokenTimer > 0.0f) { item.FakeBroken = true; @@ -397,6 +397,12 @@ namespace Barotrauma.Items.Components GUI.DrawString(spriteBatch, new Vector2(item.DrawPosition.X, -item.DrawPosition.Y + 20), "Condition: " + (int)item.Condition + "/" + (int)item.MaxCondition, GUIStyle.Orange); + if (MaxStressDeteriorationMultiplier > 1.0f) + { + GUI.DrawString(spriteBatch, + new Vector2(item.DrawPosition.X, -item.DrawPosition.Y + 40), "Stress multiplier: " + StressDeteriorationMultiplier.ToString("0.00"), + GUIStyle.Red); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs index 6eb381503..329545f2d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs @@ -303,6 +303,17 @@ namespace Barotrauma.Items.Components CreateClientEvent(new CircuitBoxRenameLabelEvent(label.ID, color, header, body)); } + public void SetConnectionLabelOverrides(CircuitBoxInputOutputNode node, Dictionary newOverrides) + { + if (GameMain.NetworkMember is null) + { + node.ReplaceAllConnectionLabelOverrides(newOverrides); + return; + } + + CreateClientEvent(new CircuitBoxRenameConnectionLabelsEvent(node.NodeType, newOverrides.ToNetDictionary())); + } + public void ResizeNode(CircuitBoxNode node, CircuitBoxResizeDirection dir, Vector2 amount) { if (Locked) { return; } @@ -528,6 +539,12 @@ namespace Barotrauma.Items.Components _ => node.Position }; } + + foreach (var labelOverride in data.LabelOverrides) + { + RenameConnectionLabelsInternal(labelOverride.Type, labelOverride.Override.ToDictionary()); + } + wasInitializedByServer = true; break; } @@ -556,6 +573,12 @@ namespace Barotrauma.Items.Components ResizeLabelInternal(data.ID, data.Position, data.Size); break; } + case CircuitBoxOpcode.RenameConnections: + { + var data = INetSerializableStruct.Read(msg); + RenameConnectionLabelsInternal(data.Type, data.Override.ToDictionary()); + break; + } default: throw new ArgumentOutOfRangeException(nameof(header), header, "This opcode cannot be handled using entity events"); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs index 8b7255a32..7a405bd47 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs @@ -292,8 +292,17 @@ namespace Barotrauma.Items.Components if (wire.HiddenInGame && Screen.Selected == GameMain.GameScreen) { continue; } Connection recipient = wire.OtherConnection(this); - LocalizedString label = recipient == null ? "" : recipient.item.Name + $" ({recipient.DisplayName})"; - if (wire.Locked) { label += "\n" + TextManager.Get("ConnectionLocked"); } + LocalizedString label; + if (wire.Item.IsLayerHidden) + { + label = TextManager.Get("ConnectionLocked"); + } + else + { + label = recipient == null ? "" : recipient.item.Name + $" ({recipient.DisplayName})"; + if (wire.Locked) { label += "\n" + TextManager.Get("ConnectionLocked"); } + } + DrawWire(spriteBatch, wire, position, wirePosition, equippedWire, panel, label); wirePosition.Y += wireInterval; @@ -494,7 +503,7 @@ namespace Barotrauma.Items.Components ConnectionPanel.HighlightedWire = wire; bool allowRewiring = GameMain.NetworkMember?.ServerSettings == null || GameMain.NetworkMember.ServerSettings.AllowRewiring || panel.AlwaysAllowRewiring; - if (allowRewiring && (!wire.Locked && !panel.Locked && !panel.TemporarilyLocked || Screen.Selected == GameMain.SubEditorScreen)) + if (allowRewiring && (!wire.Locked && !wire.Item.IsLayerHidden && !panel.Locked && !panel.TemporarilyLocked || Screen.Selected == GameMain.SubEditorScreen)) { //start dragging the wire if (PlayerInput.PrimaryMouseButtonHeld()) { DraggingConnected = wire; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs index 5021d59a6..a47e99738 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs @@ -283,12 +283,12 @@ namespace Barotrauma.Items.Components texts.Add(CharacterHUD.GetCachedHudText("PlayHint", InputType.Use)); textColors.Add(GUIStyle.Green); } - if (target.CharacterHealth.UseHealthWindow && !target.DisableHealthWindow && equipper?.FocusedCharacter == target && equipper.CanInteractWith(target, 160f, false)) + if (equipper?.FocusedCharacter == target && target.CanBeHealedBy(equipper, checkFriendlyTeam: false)) { texts.Add(CharacterHUD.GetCachedHudText("HealHint", InputType.Health)); textColors.Add(GUIStyle.Green); } - if (target.CanBeDragged) + if (target.CanBeDraggedBy(Character.Controlled)) { texts.Add(CharacterHUD.GetCachedHudText("GrabHint", InputType.Grab)); textColors.Add(GUIStyle.Green); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index febcfc55b..53117dd8f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -196,7 +196,7 @@ namespace Barotrauma.Items.Components Vector2 particlePos = GetRelativeFiringPosition(); foreach (ParticleEmitter emitter in particleEmitters) { - emitter.Emit(1.0f, particlePos, hullGuess: null, angle: -rotation, particleRotation: rotation); + emitter.Emit(1.0f, particlePos, hullGuess: null, angle: -Rotation, particleRotation: Rotation); } } @@ -213,7 +213,7 @@ namespace Barotrauma.Items.Components if (crosshairSprite != null) { Vector2 itemPos = cam.WorldToScreen(new Vector2(item.WorldRect.X + transformedBarrelPos.X, item.WorldRect.Y - transformedBarrelPos.Y)); - Vector2 turretDir = new Vector2((float)Math.Cos(rotation), (float)Math.Sin(rotation)); + Vector2 turretDir = new Vector2((float)Math.Cos(Rotation), (float)Math.Sin(Rotation)); Vector2 mouseDiff = itemPos - PlayerInput.MousePosition; crosshairPos = new Vector2( @@ -268,7 +268,7 @@ namespace Barotrauma.Items.Components foreach (ParticleEmitter emitter in particleEmitterCharges) { // color is currently not connected to ammo type, should be updated when ammo is changed - emitter.Emit(deltaTime, particlePos, hullGuess: null, angle: -rotation, particleRotation: rotation, sizeMultiplier: sizeMultiplier, colorMultiplier: emitter.Prefab.Properties.ColorMultiplier); + emitter.Emit(deltaTime, particlePos, hullGuess: null, angle: -Rotation, particleRotation: Rotation, sizeMultiplier: sizeMultiplier, colorMultiplier: emitter.Prefab.Properties.ColorMultiplier); } if (chargeSoundChannel == null || !chargeSoundChannel.IsPlaying) @@ -339,7 +339,7 @@ namespace Barotrauma.Items.Components if (crosshairSprite != null) { Vector2 itemPos = cam.WorldToScreen(item.WorldPosition); - Vector2 turretDir = new Vector2((float)Math.Cos(rotation), (float)Math.Sin(rotation)); + Vector2 turretDir = new Vector2((float)Math.Cos(Rotation), (float)Math.Sin(Rotation)); Vector2 mouseDiff = itemPos - PlayerInput.MousePosition; crosshairPos = new Vector2( @@ -372,7 +372,7 @@ namespace Barotrauma.Items.Components recoilOffset = RecoilDistance; } } - return new Vector2((float)Math.Cos(rotation), (float)Math.Sin(rotation)) * recoilOffset; + return new Vector2((float)Math.Cos(Rotation), (float)Math.Sin(Rotation)) * recoilOffset; } public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1, Color? overrideColor = null) @@ -388,13 +388,13 @@ namespace Barotrauma.Items.Components railSprite?.Draw(spriteBatch, drawPos, overrideColor ?? item.SpriteColor, - rotation + MathHelper.PiOver2, item.Scale, + Rotation + MathHelper.PiOver2, item.Scale, SpriteEffects.None, item.SpriteDepth + (railSprite.Depth - item.Sprite.Depth)); barrelSprite?.Draw(spriteBatch, drawPos - GetRecoilOffset() * item.Scale, overrideColor ?? item.SpriteColor, - rotation + MathHelper.PiOver2, item.Scale, + Rotation + MathHelper.PiOver2, item.Scale, SpriteEffects.None, item.SpriteDepth + (barrelSprite.Depth - item.Sprite.Depth)); float chargeRatio = currentChargeTime / MaxChargeTime; @@ -402,9 +402,9 @@ namespace Barotrauma.Items.Components foreach ((Sprite chargeSprite, Vector2 position) in chargeSprites) { chargeSprite?.Draw(spriteBatch, - drawPos - MathUtils.RotatePoint(new Vector2(position.X * chargeRatio, position.Y * chargeRatio) * item.Scale, rotation + MathHelper.PiOver2), + drawPos - MathUtils.RotatePoint(new Vector2(position.X * chargeRatio, position.Y * chargeRatio) * item.Scale, Rotation + MathHelper.PiOver2), item.SpriteColor, - rotation + MathHelper.PiOver2, item.Scale, + Rotation + MathHelper.PiOver2, item.Scale, SpriteEffects.None, item.SpriteDepth + (chargeSprite.Depth - item.Sprite.Depth)); } @@ -427,9 +427,9 @@ namespace Barotrauma.Items.Components float newPositionOffset = barrelPositionModifier * SpinningBarrelDistance; spinningBarrel.Draw(spriteBatch, - drawPos - MathUtils.RotatePoint(new Vector2(newPositionOffset, 0f) * item.Scale, rotation + MathHelper.PiOver2), + drawPos - MathUtils.RotatePoint(new Vector2(newPositionOffset, 0f) * item.Scale, Rotation + MathHelper.PiOver2), Color.Lerp(overrideColor ?? item.SpriteColor, newColorModifier, 0.8f), - rotation + MathHelper.PiOver2, item.Scale, + Rotation + MathHelper.PiOver2, item.Scale, SpriteEffects.None, newDepth); } } @@ -475,9 +475,9 @@ namespace Barotrauma.Items.Components { spriteBatch.DrawLine(drawPos, drawPos + center * circleRadius, GUIStyle.Green, thickness: lineThickness); } - else if (radians > Math.PI * 2) + else if (radians >= MathHelper.TwoPi) { - spriteBatch.DrawCircle(drawPos, circleRadius, 180, GUIStyle.Red, thickness: lineThickness); + spriteBatch.DrawCircle(drawPos, circleRadius, 180, GUIStyle.Green, thickness: lineThickness); } else { @@ -510,7 +510,12 @@ namespace Barotrauma.Items.Components }; widget.MouseHeld += (deltaTime) => { - minRotation = GetRotationAngle(GetDrawPos()); + float newMinRotation = GetRotationAngle(GetDrawPos()); + AngleWrapAdjustment(minRotation, newMinRotation, ref maxRotation); + + // clamp value here to keep widget movement within max range + minRotation = MathHelper.Clamp(newMinRotation, maxRotation - MathHelper.TwoPi, maxRotation); + UpdateBarrel(); MapEntity.DisableSelect = true; }; @@ -554,7 +559,12 @@ namespace Barotrauma.Items.Components }; widget.MouseHeld += (deltaTime) => { - maxRotation = GetRotationAngle(GetDrawPos()); + float newMaxRotation = GetRotationAngle(GetDrawPos()); + AngleWrapAdjustment(maxRotation, newMaxRotation, ref minRotation); + + // clamp value here to keep widget movement within max range + maxRotation = MathHelper.Clamp(newMaxRotation, minRotation, minRotation + MathHelper.TwoPi); + UpdateBarrel(); MapEntity.DisableSelect = true; }; @@ -580,10 +590,44 @@ namespace Barotrauma.Items.Components void UpdateBarrel() { - rotation = (minRotation + maxRotation) / 2; + Rotation = (minRotation + maxRotation) / 2; } } - + + private static void AngleWrapAdjustment(float currentRotation, float newRotation, ref float rangeLockedRotation) + { + if (DetectAngleWrapAround(currentRotation, newRotation)) + { + // if there's a wrap-around, also wrap the other rotation limit to keep range + if (newRotation < currentRotation) + { + rangeLockedRotation -= MathHelper.TwoPi; + } + else + { + rangeLockedRotation += MathHelper.TwoPi; + } + } + } + + private static bool DetectAngleWrapAround(float rotation, float newRotation) + { + float deltaRotation = MathF.Abs(rotation - newRotation); + + // turret angle wraps around to 0 from -2Pi and 2Pi. + // Detect wrap-around when dragging the widgets, where usual rotation delta is small, + // so a large jump in rotation (here, an arbitrary big value in the range of 0 to 2Pi) + // is considered a wrap-around for this purpose. + // NOTE: this is not a reliable way to detect angle wrap-around in general, and is only intended for + // the angle widgets! + if (deltaRotation > MathHelper.TwoPi * 0.8f) + { + return true; + } + + return false; + } + public Vector2 GetDrawPos() { Vector2 drawPos = new Vector2(item.Rect.X + transformedBarrelPos.X, item.Rect.Y - transformedBarrelPos.Y); @@ -764,7 +808,7 @@ namespace Barotrauma.Items.Components if (projectileID == 0) { return; } //ID ushort.MaxValue = launched without a projectile - if (projectileID == ushort.MaxValue) + if (projectileID == LaunchWithoutProjectileId) { Launch(null, user); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 176d5c98b..d756c442c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -319,7 +319,7 @@ namespace Barotrauma } } - string colorStr = (item.SpawnedInCurrentOutpost && !item.AllowStealing ? GUIStyle.Red : Color.White).ToStringHex(); + string colorStr = (item.Illegitimate ? GUIStyle.Red : Color.White).ToStringHex(); toolTip = $"‖color:{colorStr}‖{name}‖color:end‖"; if (item.GetComponent() != null) @@ -478,10 +478,11 @@ namespace Barotrauma { int row = (int)Math.Floor((double)i / slotsPerRow); int slotsPerThisRow = Math.Min(slotsPerRow, capacity - row * slotsPerRow); + int slotNumberOnThisRow = i - row * slotsPerRow; int rowWidth = (int)(rectSize.X * slotsPerThisRow + spacing.X * (slotsPerThisRow - 1)); slotRect.X = (int)(center.X) - rowWidth / 2; - slotRect.X += (int)((rectSize.X + spacing.X) * (i % slotsPerThisRow)); + slotRect.X += (int)((rectSize.X + spacing.X) * (slotNumberOnThisRow % slotsPerThisRow)); slotRect.Y = (int)(topLeft.Y + (rectSize.Y + spacing.Y) * row); visualSlots[i] = new VisualSlot(slotRect); @@ -1185,6 +1186,7 @@ namespace Barotrauma { DraggingItems.RemoveAll(it => !Character.Controlled.CanInteractWith(it)); } + if (DraggingItems.Any() && PlayerInput.PrimaryMouseButtonReleased()) { Character.Controlled.ClearInputs(); @@ -1193,198 +1195,234 @@ namespace Barotrauma if (!DetermineMouseOnInventory(ignoreDraggedItem: true) && (CharacterHealth.OpenHealthWindow != null || mouseOnPortrait)) { - bool dropSuccessful = false; - foreach (Item item in DraggingItems) + if (TryPortraitAndHealthDrop(mouseOnPortrait)) { - var inventory = item.ParentInventory; - var indices = inventory?.FindIndices(item); - dropSuccessful |= (CharacterHealth.OpenHealthWindow ?? Character.Controlled.CharacterHealth).OnItemDropped(item, ignoreMousePos: mouseOnPortrait); - if (dropSuccessful) - { - if (indices != null && inventory.visualSlots != null) - { - foreach (int i in indices) - { - inventory.visualSlots[i]?.ShowBorderHighlight(GUIStyle.Green, 0.1f, 0.4f); - } - } - break; - } - } - if (dropSuccessful) - { - DraggingItems.Clear(); return; } } if (selectedSlot == null) { - if (DraggingItemToWorld && - Character.Controlled.FocusedItem is { OwnInventory: { } inventory } item && item.GetComponent() is { } container && - container.HasRequiredItems(Character.Controlled, addMessage: false) && - container.AllowDragAndDrop && - inventory.CanBePut(DraggingItems.FirstOrDefault())) + HandleOutsideInventoryDrop(); + } + else if (!DraggingItems.Any(it => selectedSlot.ParentInventory.slots[selectedSlot.SlotIndex].Contains(it))) + { + HandleInventorySlotDrop(); + } + + DraggingItems.Clear(); + } + + if (selectedSlot != null && !CanSelectSlot(selectedSlot)) + { + selectedSlot = null; + } + + bool TryPortraitAndHealthDrop(bool mouseOnPortrait) + { + bool dropSuccessful = false; + foreach (Item item in DraggingItems) + { + var inventory = item.ParentInventory; + var indices = inventory?.FindIndices(item); + dropSuccessful |= (CharacterHealth.OpenHealthWindow ?? Character.Controlled.CharacterHealth).OnItemDropped(item, ignoreMousePos: mouseOnPortrait); + if (dropSuccessful) { - bool anySuccess = false; - foreach (Item it in DraggingItems) + if (indices != null && inventory.visualSlots != null) { - bool success = Character.Controlled.FocusedItem.OwnInventory.TryPutItem(it, Character.Controlled); - if (!success) { break; } - anySuccess |= success; - } - if (anySuccess) { SoundPlayer.PlayUISound(GUISoundType.PickItem); } - } - else - { - if (Screen.Selected is SubEditorScreen) - { - if (DraggingItems.First()?.ParentInventory != null) + foreach (int i in indices) { - SubEditorScreen.StoreCommand(new InventoryPlaceCommand(DraggingItems.First().ParentInventory, new List(DraggingItems), true)); + inventory.visualSlots[i]?.ShowBorderHighlight(GUIStyle.Green, 0.1f, 0.4f); } } - - SoundPlayer.PlayUISound(GUISoundType.DropItem); - bool removed = false; - if (Screen.Selected is SubEditorScreen editor) + break; + } + } + if (dropSuccessful) + { + DraggingItems.Clear(); + return true; + } + + return false; + } + + void HandleOutsideInventoryDrop() + { + bool isTargetingValidContainer = Character.Controlled.FocusedItem is { OwnInventory: { } inventory } item && + item.GetComponent() is { } container && + container.HasRequiredItems(Character.Controlled, addMessage: false) && + container.AllowDragAndDrop && + inventory.CanBePut(DraggingItems.FirstOrDefault()); + + bool isTargetingValidCharacter = IsValidTargetForDragDropGive(Character.Controlled, Character.Controlled.FocusedCharacter); + + if (DraggingItemToWorld && (isTargetingValidContainer || isTargetingValidCharacter)) + { + bool anySuccess = false; + foreach (Item it in DraggingItems) + { + bool success = false; + if (isTargetingValidContainer) { - if (editor.EntityMenu.Rect.Contains(PlayerInput.MousePosition)) + success = Character.Controlled.FocusedItem.OwnInventory.TryPutItem(it, Character.Controlled); + } + if (!success && isTargetingValidCharacter) + { + success = Character.Controlled.FocusedCharacter.Inventory.TryPutItem(it, Character.Controlled, CharacterInventory.AnySlot); + } + + if (!success) { break; } + anySuccess = true; + } + + if (anySuccess) { SoundPlayer.PlayUISound(GUISoundType.PickItem); } + } + else + { + if (Screen.Selected is SubEditorScreen) + { + if (DraggingItems.First()?.ParentInventory != null) + { + SubEditorScreen.StoreCommand(new InventoryPlaceCommand(DraggingItems.First().ParentInventory, new List(DraggingItems), true)); + } + } + + SoundPlayer.PlayUISound(GUISoundType.DropItem); + bool removed = false; + if (Screen.Selected is SubEditorScreen editor) + { + if (editor.EntityMenu.Rect.Contains(PlayerInput.MousePosition)) + { + DraggingItems.ForEachMod(it => it.Remove()); + removed = true; + } + else + { + if (editor.WiringMode) { DraggingItems.ForEachMod(it => it.Remove()); removed = true; } else { - if (editor.WiringMode) - { - DraggingItems.ForEachMod(it => it.Remove()); - removed = true; - } - else - { - DraggingItems.ForEachMod(it => it.Drop(Character.Controlled)); - } + DraggingItems.ForEachMod(it => it.Drop(Character.Controlled)); } } - else - { - DraggingItems.ForEachMod(it => it.Drop(Character.Controlled)); - DraggingItems.First().CreateDroppedStack(DraggingItems, allowClientExecute: false); - } - SoundPlayer.PlayUISound(removed ? GUISoundType.PickItem : GUISoundType.DropItem); } - } - else if (!DraggingItems.Any(it => selectedSlot.ParentInventory.slots[selectedSlot.SlotIndex].Contains(it))) - { - Inventory oldInventory = DraggingItems.First().ParentInventory; - Inventory selectedInventory = selectedSlot.ParentInventory; - int slotIndex = selectedSlot.SlotIndex; - int oldSlot = oldInventory == null ? 0 : Array.IndexOf(oldInventory.slots, DraggingItems); - - //if attempting to drop into an invalid slot in the same inventory, try to move to the correct slot - if (selectedInventory.slots[slotIndex].Empty() && - selectedInventory == Character.Controlled.Inventory && - !DraggingItems.First().AllowedSlots.Any(a => a.HasFlag(Character.Controlled.Inventory.SlotTypes[slotIndex])) && - DraggingItems.Any(it => selectedInventory.TryPutItem(it, Character.Controlled, it.AllowedSlots))) + else { - if (selectedInventory.visualSlots != null) + DraggingItems.ForEachMod(it => it.Drop(Character.Controlled)); + DraggingItems.First().CreateDroppedStack(DraggingItems, allowClientExecute: false); + } + SoundPlayer.PlayUISound(removed ? GUISoundType.PickItem : GUISoundType.DropItem); + } + } + + void HandleInventorySlotDrop() + { + Inventory oldInventory = DraggingItems.First().ParentInventory; + Inventory selectedInventory = selectedSlot.ParentInventory; + int slotIndex = selectedSlot.SlotIndex; + int oldSlot = oldInventory == null ? 0 : Array.IndexOf(oldInventory.slots, DraggingItems); + + //if attempting to drop into an invalid slot in the same inventory, try to move to the correct slot + if (selectedInventory.slots[slotIndex].Empty() && + selectedInventory == Character.Controlled.Inventory && + !DraggingItems.First().AllowedSlots.Any(a => a.HasFlag(Character.Controlled.Inventory.SlotTypes[slotIndex])) && + DraggingItems.Any(it => selectedInventory.TryPutItem(it, Character.Controlled, it.AllowedSlots))) + { + if (selectedInventory.visualSlots != null) + { + for (int i = 0; i < selectedInventory.visualSlots.Length; i++) { - for (int i = 0; i < selectedInventory.visualSlots.Length; i++) + if (DraggingItems.Any(it => selectedInventory.slots[i].Contains(it))) { - if (DraggingItems.Any(it => selectedInventory.slots[i].Contains(it))) + selectedInventory.visualSlots[slotIndex].ShowBorderHighlight(Color.White, 0.1f, 0.4f); + } + } + selectedInventory.visualSlots[slotIndex].ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.9f); + } + SoundPlayer.PlayUISound(GUISoundType.PickItem); + } + else + { + bool anySuccess = false; + //if we're dragging a stack of partial items or trying to drag to a stack of partial items + //(which should not normally exist, but can happen when e.g. fire damages a stack of items) + //don't allow combining because it leads to weird behavior (stack of items of mixed quality) + bool allowCombine = !(DraggingItems.Count(it => !it.IsFullCondition && it.Condition > 0.0f) > 1 || + selectedInventory.GetItemsAt(slotIndex).Count(it => !it.IsFullCondition && it.Condition > 0.0f) > 1); + int itemCount = 0; + foreach (Item item in DraggingItems) + { + if (selectedInventory.GetItemAt(slotIndex)?.OwnInventory?.Container is { } container && + container.Inventory.CanBePut(item)) + { + if (!container.AllowDragAndDrop || !container.AllowAccess) + { + allowCombine = false; + } + } + bool success = selectedInventory.TryPutItem(item, slotIndex, allowSwapping: !anySuccess, allowCombine, Character.Controlled); + if (success) + { + anySuccess = true; + itemCount++; + } + if (!success || itemCount >= item.Prefab.GetMaxStackSize(selectedInventory)) + { + break; + } + } + + if (anySuccess) + { + highlightedSubInventorySlots.RemoveWhere(s => s.ParentInventory == oldInventory || s.ParentInventory == selectedInventory); + if (SubEditorScreen.IsSubEditor()) + { + foreach (Item draggingItem in DraggingItems) + { + if (selectedInventory.slots[slotIndex].Contains(draggingItem)) { - selectedInventory.visualSlots[slotIndex].ShowBorderHighlight(Color.White, 0.1f, 0.4f); + SubEditorScreen.StoreCommand(new InventoryMoveCommand(oldInventory, selectedInventory, draggingItem, oldSlot, slotIndex)); } } - selectedInventory.visualSlots[slotIndex].ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.9f); } + if (selectedInventory.visualSlots != null) { selectedInventory.visualSlots[slotIndex].ShowBorderHighlight(Color.White, 0.1f, 0.4f); } SoundPlayer.PlayUISound(GUISoundType.PickItem); } else { - bool anySuccess = false; - bool allowCombine = true; - //if we're dragging a stack of partial items or trying to drag to a stack of partial items - //(which should not normally exist, but can happen when e.g. fire damages a stack of items) - //don't allow combining because it leads to weird behavior (stack of items of mixed quality) - if (DraggingItems.Count(it => !it.IsFullCondition && it.Condition > 0.0f) > 1 || - selectedInventory.GetItemsAt(slotIndex).Count(it => !it.IsFullCondition && it.Condition > 0.0f) > 1) - { - allowCombine = false; - } - int itemCount = 0; - foreach (Item item in DraggingItems) - { - if (selectedInventory.GetItemAt(slotIndex)?.OwnInventory?.Container is { } container && - container.Inventory.CanBePut(item)) - { - if (!container.AllowDragAndDrop || !container.AllowAccess) - { - allowCombine = false; - } - } - bool success = selectedInventory.TryPutItem(item, slotIndex, allowSwapping: !anySuccess, allowCombine, Character.Controlled); - if (success) - { - anySuccess = true; - itemCount++; - } - if (!success || itemCount >= item.Prefab.GetMaxStackSize(selectedInventory)) - { - break; - } - } - - if (anySuccess) - { - highlightedSubInventorySlots.RemoveWhere(s => s.ParentInventory == oldInventory || s.ParentInventory == selectedInventory); - if (SubEditorScreen.IsSubEditor()) - { - foreach (Item draggingItem in DraggingItems) - { - if (selectedInventory.slots[slotIndex].Contains(draggingItem)) - { - SubEditorScreen.StoreCommand(new InventoryMoveCommand(oldInventory, selectedInventory, draggingItem, oldSlot, slotIndex)); - } - } - } - if (selectedInventory.visualSlots != null) { selectedInventory.visualSlots[slotIndex].ShowBorderHighlight(Color.White, 0.1f, 0.4f); } - SoundPlayer.PlayUISound(GUISoundType.PickItem); - } - else - { - if (selectedInventory.visualSlots != null){ selectedInventory.visualSlots[slotIndex].ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.9f); } - SoundPlayer.PlayUISound(GUISoundType.PickItemFail); - } + if (selectedInventory.visualSlots != null){ selectedInventory.visualSlots[slotIndex].ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.9f); } + SoundPlayer.PlayUISound(GUISoundType.PickItemFail); } - - selectedInventory.HideTimer = 2.0f; - if (selectedSlot.ParentInventory?.Owner is Item parentItem && parentItem.ParentInventory != null) - { - for (int i = 0; i < parentItem.ParentInventory.capacity; i++) - { - if (parentItem.ParentInventory.HideSlot(i)) { continue; } - if (parentItem.ParentInventory.slots[i].FirstOrDefault() != parentItem) { continue; } - - highlightedSubInventorySlots.Add(new SlotReference( - parentItem.ParentInventory, parentItem.ParentInventory.visualSlots[i], - i, false, selectedSlot.ParentInventory)); - break; - } - - } - DraggingItems.Clear(); - DraggingSlot = null; } - DraggingItems.Clear(); - } + selectedInventory.HideTimer = 2.0f; + if (selectedSlot.ParentInventory?.Owner is Item parentItem && parentItem.ParentInventory != null) + { + for (int i = 0; i < parentItem.ParentInventory.capacity; i++) + { + if (parentItem.ParentInventory.HideSlot(i)) { continue; } + if (parentItem.ParentInventory.slots[i].FirstOrDefault() != parentItem) { continue; } - if (selectedSlot != null && !CanSelectSlot(selectedSlot)) - { - selectedSlot = null; - } + highlightedSubInventorySlots.Add(new SlotReference( + parentItem.ParentInventory, parentItem.ParentInventory.visualSlots[i], + i, false, selectedSlot.ParentInventory)); + break; + } + } + DraggingItems.Clear(); + DraggingSlot = null; + } + } + + private static bool IsValidTargetForDragDropGive(Character giver, Character receiver) + { + if (giver == null || receiver == null) { return false; } + if (receiver == giver) { return false; } + return receiver.IsInventoryAccessibleTo(giver, IsDragAndDropGiveAllowed ? CharacterInventory.AccessLevel.Allowed : CharacterInventory.AccessLevel.Limited); } private static bool CanSelectSlot(SlotReference selectedSlot) @@ -1504,6 +1542,31 @@ namespace Barotrauma } if (DraggingItems.Any()) + { + DrawDragRelated(); + } + + if (selectedSlot != null && selectedSlot.Item != null) + { + Rectangle slotRect = selectedSlot.Slot.Rect; + slotRect.Location += selectedSlot.Slot.DrawOffset.ToPoint(); + if (selectedSlot.TooltipNeedsRefresh()) + { + selectedSlot.RefreshTooltip(); + } + + if (!slotIconTooltip.IsNullOrEmpty()) + { + DrawToolTip(spriteBatch, slotIconTooltip, slotRect); + } + else + { + DrawToolTip(spriteBatch, selectedSlot.Tooltip, slotRect); + } + slotIconTooltip = string.Empty; + } + + void DrawDragRelated() { if (DraggingSlot == null || (!DraggingSlot.MouseOn())) { @@ -1521,10 +1584,8 @@ namespace Barotrauma if ((GUI.MouseOn == null || mouseOnHealthInterface) && selectedSlot == null) { var shadowSprite = GUIStyle.GetComponentStyle("OuterGlow").Sprites[GUIComponent.ComponentState.None][0]; - LocalizedString toolTip = mouseOnHealthInterface ? TextManager.Get("QuickUseAction.UseTreatment") : - Character.Controlled.FocusedItem != null ? - TextManager.GetWithVariable("PutItemIn", "[itemname]", Character.Controlled.FocusedItem.Name, FormatCapitals.Yes) : - TextManager.Get(Screen.Selected is SubEditorScreen editor && editor.EntityMenu.Rect.Contains(PlayerInput.MousePosition) ? "Delete" : "DropItem"); + + (LocalizedString toolTip, Color toolTipColor) = GetDragLabelTextAndColor(mouseOnHealthInterface); Vector2 nameSize = GUIStyle.Font.MeasureString(DraggingItems.First().Name); Vector2 toolTipSize = GUIStyle.SmallFont.MeasureString(toolTip); @@ -1544,7 +1605,7 @@ namespace Barotrauma GUI.DrawString(spriteBatch, textPos + new Vector2(nameSize.X * textOffset, -iconSize / 2), DraggingItems.First().Name, Color.White); GUI.DrawString(spriteBatch, textPos + new Vector2(toolTipSize.X * textOffset, 0), toolTip, - color: Character.Controlled.FocusedItem == null && !mouseOnHealthInterface ? GUIStyle.Red : Color.LightGreen, + color: toolTipColor, font: GUIStyle.SmallFont); } @@ -1587,24 +1648,31 @@ namespace Barotrauma } } - if (selectedSlot != null && selectedSlot.Item != null) + (LocalizedString, Color) GetDragLabelTextAndColor(bool mouseOnHealthInterface) { - Rectangle slotRect = selectedSlot.Slot.Rect; - slotRect.Location += selectedSlot.Slot.DrawOffset.ToPoint(); - if (selectedSlot.TooltipNeedsRefresh()) + bool useDragDropGive = IsValidTargetForDragDropGive(Character.Controlled, Character.Controlled.FocusedCharacter); + + Color toolTipColor = Color.LightGreen; + + LocalizedString toolTip; + if (mouseOnHealthInterface) { - selectedSlot.RefreshTooltip(); + toolTip = TextManager.Get("QuickUseAction.UseTreatment"); } - - if (!slotIconTooltip.IsNullOrEmpty()) + else if (Character.Controlled.FocusedItem != null) { - DrawToolTip(spriteBatch, slotIconTooltip, slotRect); + toolTip = TextManager.GetWithVariable("PutItemIn", "[itemname]", Character.Controlled.FocusedItem.Name, FormatCapitals.Yes); + } + else if (useDragDropGive) + { + toolTip = TextManager.GetWithVariable("GiveItemTo", "[character]", Character.Controlled.FocusedCharacter.Name, FormatCapitals.Yes); } else { - DrawToolTip(spriteBatch, selectedSlot.Tooltip, slotRect); + toolTipColor = GUIStyle.Red; + toolTip = TextManager.Get(Screen.Selected is SubEditorScreen editor && editor.EntityMenu.Rect.Contains(PlayerInput.MousePosition) ? "Delete" : "DropItem"); } - slotIconTooltip = string.Empty; + return (toolTip, toolTipColor); } } @@ -1801,7 +1869,7 @@ namespace Barotrauma DrawSideIcon(deconstructOrder.SymbolSprite, Direction.Right, TextManager.Get("tooltip.markedfordeconstruction"), GUIStyle.Red, out bool mouseOn); if (mouseOn) { availableContextualOrder = (item, Tags.DontDeconstructThis); } } - else if (((item.SpawnedInCurrentOutpost && !item.AllowStealing) || (inventory != null && inventory.slots[slotIndex].Items.Any(it => it.SpawnedInCurrentOutpost && !it.AllowStealing))) && CharacterInventory.LimbSlotIcons.ContainsKey(InvSlotType.LeftHand)) + else if ((item.Illegitimate || (inventory != null && inventory.slots[slotIndex].Items.Any(it => it.Illegitimate))) && CharacterInventory.LimbSlotIcons.ContainsKey(InvSlotType.LeftHand)) { DrawSideIcon(CharacterInventory.LimbSlotIcons[InvSlotType.LeftHand], Direction.Left, TextManager.Get("tooltip.stolenitem"), GUIStyle.Red, out _); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 5ff674fb4..c615ed2f2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -326,7 +326,7 @@ namespace Barotrauma public void Draw(SpriteBatch spriteBatch, bool editing, bool back = true, Color? overrideColor = null) { - if (!Visible || (!editing && HiddenInGame) || !SubEditorScreen.IsLayerVisible(this)) { return; } + if (!Visible || (!editing && IsHidden) || !SubEditorScreen.IsLayerVisible(this)) { return; } if (editing) { @@ -424,7 +424,7 @@ namespace Barotrauma textureScale: Vector2.One * Scale, depth: d); } - DrawDecorativeSprites(spriteBatch, DrawPosition, flippedX && Prefab.CanSpriteFlipX, flippedY && Prefab.CanSpriteFlipY, rotation: 0, depth); + DrawDecorativeSprites(spriteBatch, DrawPosition, flippedX && Prefab.CanSpriteFlipX, flippedY && Prefab.CanSpriteFlipY, rotation: 0, depth, overrideColor); } } else @@ -445,7 +445,7 @@ namespace Barotrauma Prefab.DamagedInfectedSprite?.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, Infector.HealthColor, Prefab.DamagedInfectedSprite.Origin, RotationRad, Scale, activeSprite.effects, depth - 0.002f); } - DrawDecorativeSprites(spriteBatch, DrawPosition, flippedX && Prefab.CanSpriteFlipX, flippedY && Prefab.CanSpriteFlipY, -RotationRad, depth); + DrawDecorativeSprites(spriteBatch, DrawPosition, flippedX && Prefab.CanSpriteFlipX, flippedY && Prefab.CanSpriteFlipY, -RotationRad, depth, overrideColor); } } else if (body.Enabled) @@ -456,30 +456,49 @@ namespace Barotrauma //don't draw the item on hands if it's also being worn if (GetComponent() is { IsActive: true }) { return; } if (!back) { return; } - float depthStep = 0.000001f; if (holdable.Picker.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == this) { - Limb holdLimb = holdable.Picker.AnimController.GetLimb(LimbType.RightArm); - if (holdLimb?.ActiveSprite != null) - { - depth = holdLimb.ActiveSprite.Depth + holdable.Picker.AnimController.GetDepthOffset() + depthStep * 2; - foreach (WearableSprite wearableSprite in holdLimb.WearingItems) - { - if (!wearableSprite.InheritLimbDepth && wearableSprite.Sprite != null) { depth = Math.Max(wearableSprite.Sprite.Depth + depthStep, depth); } - } - } + depth = GetHeldItemDepth(LimbType.RightHand, depth); } else if (holdable.Picker.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand) == this) { - Limb holdLimb = holdable.Picker.AnimController.GetLimb(LimbType.LeftArm); + depth = GetHeldItemDepth(LimbType.LeftHand, depth); + } + + float GetHeldItemDepth(LimbType limb, float depth) + { + //offset used to make sure the item draws just slightly behind the right hand, or slightly in front of the left hand + float limbDepthOffset = 0.000001f; + float depthOffset = holdable.Picker.AnimController.GetDepthOffset(); + //use the upper arm as a reference, to ensure the item gets drawn behind / in front of the whole arm (not just the forearm) + Limb holdLimb = holdable.Picker.AnimController.GetLimb(limb == LimbType.RightHand ? LimbType.RightArm : LimbType.LeftArm); if (holdLimb?.ActiveSprite != null) { - depth = holdLimb.ActiveSprite.Depth + holdable.Picker.AnimController.GetDepthOffset() - depthStep * 2; + depth = + holdLimb.ActiveSprite.Depth + + depthOffset + + limbDepthOffset * 2 * (limb == LimbType.RightHand ? 1 : -1); foreach (WearableSprite wearableSprite in holdLimb.WearingItems) { - if (!wearableSprite.InheritLimbDepth && wearableSprite.Sprite != null) { depth = Math.Min(wearableSprite.Sprite.Depth - depthStep, depth); } + if (!wearableSprite.InheritLimbDepth && wearableSprite.Sprite != null) + { + depth = + limb == LimbType.RightHand ? + Math.Max(wearableSprite.Sprite.Depth + limbDepthOffset, depth) : + Math.Min(wearableSprite.Sprite.Depth - limbDepthOffset, depth); + } + } + var head = holdable.Picker.AnimController.GetLimb(LimbType.Head); + if (head != null) + { + //ensure the holdable item is always drawn in front of the head no matter what the wearables or whatnot do with the sprite depths + depth = + limb == LimbType.RightHand ? + Math.Min(head.Sprite.Depth + depthOffset - limbDepthOffset, depth) : + Math.Max(head.Sprite.Depth + depthOffset + limbDepthOffset, depth); } } + return depth; } } Vector2 origin = GetSpriteOrigin(activeSprite); @@ -489,7 +508,7 @@ namespace Barotrauma float d = Math.Min(depth + (fadeInBrokenSprite.Sprite.Depth - activeSprite.Depth - 0.000001f), 0.999f); body.Draw(spriteBatch, fadeInBrokenSprite.Sprite, color * fadeInBrokenSpriteAlpha, d, Scale); } - DrawDecorativeSprites(spriteBatch, body.DrawPosition, flipX: body.Dir < 0, flipY: false, rotation: body.Rotation, depth: depth); + DrawDecorativeSprites(spriteBatch, body.DrawPosition, flipX: body.Dir < 0, flipY: false, rotation: body.Rotation, depth, overrideColor); } foreach (var upgrade in Upgrades) @@ -617,11 +636,11 @@ namespace Barotrauma } } - public void DrawDecorativeSprites(SpriteBatch spriteBatch, Vector2 drawPos, bool flipX, bool flipY, float rotation, float depth) + public void DrawDecorativeSprites(SpriteBatch spriteBatch, Vector2 drawPos, bool flipX, bool flipY, float rotation, float depth, Color? overrideColor = null) { foreach (var decorativeSprite in Prefab.DecorativeSprites) { - Color decorativeSpriteColor = GetSpriteColor(decorativeSprite.Color).Multiply(GetSpriteColor(spriteColor)); + Color decorativeSpriteColor = overrideColor ?? GetSpriteColor(decorativeSprite.Color).Multiply(GetSpriteColor(spriteColor)); if (!spriteAnimState[decorativeSprite].IsActive) { continue; } Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs index e12c9b41a..419253352 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs @@ -215,7 +215,9 @@ namespace Barotrauma } else { - if (Math.Sign(flowTargetHull.Rect.Y - rect.Y) != Math.Sign(lerpedFlowForce.Y)) { return; } + //do not emit particles unless water is flowing towards the target hull + //(using lerpedFlowForce smooths out "flickers" when the direction of flow is rapidly changing) + if (Math.Sign(flowTargetHull.WorldPosition.Y - WorldPosition.Y) != Math.Sign(lerpedFlowForce.Y)) { return; } float particlesPerSec = Math.Max(open * rect.Width * particleAmountMultiplier, 10.0f); float emitInterval = 1.0f / particlesPerSec; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index d2deb27cb..5841d8dc6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -210,7 +210,9 @@ namespace Barotrauma { bool primaryMouseButtonHeld = PlayerInput.PrimaryMouseButtonHeld(); bool secondaryMouseButtonHeld = PlayerInput.SecondaryMouseButtonHeld(); - if (!primaryMouseButtonHeld && !secondaryMouseButtonHeld) { return; } + bool doubleClicked = PlayerInput.DoubleClicked(); + bool secondaryDoubleClicked = PlayerInput.SecondaryDoubleClicked(); + if (!primaryMouseButtonHeld && !secondaryMouseButtonHeld && !doubleClicked && !secondaryDoubleClicked) { return; } Vector2 position = cam.ScreenToWorld(PlayerInput.MousePosition); Hull hull = FindHull(position); @@ -218,29 +220,67 @@ namespace Barotrauma if (hull == null || hull.IdFreed) { return; } if (EditWater) { + const float waterIncrement = 100000.0f; if (primaryMouseButtonHeld) { - ShowHulls = true; - hull.WaterVolume += 100000.0f * deltaTime; - hull.networkUpdatePending = true; - hull.serverUpdateDelay = 0.5f; + SetWaterVolume(hull.WaterVolume + waterIncrement * deltaTime); } else if (secondaryMouseButtonHeld) { - hull.WaterVolume -= 100000.0f * deltaTime; + SetWaterVolume(hull.WaterVolume - waterIncrement * deltaTime); + } + + if (doubleClicked) + { + SetWaterVolume(hull.Volume * MaxCompress); + } + else if (secondaryDoubleClicked) + { + SetWaterVolume(0f); + } + + void SetWaterVolume(float newVolume) + { + ShowHulls = true; + hull.WaterVolume = newVolume; hull.networkUpdatePending = true; hull.serverUpdateDelay = 0.5f; } - } else if (EditFire) { + bool networkUpdate = false; + if (primaryMouseButtonHeld) { new FireSource(position, hull, isNetworkMessage: true); + networkUpdate = true; + } + else if (secondaryMouseButtonHeld || secondaryDoubleClicked) + { + for (int index = hull.FireSources.Count - 1; index >= 0; index--) + { + var currentFireSource = hull.FireSources[index]; + + if (secondaryMouseButtonHeld) + { + const float extinguishAmount = 120f; + currentFireSource.Extinguish(deltaTime, extinguishAmount); + networkUpdate = true; + } + else + { + currentFireSource.Remove(); + networkUpdate = true; + } + } + } + + if (networkUpdate) + { hull.networkUpdatePending = true; hull.serverUpdateDelay = 0.5f; - } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index 2a60446e1..9bed09947 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -1,4 +1,4 @@ -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System.Collections.Generic; using System.Linq; @@ -70,7 +70,7 @@ namespace Barotrauma.Lights public bool LightingEnabled = true; - public bool ObstructVision; + public float ObstructVisionAmount; private readonly Texture2D visionCircle; @@ -498,7 +498,7 @@ namespace Barotrauma.Lights { foreach (MapEntity e in (Submarine.VisibleEntities ?? MapEntity.MapEntityList)) { - if (e is Item item && !item.HiddenInGame && item.GetComponent() is Wire wire) + if (e is Item item && !item.IsHidden && item.GetComponent() is Wire wire) { wire.DebugDraw(spriteBatch, alpha: 0.4f); } @@ -664,7 +664,7 @@ namespace Barotrauma.Lights visibleHulls.Clear(); foreach (Hull hull in Hull.HullList) { - if (hull.HiddenInGame) { continue; } + if (hull.IsHidden) { continue; } var drawRect = hull.Submarine == null ? hull.Rect : @@ -682,12 +682,12 @@ namespace Barotrauma.Lights public void UpdateObstructVision(GraphicsDevice graphics, SpriteBatch spriteBatch, Camera cam, Vector2 lookAtPosition) { - if ((!LosEnabled || LosMode == LosMode.None) && !ObstructVision) { return; } + if ((!LosEnabled || LosMode == LosMode.None) && ObstructVisionAmount <= 0.0f) { return; } if (ViewTarget == null) return; graphics.SetRenderTarget(LosTexture); - if (ObstructVision) + if (ObstructVisionAmount > 0.0f) { graphics.Clear(Color.Black); Vector2 diff = lookAtPosition - ViewTarget.WorldPosition; @@ -697,13 +697,14 @@ namespace Barotrauma.Lights //the visible area stretches to the maximum when the cursor is this far from the character const float MaxOffset = 256.0f; - const float MinHorizontalScale = 2.2f; - const float MaxHorizontalScale = 2.8f; - const float VerticalScale = 2.5f; + //the magic numbers here are just based on experimentation + float MinHorizontalScale = MathHelper.Lerp(3.5f, 1.5f, ObstructVisionAmount); + float MaxHorizontalScale = MinHorizontalScale * 1.25f; + float VerticalScale = MathHelper.Lerp(4.0f, 1.25f, ObstructVisionAmount); //Starting point and scale-based modifier that moves the point of origin closer to the edge of the texture if the player moves their mouse further away, or vice versa. - float relativeOriginStartPosition = 0.22f; //Increasing this value moves the origin further behind the character - float originStartPosition = visionCircle.Width * relativeOriginStartPosition; + float relativeOriginStartPosition = 0.1f; //Increasing this value moves the origin further behind the character + float originStartPosition = visionCircle.Width * relativeOriginStartPosition * MinHorizontalScale; float relativeOriginLookAtPosModifier = -0.055f; //Increase this value increases how much the vision changes by moving the mouse float originLookAtPosModifier = visionCircle.Width * relativeOriginLookAtPosModifier; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index 74eba1d6d..a29588def 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -1115,13 +1115,23 @@ namespace Barotrauma float subCrushDepth = SubmarineInfo.GetSubCrushDepth(SubmarineSelection.CurrentOrPendingSubmarine(), ref pendingSubInfo); string crushDepthWarningIconStyle = null; - if (connection.LevelData.InitialDepth * Physics.DisplayToRealWorldRatio > subCrushDepth) + + var levelData = connection.LevelData; + float spawnDepth = + levelData.InitialDepth + + //base the warning on the start or end position of the level, whichever is deeper + levelData.Size.Y * Math.Max(levelData.GenerationParams.StartPosition.Y, levelData.GenerationParams.EndPosition.Y); + + //"high warning" if the sub spawns at/below crush depth + if (spawnDepth * Physics.DisplayToRealWorldRatio > subCrushDepth) { iconCount++; crushDepthWarningIconStyle = "CrushDepthWarningHighIcon"; tooltip = "crushdepthwarninghigh"; } - else if ((connection.LevelData.InitialDepth + connection.LevelData.Size.Y) * Physics.DisplayToRealWorldRatio > subCrushDepth) + //"low warning" if the spawn position is less than the level's height away from crush depth + //(i.e. the crush depth is pretty close to the spawn pos, possibly inside the level or at least close enough that many parts of the abyss are unreachable) + else if ((spawnDepth + connection.LevelData.Size.Y) * Physics.DisplayToRealWorldRatio > subCrushDepth) { iconCount++; crushDepthWarningIconStyle = "CrushDepthWarningLowIcon"; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index 1db65ab0d..5f012c8a0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -382,7 +382,10 @@ namespace Barotrauma if (!HasBody && !ShowStructures) { return; } if (HasBody && !ShowWalls) { return; } } - else if (HiddenInGame) { return; } + else if (IsHidden) + { + return; + } Color color = IsIncludedInSelection && editing ? GUIStyle.Blue : IsHighlighted ? GUIStyle.Orange * Math.Max(spriteColor.A / (float) byte.MaxValue, 0.1f) : spriteColor; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs index 264d0aebc..456dbcc0c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs @@ -20,13 +20,13 @@ namespace Barotrauma public override bool SelectableInEditor { - get { return !IsHidden(); } + get { return ShouldDrawIcon(); } } public override void Draw(SpriteBatch spriteBatch, bool editing, bool back = true) { if (!editing && (!GameMain.DebugDraw || Screen.Selected.Cam.Zoom < 0.1f)) { return; } - if (IsHidden()) { return; } + if (!ShouldDrawIcon()) { return; } Vector2 drawPos = Position; if (Submarine != null) { drawPos += Submarine.DrawPosition; } @@ -59,8 +59,10 @@ namespace Barotrauma Color.White); } - Sprite sprite = iconSprites[SpawnType.ToString()]; Sprite sprite2 = null; + //there are no sprites for all possible combinations of SpawnType flags, but in the vanilla game the only possible combination is + //SpawnType.Disabled + some other flag, in which case it's fine to just not show the icon. + iconSprites.TryGetValue(SpawnType.ToString(), out Sprite sprite); if (spawnType == SpawnType.Human && AssignedJob?.Icon != null) { sprite = iconSprites["Path"]; @@ -87,9 +89,12 @@ namespace Barotrauma sprite = iconSprites["Ladder"]; } - float spriteScale = iconSize / (float)sprite.SourceRect.Width; - sprite.Draw(spriteBatch, drawPos, clr, origin: sprite.size / 2, scale: spriteScale, depth: 0.001f); - sprite2?.Draw(spriteBatch, drawPos + sprite.size * spriteScale * 0.5f, clr, origin: sprite2.size / 2, scale: spriteScale, depth: 0.001f); + if (sprite != null) + { + float spriteScale = iconSize / (float)sprite.SourceRect.Width; + sprite.Draw(spriteBatch, drawPos, clr, origin: sprite.size / 2, scale: spriteScale, depth: 0.001f); + sprite2?.Draw(spriteBatch, drawPos + sprite.size * spriteScale * 0.5f, clr, origin: sprite2.size / 2, scale: spriteScale, depth: 0.001f); + } if (spawnType == SpawnType.Human && AssignedJob?.Icon != null) { @@ -160,22 +165,22 @@ namespace Barotrauma public override bool IsMouseOn(Vector2 position) { - if (IsHidden()) { return false; } + if (!ShouldDrawIcon()) { return false; } float dist = Vector2.DistanceSquared(position, WorldPosition); float radius = (SpawnType == SpawnType.Path ? WaypointSize : SpawnPointSize) * 0.6f; return dist < radius * radius; } - private bool IsHidden() + private bool ShouldDrawIcon() { - if (!SubEditorScreen.IsLayerVisible(this)) { return true; } + if (!SubEditorScreen.IsLayerVisible(this)) { return false; } if (spawnType == SpawnType.Path) { - return (!GameMain.DebugDraw && !ShowWayPoints); + return GameMain.DebugDraw || ShowWayPoints; } else { - return (!GameMain.DebugDraw && !ShowSpawnPoints); + return GameMain.DebugDraw || ShowSpawnPoints; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs index cab954d3f..bbf6fa312 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs @@ -30,6 +30,7 @@ namespace Barotrauma.Networking txt = msg.ReadString(); string senderName = msg.ReadString(); + Entity sender = null; Character senderCharacter = null; Client senderClient = null; bool hasSenderClient = msg.ReadBoolean(); @@ -40,13 +41,14 @@ namespace Barotrauma.Networking => c.SessionOrAccountIdMatches(userId)); if (senderClient != null) { senderName = senderClient.Name; } } - bool hasSenderCharacter = msg.ReadBoolean(); - if (hasSenderCharacter) + bool hasSender = msg.ReadBoolean(); + if (hasSender) { - senderCharacter = Entity.FindEntityByID(msg.ReadUInt16()) as Character; - if (senderCharacter != null) + sender = Entity.FindEntityByID(msg.ReadUInt16()); + senderCharacter = sender as Character; + if (sender is Character or Item) { - senderName = senderCharacter.Name; + senderName = OrderChatMessage.NameFromEntityOrNull(sender); } } @@ -180,7 +182,7 @@ namespace Barotrauma.Networking GameMain.Client.ServerSettings.ServerLog?.WriteLine(txt, messageType); break; default: - GameMain.Client.AddChatMessage(txt, type, senderName, senderClient, senderCharacter, changeType, textColor: textColor); + GameMain.Client.AddChatMessage(txt, type, senderName, senderClient, sender, changeType, textColor: textColor); if (type == ChatMessageType.Radio && CanUseRadio(senderCharacter, out WifiComponent radio)) { Signal s = new Signal(txt, sender: senderCharacter, source: radio.Item); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 27dc9eca4..2f0b34bf4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -21,6 +21,10 @@ namespace Barotrauma.Networking public override bool IsClient => true; public override bool IsServer => false; + +#if DEBUG + public float DebugServerVoipAmplitude; +#endif public override Voting Voting { get; } @@ -112,6 +116,8 @@ namespace Barotrauma.Networking //has the client been given a character to control this round public bool HasSpawned; + public float EndRoundTimeRemaining { get; private set; } + public LocalizedString TraitorFirstObjective; public TraitorEventPrefab TraitorMission = null; @@ -198,20 +204,6 @@ namespace Barotrauma.Networking CanBeFocused = false }; - cameraFollowsSub = new GUITickBox(new RectTransform(new Vector2(0.05f, 0.05f), inGameHUD.RectTransform, anchor: Anchor.TopCenter, pivot: Pivot.CenterLeft) - { - AbsoluteOffset = new Point(0, HUDLayoutSettings.ButtonAreaTop.Y + HUDLayoutSettings.ButtonAreaTop.Height / 2), - MaxSize = new Point(GUI.IntScale(25)) - }, TextManager.Get("CamFollowSubmarine")) - { - Selected = Camera.FollowSub, - OnSelected = (tbox) => - { - Camera.FollowSub = tbox.Selected; - return true; - } - }; - chatBox = new ChatBox(inGameHUD, isSinglePlayer: false); chatBox.OnEnterMessage += EnterChatMessage; chatBox.InputBox.OnTextChanged += TypingChatMessage; @@ -250,6 +242,19 @@ namespace Barotrauma.Networking } }; ShowLogButton.TextBlock.AutoScaleHorizontal = true; + + cameraFollowsSub = new GUITickBox(new RectTransform(new Vector2(0.1f, 0.4f), buttonContainer.RectTransform) + { + MinSize = new Point(150, 0) + }, TextManager.Get("CamFollowSubmarine")) + { + Selected = Camera.FollowSub, + OnSelected = (tbox) => + { + Camera.FollowSub = tbox.Selected; + return true; + } + }; GameMain.DebugDraw = false; Hull.EditFire = false; @@ -674,6 +679,11 @@ namespace Barotrauma.Networking VoipClient.Read(inc); break; +#if DEBUG + case ServerPacketHeader.VOICE_AMPLITUDE_DEBUG: + GameMain.Client.DebugServerVoipAmplitude = inc.ReadRangedSingle(min: 0, max: 1, bitCount: 8); + break; +#endif case ServerPacketHeader.QUERY_STARTGAME: DebugConsole.Log("Received QUERY_STARTGAME packet."); string subName = inc.ReadString(); @@ -1382,6 +1392,7 @@ namespace Barotrauma.Networking ServerSettings.AllowRewiring = inc.ReadBoolean(); ServerSettings.AllowImmediateItemDelivery = inc.ReadBoolean(); ServerSettings.AllowFriendlyFire = inc.ReadBoolean(); + ServerSettings.AllowDragAndDropGive = inc.ReadBoolean(); ServerSettings.LockAllDefaultWires = inc.ReadBoolean(); ServerSettings.AllowLinkingWifiToChat = inc.ReadBoolean(); ServerSettings.MaximumMoneyTransferRequest = inc.ReadInt32(); @@ -1812,6 +1823,8 @@ namespace Barotrauma.Networking GameStarted = inc.ReadBoolean(); bool allowSpectating = inc.ReadBoolean(); + bool permadeathMode = inc.ReadBoolean(); + bool ironmanMode = inc.ReadBoolean(); ReadPermissions(inc); @@ -1819,8 +1832,17 @@ namespace Barotrauma.Networking { if (Screen.Selected != GameMain.GameScreen) { - new GUIMessageBox(TextManager.Get("PleaseWait"), TextManager.Get(allowSpectating ? "RoundRunningSpectateEnabled" : "RoundRunningSpectateDisabled")); - if (Screen.Selected is not ModDownloadScreen) { GameMain.NetLobbyScreen.Select(); } + LocalizedString message; + if (permadeathMode) + { + message = TextManager.Get(ironmanMode ? "RoundRunningIronman" : "RoundRunningPermadeath"); + } + else + { + message = TextManager.Get(allowSpectating ? "RoundRunningSpectateEnabled" : "RoundRunningSpectateDisabled"); + } + new GUIMessageBox(TextManager.Get("PleaseWait"), message); + if (!(Screen.Selected is ModDownloadScreen)) { GameMain.NetLobbyScreen.Select(); } } } } @@ -2103,6 +2125,8 @@ namespace Barotrauma.Networking float sendingTime = inc.ReadSingle() - 0.0f;//TODO: reimplement inc.SenderConnection.RemoteTimeOffset; + EndRoundTimeRemaining = inc.ReadSingle(); + SegmentTableReader.Read(inc, segmentDataReader: (segment, inc) => { @@ -2373,7 +2397,16 @@ namespace Barotrauma.Networking WaitForNextRoundRespawn = waitForNextRoundRespawn; IWriteMessage msg = new WriteOnlyMessage(); msg.WriteByte((byte)ClientPacketHeader.READY_TO_SPAWN); - msg.WriteBoolean((bool)waitForNextRoundRespawn); + msg.WriteBoolean(GameMain.NetLobbyScreen.Spectating); + msg.WriteBoolean(waitForNextRoundRespawn); + ClientPeer?.Send(msg, DeliveryMethod.Reliable); + } + + public void SendTakeOverBotRequest(CharacterInfo bot) + { + IWriteMessage msg = new WriteOnlyMessage(); + msg.WriteByte((byte)ClientPacketHeader.TAKEOVERBOT); + msg.WriteUInt16(bot.ID); ClientPeer?.Send(msg, DeliveryMethod.Reliable); } @@ -2704,16 +2737,19 @@ namespace Barotrauma.Networking public override void AddChatMessage(ChatMessage message) { if (string.IsNullOrEmpty(message.Text)) { return; } - if (message.Sender != null && !message.Sender.IsDead) + if (message.SenderCharacter is { IsDead: false } sender) { if (message.Text.IsNullOrEmpty()) { - message.Sender.ShowTextlessSpeechBubble(2.0f, message.Color); - + sender.ShowTextlessSpeechBubble(2.0f, message.Color); } else { - message.Sender.ShowSpeechBubble(message.Color, message.Text); + sender.ShowSpeechBubble(message.Color, message.Text); + if (!sender.IsBot) + { + sender.TextChatVolume = 1f; + } } } GameMain.NetLobbyScreen.NewChatMessage(message); @@ -3197,19 +3233,19 @@ namespace Barotrauma.Networking { LocalizedString 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); + bool hideRespawnButtons = false; + + if (EndRoundTimeRemaining > 0) + { + respawnText = TextManager.GetWithVariable("endinground", "[time]", ToolBox.SecondsToReadableTime(EndRoundTimeRemaining)) + .Fallback(ToolBox.SecondsToReadableTime(EndRoundTimeRemaining), useDefaultLanguageIfFound: false); + } if (RespawnManager.CurrentState == RespawnManager.State.Waiting) { if (RespawnManager.RespawnCountdownStarted) { float timeLeft = (float)(RespawnManager.RespawnTime - DateTime.Now).TotalSeconds; - respawnText = TextManager.GetWithVariable( - RespawnManager.UsingShuttle && !RespawnManager.ForceSpawnInMainSub ? - "RespawnShuttleDispatching" : "RespawningIn", "[time]", ToolBox.SecondsToReadableTime(timeLeft)); + respawnText = TextManager.GetWithVariable("RespawningIn", "[time]", ToolBox.SecondsToReadableTime(timeLeft)); } else if (RespawnManager.PendingRespawnCount > 0) { @@ -3232,12 +3268,12 @@ namespace Barotrauma.Networking //textScale = 1.0f + phase * 0.5f; textColor = Color.Lerp(GUIStyle.Red, Color.White, 1.0f - phase); } - canChooseRespawn = false; + hideRespawnButtons = true; } - GameMain.GameSession?.SetRespawnInfo( - visible: !respawnText.IsNullOrEmpty() || canChooseRespawn, text: respawnText.Value, textColor: textColor, - buttonsVisible: canChooseRespawn, waitForNextRoundRespawn: (WaitForNextRoundRespawn ?? true)); + GameMain.GameSession.SetRespawnInfo( + text: respawnText.Value, textColor: textColor, + waitForNextRoundRespawn: (WaitForNextRoundRespawn ?? true), hideButtons: hideRespawnButtons); } if (!ShowNetStats) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs index bd219b5f5..e4f182e02 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; namespace Barotrauma.Networking { @@ -23,6 +22,14 @@ namespace Barotrauma.Networking get; private set; } + public static void ShowDeathPromptIfNeeded(float delay = 1.0f) + { + if (UseDeathPrompt) + { + DeathPrompt.Create(delay); + } + } + partial void UpdateTransportingProjSpecific(float deltaTime) { if (GameMain.Client?.Character == null || GameMain.Client.Character.Submarine != RespawnShuttle) { return; } @@ -37,72 +44,10 @@ namespace Barotrauma.Networking } } - private CoroutineHandle respawnPromptCoroutine; - - public void ShowRespawnPromptIfNeeded(float delay = 5.0f) - { - if (!UseRespawnPrompt) { return; } - if (CoroutineManager.IsCoroutineRunning(respawnPromptCoroutine) || GUIMessageBox.MessageBoxes.Any(mb => mb.UserData as string == "respawnquestionprompt")) - { - return; - } - - respawnPromptCoroutine = CoroutineManager.Invoke(() => - { - if (Character.Controlled != null || (GameMain.GameSession is not { IsRunning: true })) { return; } - - LocalizedString text; - GUIMessageBox respawnPrompt; - if (SkillLossPercentageOnImmediateRespawn > 0) - { - // Respawn asap with extra skill loss? - text = TextManager.GetWithVariable("respawnquestionprompt", "[percentage]", ((int)Math.Round(SkillLossPercentageOnImmediateRespawn)).ToString()); - respawnPrompt = new GUIMessageBox( - TextManager.Get("tutorial.tryagainheader"), text, - new LocalizedString[] { TextManager.Get("respawnquestionpromptrespawn"), TextManager.Get("respawnquestionpromptwait") }) - { - UserData = "respawnquestionprompt" - }; - } - else - { - // Respawn asap? - text = TextManager.Get("respawnquestionpromptnoloss"); - respawnPrompt = new GUIMessageBox( - TextManager.Get("tutorial.tryagainheader"), text, - new LocalizedString[] { TextManager.Get("respawnquestionpromptrespawnnoloss"), TextManager.Get("respawnquestionpromptwait") }) - { - UserData = "respawnquestionprompt" - }; - } - if (SkillLossPercentageOnDeath > 0) - { - // You have died... etc added BEFORE the above text - text = - TextManager.GetWithVariable("respawnskillpenalty", "[percentage]", ((int)SkillLossPercentageOnDeath).ToString()) + - "\n\n" + text; - }; - - 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: delay); - } - public void ClientEventRead(IReadMessage msg, float sendingTime) { bool respawnPromptPending = false; var newState = (State)msg.ReadRangedInteger(0, Enum.GetNames(typeof(State)).Length); - ForceSpawnInMainSub = false; switch (newState) { case State.Transporting: @@ -122,7 +67,6 @@ namespace Barotrauma.Networking RequiredRespawnCount = msg.ReadUInt16(); respawnPromptPending = msg.ReadBoolean(); RespawnCountdownStarted = msg.ReadBoolean(); - ForceSpawnInMainSub = msg.ReadBoolean(); ResetShuttle(); float newRespawnTime = msg.ReadSingle(); RespawnTime = DateTime.Now + new TimeSpan(0, 0, 0, 0, milliseconds: (int)(newRespawnTime * 1000.0f)); @@ -136,7 +80,7 @@ namespace Barotrauma.Networking if (respawnPromptPending) { GameMain.Client.HasSpawned = true; - ShowRespawnPromptIfNeeded(delay: 1.0f); + DeathPrompt.Create(delay: 1.0f); } msg.ReadPadBits(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettingsUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettingsUI.cs index 0aad3ebfa..f2d99e79d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettingsUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettingsUI.cs @@ -468,11 +468,28 @@ namespace Barotrauma.Networking }; AssignGUIComponent(nameof(SaveServerLogs), saveLogsBox); + LocalizedString newCampaignDefaultSalaryLabel = TextManager.Get("ServerSettingsNewCampaignDefaultSalary"); + NetLobbyScreen.CreateLabeledSlider(listBox.Content, headerTag: "ServerSettingsNewCampaignDefaultSalary", valueLabelTag: "ServerSettingsKickVotesRequired", tooltipTag: "ServerSettingsNewCampaignDefaultSalaryToolTip", + out var defaultSalarySlider, out var defaultSalarySliderLabel); + defaultSalarySlider.Range = new Vector2(0, 100); + defaultSalarySlider.StepValue = 1; + defaultSalarySlider.OnMoved = (scrollBar, _) => + { + if (scrollBar.UserData is not GUITextBlock text) { return false; } + text.Text = TextManager.AddPunctuation( + ':', + newCampaignDefaultSalaryLabel, + TextManager.GetWithVariable("percentageformat", "[value]", ((int)Math.Round(scrollBar.BarScrollValue, digits: 0)).ToString())); + return true; + }; + AssignGUIComponent(nameof(NewCampaignDefaultSalary), defaultSalarySlider); + defaultSalarySlider.OnMoved(defaultSalarySlider, defaultSalarySlider.BarScroll); + //-------------------------------------------------------------------------------- // game settings //-------------------------------------------------------------------------------- - GUILayoutGroup buttonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), listBox.Content.RectTransform), isHorizontal: true) + GUILayoutGroup buttonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), listBox.Content.RectTransform), isHorizontal: true, childAnchor: Anchor.BottomLeft) { Stretch = true, RelativeSpacing = 0.05f @@ -683,7 +700,7 @@ namespace Barotrauma.Networking // antigriefing //-------------------------------------------------------------------------------- - var tickBoxContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.25f), listBox.Content.RectTransform)) + var tickBoxContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.268f), listBox.Content.RectTransform)) { AutoHideScrollBar = true, UseGridLayout = true @@ -693,6 +710,10 @@ namespace Barotrauma.Networking var allowFriendlyFire = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), TextManager.Get("ServerSettingsAllowFriendlyFire")); AssignGUIComponent(nameof(AllowFriendlyFire), allowFriendlyFire); + + var allowDragAndDropGive = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), + TextManager.Get("ServerSettingsAllowDragAndDropGive")); + AssignGUIComponent(nameof(AllowDragAndDropGive), allowDragAndDropGive); var killableNPCs = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), TextManager.Get("ServerSettingsKillableNPCs")); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipConfig.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipConfig.cs index 1d9639fc0..2774125b0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipConfig.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipConfig.cs @@ -1,19 +1,10 @@ using Concentus.Enums; using Concentus.Structs; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Barotrauma.Networking { static partial class VoipConfig { - public const int FREQUENCY = 48000; //48Khz - public const int BITRATE = 16000; //16Kbps - public const int BUFFER_SIZE = (8 * MAX_COMPRESSED_SIZE * FREQUENCY) / BITRATE; //20ms window - public static OpusEncoder CreateEncoder() { var encoder = new OpusEncoder(FREQUENCY, 1, OpusApplication.OPUS_APPLICATION_VOIP); @@ -22,10 +13,5 @@ namespace Barotrauma.Networking encoder.SignalType = OpusSignal.OPUS_SIGNAL_VOICE; return encoder; } - - public static OpusDecoder CreateDecoder() - { - return new OpusDecoder(FREQUENCY, 1); - } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs b/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs index 07ebfee4b..75d21f895 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs @@ -228,8 +228,11 @@ namespace Barotrauma static MouseState latestMouseState; //the absolute latest state, do NOT use for player interaction static KeyboardState keyboardState, oldKeyboardState; - static double timeSinceClick; - static Point lastClickPosition; + static double timeSincePrimaryClick; + static Point lastPrimaryClickPosition; + + static double timeSinceSecondaryClick; + static Point lastSecondaryClickPosition; const float DoubleClickDelay = 0.4f; public static float MaxDoubleClickDistance @@ -237,7 +240,8 @@ namespace Barotrauma get { return Math.Max(15.0f * Math.Max(GameMain.GraphicsHeight / 1920.0f, GameMain.GraphicsHeight / 1080.0f), 10.0f); } } - static bool doubleClicked; + static bool primaryDoubleClicked; + static bool secondaryDoubleClicked; static bool allowInput; static bool wasWindowActive; @@ -406,7 +410,12 @@ namespace Barotrauma public static bool DoubleClicked() { - return AllowInput && doubleClicked; + return AllowInput && primaryDoubleClicked; + } + + public static bool SecondaryDoubleClicked() + { + return AllowInput && secondaryDoubleClicked; } public static bool KeyHit(InputType inputType) @@ -466,7 +475,8 @@ namespace Barotrauma public static void Update(double deltaTime) { - timeSinceClick += deltaTime; + timeSincePrimaryClick += deltaTime; + timeSinceSecondaryClick += deltaTime; if (!GameMain.WindowActive) { @@ -495,11 +505,33 @@ namespace Barotrauma MouseSpeedPerSecond = MouseSpeed / (float)deltaTime; // Split into two to not accept drag & drop releasing as part of a double-click - doubleClicked = false; + primaryDoubleClicked = false; if (PrimaryMouseButtonClicked()) { - float dist = (mouseState.Position - lastClickPosition).ToVector2().Length(); + primaryDoubleClicked = UpdateDoubleClicking(ref lastPrimaryClickPosition, ref timeSincePrimaryClick); + } + if (PrimaryMouseButtonDown()) + { + lastPrimaryClickPosition = mouseState.Position; + } + + secondaryDoubleClicked = false; + if (SecondaryMouseButtonClicked()) + { + secondaryDoubleClicked = UpdateDoubleClicking(ref lastSecondaryClickPosition, ref timeSinceSecondaryClick); + } + + if (SecondaryMouseButtonDown()) + { + lastSecondaryClickPosition = mouseState.Position; + } + + bool UpdateDoubleClicking(ref Point lastClickPosition, ref double timeSinceClick) + { + bool doubleClicked = false; + float dist = (mouseState.Position - lastClickPosition).ToVector2().Length(); + if (timeSinceClick < DoubleClickDelay && dist < MaxDoubleClickDistance) { doubleClicked = true; @@ -513,11 +545,8 @@ namespace Barotrauma { timeSinceClick = 0.0; } - } - - if (PrimaryMouseButtonDown()) - { - lastClickPosition = mouseState.Position; + + return doubleClicked; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index 9c1871345..426910cd8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -781,7 +781,7 @@ namespace Barotrauma.CharacterEditor // Lightmaps if (GameMain.LightManager.LightingEnabled && Character.Controlled != null) { - GameMain.LightManager.ObstructVision = Character.Controlled.ObstructVision; + GameMain.LightManager.ObstructVisionAmount = Character.Controlled.ObstructVisionAmount; GameMain.LightManager.RenderLightMap(graphics, spriteBatch, cam); GameMain.LightManager.UpdateObstructVision(graphics, spriteBatch, cam, Character.Controlled.CursorWorldPosition); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index a85d4790c..c9e0d8f5b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -1,4 +1,4 @@ -using Barotrauma.Extensions; +using Barotrauma.Extensions; using Barotrauma.Lights; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; @@ -177,10 +177,15 @@ namespace Barotrauma Stopwatch sw = new Stopwatch(); sw.Start(); - GameMain.LightManager.ObstructVision = - Character.Controlled != null && - Character.Controlled.ObstructVision && - (Character.Controlled.ViewTarget == Character.Controlled || Character.Controlled.ViewTarget == null); + if (Character.Controlled != null && + (Character.Controlled.ViewTarget == Character.Controlled || Character.Controlled.ViewTarget == null)) + { + GameMain.LightManager.ObstructVisionAmount = Character.Controlled.ObstructVisionAmount; + } + else + { + GameMain.LightManager.ObstructVisionAmount = 0.0f; + } GameMain.LightManager.UpdateObstructVision(graphics, spriteBatch, cam, Character.Controlled?.CursorWorldPosition ?? Vector2.Zero); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index 7606eefc9..5b9394728 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -71,7 +71,7 @@ namespace Barotrauma currentLevelData = LevelData.CreateRandom(seedBox.Text, generationParams: selectedParams); editorContainer.ClearChildren(); SortLevelObjectsList(currentLevelData); - new SerializableEntityEditor(editorContainer.Content.RectTransform, selectedParams, false, true, elementHeight: 20); + new SerializableEntityEditor(editorContainer.Content.RectTransform, selectedParams, inGame: false, showName: true, elementHeight: 20, titleFont: GUIStyle.LargeFont); return true; }; @@ -996,7 +996,7 @@ namespace Barotrauma { foreach (Item item in Item.ItemList) { - if (item == null || item.HiddenInGame) { continue; } + if (item == null || item.IsHidden) { continue; } foreach (var light in item.GetComponents()) { light.Update((float)deltaTime, Cam); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index ca5efd9f1..3de5dac9e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -63,6 +63,9 @@ namespace Barotrauma private GUITickBox spectateBox; public bool Spectating => spectateBox is { Selected: true, Visible: true }; + public bool PermadeathMode => GameMain.Client?.ServerSettings?.RespawnMode == RespawnMode.Permadeath; + public bool PermanentlyDead => campaignCharacterInfo?.PermanentlyDead ?? false; + private GUILayoutGroup playerInfoContent; private GUIComponent changesPendingText; private bool createPendingChangesText = true; @@ -87,7 +90,14 @@ namespace Barotrauma private GUIFrame characterInfoFrame; private GUIFrame appearanceFrame; - private readonly List respawnSettingsElements = new List(); + private GUISelectionCarousel respawnModeSelection; + private GUITextBlock respawnModeLabel; + private GUIComponent respawnIntervalElement; + + private readonly List midRoundRespawnSettings = new List(); + private readonly List permadeathEnabledRespawnSettings = new List(); + private readonly List permadeathDisabledRespawnSettings = new List(); + private readonly List ironmanDisabledRespawnSettings = new List(); private readonly List campaignDisabledElements = new List(); public CharacterInfo.AppearanceCustomizationMenu CharacterAppearanceCustomizationMenu { get; set; } @@ -191,7 +201,7 @@ namespace Barotrauma public bool UsingShuttle { - get { return shuttleTickBox.Selected; } + get { return shuttleTickBox.Selected && !PermadeathMode; } set { shuttleTickBox.Selected = value; } } @@ -955,19 +965,17 @@ namespace Barotrauma // ------------------------------------------------------------------ - var respawnBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), settingsContent.RectTransform) { AbsoluteOffset = new Point((int)respawnSettingsHeader.Padding.X, 0) }, - TextManager.Get("ServerSettingsAllowRespawning")) + var respawnModeHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; + respawnModeLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 0.0f), respawnModeHolder.RectTransform), TextManager.Get("RespawnMode"), wrap: true); + respawnModeSelection = new GUISelectionCarousel(new RectTransform(new Vector2(0.6f, 1.0f), respawnModeHolder.RectTransform)); + foreach (var respawnMode in Enum.GetValues(typeof(RespawnMode)).Cast()) { - ToolTip = TextManager.Get("RespawnExplanation"), - OnSelected = (tickbox) => - { - GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); - RefreshEnabledElements(); - return true; - } - }; - AssignComponentToServerSetting(respawnBox, nameof(ServerSettings.AllowRespawn)); - clientDisabledElements.Add(respawnBox); + respawnModeSelection.AddElement(respawnMode, TextManager.Get($"respawnmode.{respawnMode}"), TextManager.Get($"respawnmode.{respawnMode}.tooltip")); + } + + respawnModeSelection.ElementSelectionCondition += (value) => value != RespawnMode.Permadeath || SelectedMode == GameModePreset.MultiPlayerCampaign; + respawnModeSelection.OnValueChanged += (_) => GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); + AssignComponentToServerSetting(respawnModeSelection, nameof(ServerSettings.RespawnMode)); GUILayoutGroup shuttleHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), settingsContent.RectTransform), isHorizontal: true) { @@ -977,7 +985,7 @@ namespace Barotrauma shuttleTickBox = new GUITickBox(new RectTransform(Vector2.One, shuttleHolder.RectTransform), TextManager.Get("RespawnShuttle")) { ToolTip = TextManager.Get("RespawnShuttleExplanation"), - Selected = true, + Selected = !PermadeathMode, OnSelected = (GUITickBox box) => { GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); @@ -985,7 +993,7 @@ namespace Barotrauma } }; AssignComponentToServerSetting(shuttleTickBox, nameof(ServerSettings.UseRespawnShuttle)); - respawnSettingsElements.Add(shuttleTickBox); + midRoundRespawnSettings.Add(shuttleTickBox); shuttleTickBox.TextBlock.RectTransform.SizeChanged += () => { @@ -1008,9 +1016,9 @@ namespace Barotrauma }; ShuttleList.ListBox.RectTransform.MinSize = new Point(250, 0); shuttleHolder.RectTransform.MinSize = new Point(0, ShuttleList.RectTransform.Children.Max(c => c.MinSize.Y)); - respawnSettingsElements.Add(ShuttleList); + midRoundRespawnSettings.Add(ShuttleList); - var respawnIntervalElement = CreateLabeledSlider(settingsContent, "ServerSettingsRespawnInterval", "", "", out var respawnIntervalSlider, out var respawnIntervalSliderLabel, + respawnIntervalElement = CreateLabeledSlider(settingsContent, "ServerSettingsRespawnInterval", "", "", out var respawnIntervalSlider, out var respawnIntervalSliderLabel, range: new Vector2(10.0f, 600.0f)); LocalizedString intervalLabel = respawnIntervalSliderLabel.Text; respawnIntervalSlider.StepValue = 10.0f; @@ -1026,7 +1034,6 @@ namespace Barotrauma return true; }; respawnIntervalSlider.OnMoved(respawnIntervalSlider, respawnIntervalSlider.BarScroll); - respawnSettingsElements.AddRange(respawnIntervalElement.GetAllChildren()); AssignComponentToServerSetting(respawnIntervalSlider, nameof(ServerSettings.RespawnInterval)); var minRespawnElement = CreateLabeledSlider(settingsContent, "ServerSettingsMinRespawn", "", "ServerSettingsMinRespawnToolTip", out var minRespawnSlider, out var minRespawnSliderLabel, @@ -1043,7 +1050,7 @@ namespace Barotrauma return true; }; minRespawnSlider.OnMoved(minRespawnSlider, minRespawnSlider.BarScroll); - respawnSettingsElements.AddRange(minRespawnElement.GetAllChildren()); + midRoundRespawnSettings.AddRange(minRespawnElement.GetAllChildren()); AssignComponentToServerSetting(minRespawnSlider, nameof(ServerSettings.MinRespawnRatio)); var respawnDurationElement = CreateLabeledSlider(settingsContent, "ServerSettingsRespawnDuration", "", "ServerSettingsRespawnDurationTooltip", out var respawnDurationSlider, out var respawnDurationSliderLabel, @@ -1068,7 +1075,7 @@ namespace Barotrauma return value <= 0.0f ? 1.0f : (value - scrollBar.Range.X) / (scrollBar.Range.Y - scrollBar.Range.X); }; respawnDurationSlider.OnMoved(respawnDurationSlider, respawnDurationSlider.BarScroll); - respawnSettingsElements.AddRange(respawnDurationElement.GetAllChildren()); + midRoundRespawnSettings.AddRange(respawnDurationElement.GetAllChildren()); AssignComponentToServerSetting(respawnDurationSlider, nameof(ServerSettings.MaxTransportTime)); var skillLossElement = CreateLabeledSlider(settingsContent, "ServerSettingsSkillLossPercentageOnDeath", "", "ServerSettingsSkillLossPercentageOnDeathToolTip", @@ -1085,7 +1092,8 @@ namespace Barotrauma GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); return true; }; - respawnSettingsElements.AddRange(skillLossElement.GetAllChildren()); + permadeathDisabledRespawnSettings.AddRange(skillLossElement.GetAllChildren()); + clientDisabledElements.AddRange(skillLossElement.GetAllChildren()); AssignComponentToServerSetting(skillLossSlider, nameof(ServerSettings.SkillLossPercentageOnDeath)); skillLossSlider.OnMoved(skillLossSlider, skillLossSlider.BarScroll); @@ -1103,11 +1111,41 @@ namespace Barotrauma GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); return true; }; - respawnSettingsElements.AddRange(skillLossImmediateRespawnElement.GetAllChildren()); + midRoundRespawnSettings.AddRange(skillLossImmediateRespawnElement.GetAllChildren()); + permadeathDisabledRespawnSettings.AddRange(skillLossImmediateRespawnElement.GetAllChildren()); AssignComponentToServerSetting(skillLossImmediateRespawnSlider, nameof(ServerSettings.SkillLossPercentageOnImmediateRespawn)); skillLossImmediateRespawnSlider.OnMoved(skillLossImmediateRespawnSlider, skillLossImmediateRespawnSlider.BarScroll); - foreach (var respawnElement in respawnSettingsElements) + var allowBotTakeoverTickbox = new GUITickBox(new RectTransform(Vector2.One, settingsContent.RectTransform), TextManager.Get("AllowBotTakeover")) + { + ToolTip = TextManager.Get("AllowBotTakeover.Tooltip"), + Selected = GameMain.Client != null && GameMain.Client.ServerSettings.AllowBotTakeoverOnPermadeath, + OnSelected = (GUITickBox box) => + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); + return true; + } + }; + AssignComponentToServerSetting(allowBotTakeoverTickbox, nameof(ServerSettings.AllowBotTakeoverOnPermadeath)); + permadeathEnabledRespawnSettings.Add(allowBotTakeoverTickbox); + ironmanDisabledRespawnSettings.Add(allowBotTakeoverTickbox); + clientDisabledElements.Add(allowBotTakeoverTickbox); + + var ironmanTickbox = new GUITickBox(new RectTransform(Vector2.One, settingsContent.RectTransform), TextManager.Get("IronmanMode").ToUpper()) + { + ToolTip = TextManager.Get("IronmanMode.Tooltip"), + Selected = GameMain.Client != null && GameMain.Client.ServerSettings.IronmanMode, + OnSelected = (GUITickBox box) => + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); + return true; + } + }; + AssignComponentToServerSetting(ironmanTickbox, nameof(ServerSettings.IronmanMode)); + permadeathEnabledRespawnSettings.Add(ironmanTickbox); + clientDisabledElements.Add(ironmanTickbox); + + foreach (var respawnElement in midRoundRespawnSettings) { if (!clientDisabledElements.Contains(respawnElement)) { @@ -1650,19 +1688,31 @@ namespace Barotrauma bool campaignStarted = CampaignFrame.Visible; bool gameStarted = client != null && client.GameStarted; - //disable elements the client doesn't have access to + // First, enable or disable elements based on client permissions foreach (var element in clientDisabledElements) { element.Enabled = manageSettings; } + + // Then disable elements depending on other conditions traitorElements.ForEach(e => e.Enabled &= settings.TraitorProbability > 0); SetTraitorDangerIndicators(settings.TraitorDangerLevel); - respawnSettingsElements.ForEach(e => e.Enabled &= settings.AllowRespawn); + respawnModeSelection.Enabled = respawnModeLabel.Enabled = manageSettings && !gameStarted; + midRoundRespawnSettings.ForEach(e => e.Enabled &= settings.RespawnMode == RespawnMode.MidRound); + permadeathDisabledRespawnSettings.ForEach(e => e.Enabled &= settings.RespawnMode != RespawnMode.Permadeath); + permadeathEnabledRespawnSettings.ForEach(e => e.Enabled &= settings.RespawnMode == RespawnMode.Permadeath && !gameStarted); + ironmanDisabledRespawnSettings.ForEach(e => e.Enabled &= !settings.IronmanMode); + + // The respawn interval is used even if the shuttle is not + respawnIntervalElement.GetAllChildren().ForEach(e => e.Enabled = settings.RespawnMode != RespawnMode.BetweenRounds && manageSettings); //go through the individual elements that are only enabled in a specific context + shuttleTickBox.Enabled &= !gameStarted; if (ShuttleList != null) { - ShuttleList.Enabled = ShuttleList.ButtonEnabled = HasPermission(ClientPermissions.SelectSub) && !gameStarted && settings.AllowRespawn; + // Shuttle list depends on shuttle tickbox + ShuttleList.Enabled &= shuttleTickBox.Enabled && HasPermission(ClientPermissions.SelectSub); + ShuttleList.ButtonEnabled = ShuttleList.Enabled; } if (SubList != null) { @@ -1672,7 +1722,6 @@ namespace Barotrauma { ModeList.Enabled = !gameStarted && (settings.AllowModeVoting || HasPermission(ClientPermissions.SelectMode)); } - shuttleTickBox.Enabled &= !gameStarted; RefreshStartButtonVisibility(); @@ -1750,6 +1799,10 @@ namespace Barotrauma private void UpdatePlayerFrame(CharacterInfo characterInfo, bool allowEditing, GUIComponent parent, bool createPendingText = true) { if (GameMain.Client == null) { return; } + + // When permanently dead and still characterless, spectating is the only option + spectateBox.Enabled = !PermanentlyDead; + createPendingChangesText = createPendingText; if (characterInfo == null || CampaignCharacterDiscarded) { @@ -1780,41 +1833,57 @@ namespace Barotrauma MaxTextLength = Client.MaxNameLength, OverflowClip = true }; - - CharacterNameBox.OnEnterPressed += (tb, text) => { CharacterNameBox.Deselect(); return true; }; - CharacterNameBox.OnDeselected += (tb, key) => + + if (PermanentlyDead) { - if (GameMain.Client == null) { return; } - string newName = Client.SanitizeName(tb.Text); - if (newName == GameMain.Client.Name) return; - if (string.IsNullOrWhiteSpace(newName)) + CharacterNameBox.Readonly = true; + CharacterNameBox.Enabled = false; + } + else + { + CharacterNameBox.OnEnterPressed += (tb, text) => { - tb.Text = GameMain.Client.Name; - } - else + CharacterNameBox.Deselect(); + return true; + }; + CharacterNameBox.OnDeselected += (tb, key) => { - if (isGameRunning) + if (GameMain.Client == null) { - GameMain.Client.PendingName = tb.Text; - TabMenu.PendingChanges = true; - if (createPendingText) - { - CreateChangesPendingText(); - } + return; + } + + string newName = Client.SanitizeName(tb.Text); + if (newName == GameMain.Client.Name) { return; } + if (string.IsNullOrWhiteSpace(newName)) + { + tb.Text = GameMain.Client.Name; } else { - ReadyToStartBox.Selected = false; + if (isGameRunning) + { + GameMain.Client.PendingName = tb.Text; + TabMenu.PendingChanges = true; + if (createPendingText) + { + CreateChangesPendingText(); + } + } + else + { + ReadyToStartBox.Selected = false; + } + + GameMain.Client.SetName(tb.Text); } - - GameMain.Client.SetName(tb.Text); - } - }; - + }; + } + //spacing new GUIFrame(new RectTransform(new Vector2(1.0f, 0.006f), parent.RectTransform), style: null); - if (allowEditing) + if (allowEditing && (!PermadeathMode || !isGameRunning)) { GUILayoutGroup characterInfoTabs = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.07f), parent.RectTransform), isHorizontal: true) { @@ -1892,37 +1961,70 @@ namespace Barotrauma { characterInfo.CreateIcon(new RectTransform(new Vector2(1.0f, 0.16f), parent.RectTransform, Anchor.TopCenter)); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), characterInfo.Job.Name, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont, wrap: true) + if (PermanentlyDead) { - HoverColor = Color.Transparent, - SelectedColor = Color.Transparent - }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), TextManager.Get("Skills"), font: GUIStyle.SubHeadingFont); - foreach (Skill skill in characterInfo.Job.GetSkills()) + new GUITextBlock( + new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), + TextManager.Get("deceased"), + textAlignment: Alignment.Center, font: GUIStyle.LargeFont); + + if (GameMain.Client?.ServerSettings is { IronmanMode: true }) + { + new GUITextBlock( + new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), + TextManager.Get("lobby.ironmaninfo"), + textAlignment: Alignment.Center, wrap: true); + } + else + { + new GUITextBlock( + new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), + TextManager.Get("lobby.permadeathinfo"), + textAlignment: Alignment.Center, wrap: true); + new GUITextBlock( + new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), + TextManager.Get("lobby.permadeathoptionsexplanation"), + textAlignment: Alignment.Center, wrap: true); + } + } + else { - Color textColor = Color.White * (0.5f + skill.Level / 200.0f); - var skillText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), - " - " + TextManager.AddPunctuation(':', TextManager.Get("SkillName." + skill.Identifier), ((int)skill.Level).ToString()), - textColor, - font: GUIStyle.SmallFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), characterInfo.Job.Name, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont, wrap: true) + { + HoverColor = Color.Transparent, + SelectedColor = Color.Transparent + }; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), TextManager.Get("Skills"), font: GUIStyle.SubHeadingFont); + foreach (Skill skill in characterInfo.Job.GetSkills()) + { + Color textColor = Color.White * (0.5f + skill.Level / 200.0f); + var skillText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), + " - " + TextManager.AddPunctuation(':', TextManager.Get("SkillName." + skill.Identifier), ((int)skill.Level).ToString()), + textColor, + font: GUIStyle.SmallFont); + } } // Spacing new GUIFrame(new RectTransform(new Vector2(1.0f, 0.15f), parent.RectTransform), style: null); - new GUIButton(new RectTransform(new Vector2(0.8f, 0.1f), parent.RectTransform, Anchor.BottomCenter), TextManager.Get("CreateNew")) + if (GameMain.Client?.ServerSettings?.RespawnMode != RespawnMode.Permadeath) { - IgnoreLayoutGroups = true, - OnClicked = (btn, userdata) => + // Button to create new character + new GUIButton(new RectTransform(new Vector2(0.8f, 0.1f), parent.RectTransform, Anchor.BottomCenter), TextManager.Get("CreateNew")) { - TryDiscardCampaignCharacter(() => + IgnoreLayoutGroups = true, + OnClicked = (btn, userdata) => { - UpdatePlayerFrame(null, true, parent); - }); - return true; - } - }; + TryDiscardCampaignCharacter(() => + { + UpdatePlayerFrame(null, true, parent); + }); + return true; + } + }; + } } TeamPreferenceListBox = null; @@ -2095,14 +2197,20 @@ namespace Barotrauma { if (GameMain.Client == null) { return; } spectateBox.Selected = spectate; + if (spectate) { - playerInfoContent.ClearChildren(); - GameMain.Client.CharacterInfo?.Remove(); GameMain.Client.CharacterInfo = null; - GameMain.Client.Character?.Remove(); - GameMain.Client.Character = null; + // TODO: The following lines are ancient, unexplained, and they cause a client spectating because of permadeath + // to get kicked from the server at round transition because the server expects to be in control of + // removing Characters and the client to still have one. Commenting these lines out for now, but + // if no side-effects occur, they can just be deleted. + //GameMain.Client.Character?.Remove(); + //GameMain.Client.Character = null; + + playerInfoContent.ClearChildren(); + new GUITextBlock(new RectTransform(Vector2.One, playerInfoContent.RectTransform, Anchor.Center), TextManager.Get("PlayingAsSpectator"), textAlignment: Alignment.Center); @@ -2118,6 +2226,10 @@ namespace Barotrauma // Server owner is allowed to spectate regardless of the server settings if (GameMain.Client != null && GameMain.Client.IsServerOwner) { return; } + // A client whose character has faced permadeath and hasn't chosen a new + // character yet has no choice but to spectate + if (campaignCharacterInfo != null && campaignCharacterInfo.PermanentlyDead) { return; } + // Show the player config menu if spectating is not allowed if (spectateBox.Selected && !allowSpectating) { spectateBox.Selected = false; } @@ -3609,6 +3721,7 @@ namespace Barotrauma GameMain.GameSession = null; } + respawnModeSelection.Refresh(); // not all respawn modes are compatible with all game modes RefreshGameModeContent(); RefreshEnabledElements(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 32a0a41e4..008b62f46 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -27,38 +27,8 @@ namespace Barotrauma get => Submarine.MainSub; set => Submarine.MainSub = value; } - - private enum LayerVisibility - { - Visible, - Invisible - } - private enum LayerLinkage - { - Unlinked, - Linked - } - - private readonly struct LayerData - { - public readonly LayerVisibility Visible; - public readonly LayerLinkage Linkage; - - public static readonly LayerData Default = new LayerData(LayerVisibility.Visible, LayerLinkage.Unlinked); - - public LayerData(LayerVisibility visible, LayerLinkage linkage) - { - Visible = visible; - Linkage = linkage; - } - - public void Deconstruct(out LayerVisibility isvisible, out LayerLinkage islinked) - { - isvisible = Visible; - islinked = Linkage; - } - } + private readonly record struct LayerData(bool IsVisible = true, bool IsGrouped = false); public enum Mode { @@ -1105,11 +1075,22 @@ namespace Barotrauma GameSession gameSession = new GameSession(backedUpSubInfo, "", GameModePreset.TestMode, CampaignSettings.Empty, null); gameSession.StartRound(null, false); - (gameSession.GameMode as TestGameMode).OnRoundEnd = () => + + foreach ((string layerName, LayerData layerData) in Layers) { - Submarine.Unload(); - GameMain.SubEditorScreen.Select(); - }; + Identifier identifier = layerName.ToIdentifier(); + bool enabled = layerData.IsVisible; + MainSub.SetLayerEnabled(identifier, enabled); + } + + if (gameSession.GameMode is TestGameMode testGameMode) + { + testGameMode.OnRoundEnd = () => + { + Submarine.Unload(); + GameMain.SubEditorScreen.Select(); + }; + } return true; } @@ -1469,6 +1450,7 @@ namespace Barotrauma { var subInfo = new SubmarineInfo(); MainSub = new Submarine(subInfo, showErrorMessages: false); + ReconstructLayers(); } MainSub.UpdateTransform(interpolate: false); @@ -1504,7 +1486,10 @@ namespace Barotrauma } ImageManager.OnEditorSelected(); - ReconstructLayers(); + if (Layers.None()) + { + ReconstructLayers(); + } } public override void OnFileDropped(string filePath, string extension) @@ -1661,7 +1646,6 @@ namespace Barotrauma }); ClearFilter(); - ClearLayers(); } private void CreateDummyCharacter() @@ -2165,32 +2149,32 @@ namespace Barotrauma if (Layers.Any()) { var layerVisibilityGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.01f), leftColumn.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + var visibleLayers = Layers.Where(l => !MainSub.Info.LayersHiddenByDefault.Contains(l.Key.ToIdentifier())); + LocalizedString visibleLayersString = LocalizedString.Join(", ", visibleLayers.Select(l => TextManager.Capitalize(l.Key)) ?? ((LocalizedString)"None").ToEnumerable()); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), layerVisibilityGroup.RectTransform), TextManager.Get("editor.layer.visiblebydefault"), textAlignment: Alignment.CenterLeft); - var layerVisibilityDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), layerVisibilityGroup.RectTransform), - text: LocalizedString.Join(", ", Layers.Where(l => !Submarine.MainSub?.Info?.LayersHiddenByDefault?.Contains(l.ToIdentifier()) ?? false).Select(lt => TextManager.Capitalize(lt.Key)) ?? ((LocalizedString)"None").ToEnumerable()), selectMultiple: true); - foreach (string layerName in Layers.Keys) + var layerVisibilityDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), layerVisibilityGroup.RectTransform), text: visibleLayersString, selectMultiple: true); + foreach (var layer in Layers) { + string layerName = layer.Key; layerVisibilityDropDown.AddItem(TextManager.Capitalize(layerName), layerName); - if (MainSub?.Info == null) { continue; } - if (!MainSub.Info.LayersHiddenByDefault.Contains(layerName.ToIdentifier())) + if (visibleLayers.Contains(layer)) { layerVisibilityDropDown.SelectItem(layerName); } } - layerVisibilityDropDown.OnSelected += (_, __) => + layerVisibilityDropDown.OnSelected += (button, obj) => { - if (MainSub.Info == null) { return false; } - MainSub.Info.LayersHiddenByDefault.Clear(); - foreach (string layerName in Layers.Keys) + string layerName = (string)obj; + bool isVisible = layerVisibilityDropDown.SelectedDataMultiple.Contains(obj); + if (isVisible) { - //selected as visible = not hidden - if (layerVisibilityDropDown.SelectedDataMultiple.Any(o => o as string == layerName)) - { - continue; - } - MainSub.Info.LayersHiddenByDefault.Add(layerName.ToIdentifier()); + MainSub.Info.LayersHiddenByDefault.Remove(layerName.ToIdentifier()); } - + else + { + MainSub.Info.LayersHiddenByDefault.Add(layerName.ToIdentifier()); + } + UpdateLayerPanel(); layerVisibilityDropDown.Text = ToolBox.LimitString(layerVisibilityDropDown.Text.Value, layerVisibilityDropDown.Font, layerVisibilityDropDown.Rect.Width); return true; }; @@ -2508,6 +2492,15 @@ namespace Barotrauma return true; } }; + new GUITickBox(new RectTransform(new Vector2(1.0f, 0.25f), beaconSettingsContainer.RectTransform), TextManager.Get("allowdamageddevices")) + { + Selected = MainSub?.Info?.BeaconStationInfo?.AllowDamagedDevices ?? true, + OnSelected = (tb) => + { + MainSub.Info.BeaconStationInfo.AllowDamagedDevices = tb.Selected; + return true; + } + }; new GUITickBox(new RectTransform(new Vector2(1.0f, 0.25f), beaconSettingsContainer.RectTransform), TextManager.Get("allowdisconnectedwires")) { Selected = MainSub?.Info?.BeaconStationInfo?.AllowDisconnectedWires ?? true, @@ -3932,15 +3925,14 @@ namespace Barotrauma MapEntity.HighlightedEntities.ToList() : new List(MapEntity.SelectedList); - Item target = null; - - var single = targets.Count == 1 ? targets.Single() : null; - if (single is Item item && item.Components.Any(static ic => ic is not ConnectionPanel && ic is not Repairable && ic.GuiFrame != null)) - { - // Do not offer the ability to open the inventory if the inventory should never be drawn - var containers = item.GetComponents(); - if (containers.Any(static c => c.DrawInventory) || item.GetComponent() is not null) { target = item; } - } + bool allowOpening = false; + var targetItem = (targets.Count == 1 ? targets.Single() : null) as Item; + // Do not offer the ability to open the inventory if the inventory should never be drawn + allowOpening = targetItem is not null && targetItem.Components.Any(static ic => + ic is not ConnectionPanel && + ic is not Repairable && + ic is not ItemContainer { DrawInventory: false } && + ic.GuiFrame != null); bool hasTargets = targets.Count > 0; @@ -3984,7 +3976,6 @@ namespace Barotrauma } else { - List availableLayers = new List { new ContextMenuOption("editor.layer.nolayer", true, onSelected: () => { MoveToLayer(null, targets); }) @@ -3992,7 +3983,8 @@ namespace Barotrauma availableLayers.AddRange(Layers.Select(layer => new ContextMenuOption(layer.Key, true, onSelected: () => { MoveToLayer(layer.Key, targets); }))); List availableLayerOptions = new List - { new ContextMenuOption("editor.layer.movetolayer", isEnabled: hasTargets, availableLayers.ToArray()), + { + new ContextMenuOption("editor.layer.movetolayer", isEnabled: hasTargets, availableLayers.ToArray()), new ContextMenuOption("editor.layer.createlayer", isEnabled: hasTargets, onSelected: () => { CreateNewLayer(null, targets); }), new ContextMenuOption("editor.layer.selectall", isEnabled: hasTargets, onSelected: () => { @@ -4006,7 +3998,7 @@ namespace Barotrauma availableLayerOptions.AddRange(Layers.Select(layer => new ContextMenuOption(layer.Key, true, onSelected: () => { MoveToLayer(layer.Key, targets); }))); GUIContextMenu.CreateContextMenu( - new ContextMenuOption("label.openlabel", isEnabled: target != null, onSelected: () => OpenItem(target)), + new ContextMenuOption("label.openlabel", isEnabled: allowOpening, onSelected: () => OpenItem(targetItem)), new ContextMenuOption("editor.cut", isEnabled: hasTargets, onSelected: () => MapEntity.Cut(targets)), new ContextMenuOption("editor.copytoclipboard", isEnabled: hasTargets, onSelected: () => MapEntity.Copy(targets)), new ContextMenuOption("editor.paste", isEnabled: MapEntity.CopiedList.Any(), onSelected: () => MapEntity.Paste(cam.ScreenToWorld(PlayerInput.MousePosition))), @@ -4061,13 +4053,13 @@ namespace Barotrauma MoveToLayer(name, content); } - Layers.Add(name, LayerData.Default); + Layers.Add(name, new LayerData()); UpdateLayerPanel(); } private void RenameLayer(string original, string newName) { - Layers.Remove(original); + Layers.Remove(original, out LayerData originalData); foreach (MapEntity entity in MapEntity.MapEntityList.Where(entity => entity.Layer == original)) { @@ -4076,7 +4068,7 @@ namespace Barotrauma if (!string.IsNullOrWhiteSpace(newName)) { - Layers.TryAdd(newName, LayerData.Default); + Layers.TryAdd(newName, originalData); } UpdateLayerPanel(); } @@ -4088,7 +4080,7 @@ namespace Barotrauma { if (!string.IsNullOrWhiteSpace(entity.Layer)) { - Layers.TryAdd(entity.Layer, LayerData.Default); + Layers.TryAdd(entity.Layer, new LayerData(!entity.IsLayerHidden)); } } UpdateLayerPanel(); @@ -4099,6 +4091,18 @@ namespace Barotrauma Layers.Clear(); UpdateLayerPanel(); } + + private static void SetLayerVisibility(string layerName, bool isVisible) + { + if (Layers.Remove(layerName, out LayerData layerData)) + { + Layers.Add(layerName, layerData with { IsVisible = isVisible }); + } + else + { + Layers.Add(layerName, new LayerData(isVisible)); + } + } private void PasteAssembly(string text = null, Vector2? pos = null) { @@ -4492,39 +4496,39 @@ namespace Barotrauma } /// - /// Tries to open an item container in the submarine editor using the dummy character + /// Tries to open an item in the submarine editor using the dummy character /// - /// The item we want to open - private void OpenItem(Item itemContainer) + /// The item we want to open + private void OpenItem(Item item) { - if (dummyCharacter == null || itemContainer == null) { return; } + if (dummyCharacter == null || item == null) { return; } - if ((itemContainer.GetComponent() is { Attached: false } || itemContainer.GetComponent() != null) && itemContainer.GetComponent() != null) + if ((item.GetComponent() is { Attached: false } || item.GetComponent() != null) && item.GetComponent() != null) { // We teleport our dummy character to the item so it appears as the entity stays still when in reality the dummy is holding it - oldItemPosition = itemContainer.SimPosition; + oldItemPosition = item.SimPosition; TeleportDummyCharacter(oldItemPosition); // Override this so we can be sure the container opens - var container = itemContainer.GetComponent(); + var container = item.GetComponent(); if (container != null) { container.KeepOpenWhenEquipped = true; } // We accept any slots except "Any" since that would take priority List allowedSlots = new List(); - itemContainer.AllowedSlots.ForEach(type => + item.AllowedSlots.ForEach(type => { if (type != InvSlotType.Any) { allowedSlots.Add(type); } }); // Try to place the item in the dummy character's inventory - bool success = dummyCharacter.Inventory.TryPutItem(itemContainer, dummyCharacter, allowedSlots); - if (success) { OpenedItem = itemContainer; } + bool success = dummyCharacter.Inventory.TryPutItem(item, dummyCharacter, allowedSlots); + if (success) { OpenedItem = item; } else { return; } } MapEntity.SelectedList.Clear(); MapEntity.FilteredSelectedList.Clear(); - MapEntity.SelectEntity(itemContainer); - dummyCharacter.SelectedItem = itemContainer; + MapEntity.SelectEntity(item); + dummyCharacter.SelectedItem = item; FilterEntities(entityFilterBox.Text); MapEntity.StopSelection(); } @@ -5176,7 +5180,7 @@ namespace Barotrauma }; new GUIButton(new RectTransform(new Vector2(0.6f, 1f), buttonHeaders.RectTransform), TextManager.Get("name"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes }; - foreach (var (layer, (visibility, linkage)) in Layers) + foreach ((string layer, (bool isVisible, bool isGrouped)) in Layers) { GUIFrame parent = new GUIFrame(new RectTransform(new Vector2(1f, 0.1f), layerList.Content.RectTransform), style: "ListBoxElement") { @@ -5188,7 +5192,7 @@ namespace Barotrauma GUILayoutGroup layerVisibilityLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.25f, 1f), layerGroup.RectTransform), childAnchor: Anchor.Center); GUITickBox layerVisibleButton = new GUITickBox(new RectTransform(Vector2.One, layerVisibilityLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), string.Empty) { - Selected = visibility == LayerVisibility.Visible, + Selected = isVisible, OnSelected = box => { if (!Layers.TryGetValue(layer, out LayerData data)) @@ -5196,12 +5200,15 @@ namespace Barotrauma UpdateLayerPanel(); return false; } - //hiding a layer automatically deselects it (can't edit a hidden layer) if (!box.Selected && layerList.SelectedData as string == layer) { - layerList.Deselect(); + //hiding a layer automatically deselects it (can't edit a hidden layer) + if (!box.Selected) + { + layerList.Deselect(); + } } - Layers[layer] = new LayerData(box.Selected ? LayerVisibility.Visible : LayerVisibility.Invisible, data.Linkage); + Layers[layer] = data with { IsVisible = box.Selected }; return true; } }; @@ -5209,7 +5216,7 @@ namespace Barotrauma GUILayoutGroup layerChainLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.15f, 1f), layerGroup.RectTransform), childAnchor: Anchor.Center); GUITickBox layerChainButton = new GUITickBox(new RectTransform(Vector2.One, layerChainLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), string.Empty) { - Selected = linkage == LayerLinkage.Linked, + Selected = isGrouped, OnSelected = box => { if (!Layers.TryGetValue(layer, out LayerData data)) @@ -5218,7 +5225,7 @@ namespace Barotrauma return false; } - Layers[layer] = new LayerData(data.Visible, box.Selected ? LayerLinkage.Linked : LayerLinkage.Unlinked); + Layers[layer] = data with { IsGrouped = box.Selected }; return true; } }; @@ -5249,7 +5256,6 @@ namespace Barotrauma btn.ToolTip = originalBtnText; } } - } public void UpdateUndoHistoryPanel() @@ -6303,11 +6309,11 @@ namespace Barotrauma if (!Layers.TryGetValue(entity.Layer, out LayerData data)) { - Layers.TryAdd(entity.Layer, LayerData.Default); + Layers.TryAdd(entity.Layer, new LayerData(!entity.IsLayerHidden)); return true; } - return data.Visible == LayerVisibility.Visible; + return data.IsVisible; } public static bool IsLayerLinked(MapEntity entity) @@ -6316,11 +6322,11 @@ namespace Barotrauma if (!Layers.TryGetValue(entity.Layer, out LayerData data)) { - Layers.TryAdd(entity.Layer, LayerData.Default); + Layers.TryAdd(entity.Layer, new LayerData(!entity.IsLayerHidden)); return true; } - return data.Linkage == LayerLinkage.Linked; + return data.IsGrouped; } public static ImmutableHashSet GetEntitiesInSameLayer(MapEntity entity) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index bede9e3c6..fffd275a6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -326,7 +326,7 @@ namespace Barotrauma public SerializableEntityEditor(RectTransform parent, ISerializableEntity entity, IEnumerable properties, bool showName, string style = "", int elementHeight = 24, GUIFont titleFont = null) : base(style, new RectTransform(Vector2.One, parent)) { - this.elementHeight = (int)(elementHeight * GUI.Scale); + elementHeight = (int)(elementHeight * GUI.Scale); var tickBoxStyle = GUIStyle.GetComponentStyle("GUITickBox"); var textBoxStyle = GUIStyle.GetComponentStyle("GUITextBox"); var numberInputStyle = GUIStyle.GetComponentStyle("GUINumberInput"); @@ -343,7 +343,50 @@ namespace Barotrauma Color = Color.Black }; } - properties.ForEach(ep => CreateNewField(ep, entity)); + + List
headers = new List
() + { + //"no header" comes first = properties under no header are listed first + null + }; + //check which header each property is under + Dictionary propertyHeaders = new Dictionary(); + Header prevHeader = null; + foreach (var property in properties) + { + var header = property.GetAttribute
(); + if (header != null) + { + prevHeader = header; + //Attribute.Equals is based on the equality of the fields, + //so in practice we treat identical headers split into different files/classes as the same header + if (!headers.Contains(header)) + { + //collect headers into a list in the order they're encountered in + //(to keep them in the same order as they're defined in the code, as the dictionary is not in any particular order) + headers.Add(header); + } + } + propertyHeaders[property] = prevHeader; + } + + prevHeader = null; + foreach (Header header in headers) + { + //go through all the properties that belong under this header + foreach (var property in properties) + { + if (!Equals(propertyHeaders[property], header)) { continue; } + //don't create a header if the previous header has the same text as this one (= if we already created this header before) + if (header != null && !Equals(header, prevHeader)) + { + new GUITextBlock(new RectTransform(new Point(Rect.Width, Math.Max(elementHeight, 26)), layoutGroup.RectTransform, isFixedSize: true), + header.Text, textColor: GUIStyle.TextColorBright, font: GUIStyle.SubHeadingFont); + prevHeader = header; + } + CreateNewField(property, entity); + } + } //scale the size of this component and the layout group to fit the children Recalculate(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs index 87c38b8de..17f53bf9a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs @@ -137,22 +137,10 @@ namespace Barotrauma.Sounds { for (int i = 0; i < length; i++) { - outBuffer[i] = FloatToShort(inBuffer[i]); + outBuffer[i] = ToolBox.FloatToShortAudioSample(inBuffer[i]); } } - static protected short FloatToShort(float fVal) - { - int temp = (int)(32767 * fVal); - if (temp > short.MaxValue) temp = short.MaxValue; - else if (temp < short.MinValue) temp = short.MinValue; - return (short)temp; - } - static protected float ShortToFloat(short shortVal) - { - return shortVal / 32767f; - } - public abstract int FillStreamBuffer(int samplePos, short[] buffer); public abstract float GetAmplitudeAtPlaybackPos(int playbackPos); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs index 8bc574c86..7aefc3594 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs @@ -107,7 +107,7 @@ namespace Barotrauma.Sounds float finalGain = gain * GameSettings.CurrentConfig.Audio.VoiceChatVolume * client.VoiceVolume; for (int i = 0; i < readSamples; i++) { - float fVal = ShortToFloat(buffer[i]); + float fVal = ToolBox.ShortAudioSampleToFloat(buffer[i]); if (finalGain > 1.0f) //TODO: take distance into account? { @@ -128,7 +128,7 @@ namespace Barotrauma.Sounds fVal = Math.Clamp(filter.Process(fVal) * PostRadioFilterBoost, -1f, 1f); } } - buffer[i] = FloatToShort(fVal); + buffer[i] = ToolBox.FloatToShortAudioSample(fVal); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs index 873e8a9ec..7d3882def 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs @@ -99,10 +99,10 @@ namespace Barotrauma.Steam { currentLobby?.SetData("EosEndpoint", puids[0].Value); } - + DebugConsole.Log("Lobby updated!"); } - + private static void SetServerListInfo(Identifier key, object value) { switch (value) @@ -115,7 +115,7 @@ namespace Barotrauma.Steam .JoinEscaped(',')); return; } - + currentLobby?.SetData(key.Value.ToLowerInvariant(), value.ToString()); } diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 303aedb2f..26264d70b 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,8 +6,8 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.4.6.0 - Copyright © FakeFish 2018-2023 + 1.5.7.0 + Copyright © FakeFish 2018-2024 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index bd6b38153..6401060dd 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,8 +6,8 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.4.6.0 - Copyright © FakeFish 2018-2023 + 1.5.7.0 + Copyright © FakeFish 2018-2024 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index d3675cef9..95348b2e7 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,8 +6,8 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.4.6.0 - Copyright © FakeFish 2018-2023 + 1.5.7.0 + Copyright © FakeFish 2018-2024 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 013e7d3db..cdcbc590f 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.4.6.0 + 1.5.7.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer @@ -64,12 +64,14 @@ + + diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 92f84c239..c0b09740b 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.4.6.0 + 1.5.7.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer @@ -70,12 +70,14 @@ + + diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs index 2e5acdab7..144b8fbdc 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs @@ -1,4 +1,5 @@ using Barotrauma.Networking; +using System.Linq; using System.Xml.Linq; namespace Barotrauma @@ -26,6 +27,27 @@ namespace Barotrauma } } + if (GameMain.Server is { ServerSettings.RespawnMode: RespawnMode.Permadeath } && + GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign && + causeOfDeath != CauseOfDeathType.Disconnected) + { + Client ownerClient = GameMain.Server.ConnectedClients.FirstOrDefault(c => c.Character == this); + if (ownerClient != null) + { + ownerClient.SpectateOnly = true; + CharacterCampaignData matchingData = mpCampaign.GetClientCharacterData(ownerClient); + if (matchingData != null) + { + matchingData.ApplyPermadeath(); + + if (GameMain.Server is { ServerSettings.IronmanMode: true }) + { + mpCampaign.SaveSingleCharacter(matchingData); + } + } + } + } + if (HasAbilityFlag(AbilityFlags.RetainExperienceForNewCharacter)) { var ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == this); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs index 50fa97fdb..ec7154a5e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs @@ -96,6 +96,7 @@ namespace Barotrauma msg.WriteInt32(ExperiencePoints); msg.WriteRangedInteger(AdditionalTalentPoints, 0, MaxAdditionalTalentPoints); + msg.WriteBoolean(PermanentlyDead); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index d1c337634..0285452fa 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -963,7 +963,7 @@ namespace Barotrauma } client.Muted = true; GameMain.Server.SendDirectChatMessage(TextManager.Get("MutedByServer").Value, client, ChatMessageType.MessageBox); - }, + }, () => { if (GameMain.Server == null) return null; @@ -1599,6 +1599,19 @@ namespace Barotrauma NewMessage("Disabled RequireClientNameMatch"); } })); + + AssignOnExecute("debugvoip", _ => + { + VoipServerDecoder.DebugVoip = !VoipServerDecoder.DebugVoip; + NewMessage("Debugging voice chat is now " + (VoipServerDecoder.DebugVoip ? "enabled" : "disabled"), Color.White); + }); + + AssignOnClientRequestExecute("debugvoip", (client, _, _) => + { + VoipServerDecoder.DebugVoip = !VoipServerDecoder.DebugVoip; + NewMessage("Debugging voice chat is now " + (VoipServerDecoder.DebugVoip ? "enabled" : "disabled") + " by " + client.Name, Color.White); + GameMain.Server.SendConsoleMessage("Debugging voice chat is now " + (VoipServerDecoder.DebugVoip ? "enabled" : "disabled"), client); + }); #endif AssignOnClientRequestExecute( @@ -1761,11 +1774,7 @@ namespace Barotrauma "teleportcharacter|teleport", (Client client, Vector2 cursorWorldPos, string[] args) => { - Character tpCharacter = (args.Length == 0) ? client.Character : FindMatchingCharacter(args, false); - if (tpCharacter != null) - { - tpCharacter.TeleportTo(cursorWorldPos); - } + TeleportCharacter(cursorWorldPos, client.Character, args); } ); @@ -1922,6 +1931,17 @@ namespace Barotrauma foreach (Client c in GameMain.Server.ConnectedClients) { if (c.Character != revivedCharacter) { continue; } + + // If killed in ironman mode, the character has been wiped from the save mid-round, so its + // original data needs to be restored to the save file (without making a backup of the dead character) + if (GameMain.Server.ServerSettings.IronmanMode && GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign) + { + if (mpCampaign.RestoreSingleCharacterFromBackup(c) is CharacterCampaignData characterToRestore) + { + characterToRestore.CharacterInfo.PermanentlyDead = false; + mpCampaign.SaveSingleCharacter(characterToRestore, skipBackup: true); + } + } //clients stop controlling the character when it dies, force control back GameMain.Server.SetClientCharacter(c, revivedCharacter); @@ -2545,6 +2565,91 @@ namespace Barotrauma } ); + commands.Add(new Command("setsalary", "setsalary [0-100] [character/default]: Sets the salary of a certain character or the default salary to a percentage.", (string[] args) => + { + if (args.Length < 2) + { + NewMessage($"Missing arguments. Expected at least 2 but got {args.Length} (amount, character)", Color.Red); + return; + } + + if (GameMain.GameSession?.Campaign is not MultiPlayerCampaign mpCampaign) + { + NewMessage("No campaign active.", Color.Red); + return; + } + + if (!int.TryParse(args[0], out int amount)) + { + NewMessage($"{args[0]} is not a valid amount.", Color.Red); + return; + } + + if (args[1].Equals("default", StringComparison.OrdinalIgnoreCase)) + { + mpCampaign.Bank.SetRewardDistribution(amount); + NewMessage($"Set the default salary to {amount}%", Color.White); + return; + } + + Character character = FindMatchingCharacter(args.Skip(1).ToArray()); + if (character is null) + { + NewMessage($"Character not found \"{args[1]}\".", Color.Red); + return; + } + + character.Wallet.SetRewardDistribution(amount); + NewMessage($"Set {character.Name}'s salary to {amount}%", Color.White); + })); + + AssignOnClientRequestExecute( + "setsalary", + (senderClient, cursorWorldPos, args) => + { + if (args.Length < 2) + { + GameMain.Server.SendConsoleMessage($"Missing arguments. Expected at least 2 but got {args.Length} (amount, character)", senderClient, Color.Red); + return; + } + + if (!CampaignMode.AllowedToManageWallets(senderClient)) + { + GameMain.Server.SendConsoleMessage("You are not allowed to manage wallets.", senderClient, Color.Red); + return; + } + + if (GameMain.GameSession?.Campaign is not MultiPlayerCampaign mpCampaign) + { + GameMain.Server.SendConsoleMessage("No campaign active.", senderClient, Color.Red); + return; + } + + if (!int.TryParse(args[0], out int amount)) + { + GameMain.Server.SendConsoleMessage($"{args[0]} is not a valid amount.", senderClient, Color.Red); + return; + } + + if (args[1].Equals("default", StringComparison.OrdinalIgnoreCase)) + { + mpCampaign.Bank.SetRewardDistribution(amount); + GameMain.Server.SendConsoleMessage($"Set the default salary to {amount}%", senderClient); + return; + } + + Character character = FindMatchingCharacter(args.Skip(1).ToArray()); + if (character is null) + { + GameMain.Server.SendConsoleMessage($"Character not found \"{args[1]}\".", senderClient, Color.Red); + return; + } + + character.Wallet.SetRewardDistribution(amount); + GameMain.Server.SendConsoleMessage($"Set {character.Name}'s salary to {amount}%.", senderClient); + } + ); + commands.Add(new Command("readycheck", "Commence a ready check.", (string[] args) => { if (Screen.Selected == GameMain.GameScreen && GameMain.NetworkMember != null) @@ -2607,7 +2712,7 @@ namespace Barotrauma })); #endif } - + public static void ServerRead(IReadMessage inc, Client sender) { string consoleCommand = inc.ReadString(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AlienRuinMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EliminateTargetsMission.cs similarity index 92% rename from Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AlienRuinMission.cs rename to Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EliminateTargetsMission.cs index af7c2c1a9..534532927 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AlienRuinMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EliminateTargetsMission.cs @@ -2,7 +2,7 @@ using Barotrauma.Networking; namespace Barotrauma { - partial class AlienRuinMission : Mission + partial class EliminateTargetsMission : Mission { public override void ServerWriteInitial(IWriteMessage msg, Client c) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs index 5b2daa59b..f9404cc87 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs @@ -123,6 +123,12 @@ namespace Barotrauma public bool IsDuplicate(CharacterCampaignData other) { +#if DEBUG + if (RequireClientNameMatch) + { + return AccountId == other.AccountId && other.ClientAddress == ClientAddress && Name == other.Name; + } +#endif return AccountId == other.AccountId && other.ClientAddress == ClientAddress; } @@ -133,6 +139,13 @@ namespace Barotrauma WalletData = null; } + public void ApplyPermadeath() + { + Reset(); + CharacterInfo.PermanentlyDead = true; + DebugConsole.NewMessage($"Permadeath applied on {Name}'s CharacterCampaignData.CharacterInfo."); + } + public void SpawnInventoryItems(Character character, Inventory inventory) { if (character == null) @@ -158,7 +171,7 @@ namespace Barotrauma public void ApplyWalletData(Character character) { - character.Wallet = new Wallet(Option.Some(character), WalletData); + character.Wallet = new Wallet(Option.Some(character), WalletData); } public XElement Save() @@ -167,7 +180,6 @@ namespace Barotrauma new XAttribute("name", Name), new XAttribute("address", ClientAddress), new XAttribute("accountid", AccountId.TryUnwrap(out var accountId) ? accountId.StringRepresentation : "")); - CharacterInfo?.Save(element); if (itemData != null) { element.Add(itemData); } if (healthData != null) { element.Add(healthData); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index abeeeb1df..1f86f5297 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -16,6 +16,12 @@ namespace Barotrauma private readonly HashSet transactions = new HashSet(); private const float clientCheckInterval = 10; private float clientCheckTimer = clientCheckInterval; + + /// + /// Temporary backup storage for characters that have been overwritten by SaveSingleCharacter, this will be gone + /// once the round ends or the server closes. Currently needed to enable the console command "revive" in ironman mode. + /// + public List replacedCharacterDataBackup = new List(); public override Wallet GetWallet(Client client = null) { @@ -295,6 +301,12 @@ namespace Barotrauma MoveDiscardedCharacterBalancesToBank(); characterData.ForEach(cd => cd.HasSpawned = false); + foreach (var cd in characterData) + { + //remove from crewmanager - we don't need to save the data there if it's been saved as CharacterCampaignData + //(e.g. if a client has taken over a bot, we need to do this to prevent it being saved twice) + CrewManager.RemoveCharacterInfo(cd.CharacterInfo); + } SavePets(); @@ -380,6 +392,9 @@ namespace Barotrauma GameMain.GameSession.EventManager.RegisterEventHistory(); } + //store the currently active missions at this point so we can communicate their states to clients, they're cleared in EndRound + List missions = GameMain.GameSession.Missions.ToList(); + GameMain.GameSession.EndRound("", transitionType); //-------------------------------------- @@ -407,7 +422,7 @@ namespace Barotrauma //-------------------------------------- - GameMain.Server.EndGame(transitionType, wasSaved: true); + GameMain.Server.EndGame(transitionType, wasSaved: true, missions); ForceMapUI = false; @@ -513,6 +528,9 @@ namespace Barotrauma Map?.Radiation?.UpdateRadiation(deltaTime); base.Update(deltaTime); + + MedicalClinic?.Update(deltaTime); + if (Level.Loaded != null) { if (Level.Loaded.Type == LevelData.LevelType.LocationConnection) @@ -1165,42 +1183,59 @@ namespace Barotrauma if (!AllowedToManageWallets(sender)) { return; } - Character targetCharacter = Character.CharacterList.FirstOrDefault(c => c.ID == update.Target); - targetCharacter?.Wallet.SetRewardDistribution(update.NewRewardDistribution); - GameServer.Log($"{sender.Name} changed the salary of {targetCharacter?.Name ?? "the bank"} to {update.NewRewardDistribution}%.", ServerLog.MessageType.Money); + if (update.Target.TryUnwrap(out ushort id)) + { + Character targetCharacter = Character.CharacterList.FirstOrDefault(c => c.ID == id); + targetCharacter?.Wallet.SetRewardDistribution(update.NewRewardDistribution); + GameServer.Log($"{sender.Name} changed the salary of {targetCharacter?.Name} to {update.NewRewardDistribution}%.", ServerLog.MessageType.Money); + return; + } + + Bank.SetRewardDistribution(update.NewRewardDistribution); + GameServer.Log($"{sender.Name} changed the default salary to {update.NewRewardDistribution}%.", ServerLog.MessageType.Money); + } + + public void ResetSalaries(Client sender) + { + if (!AllowedToManageWallets(sender)) { return; } + + foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Player)) + { + character.Wallet.SetRewardDistribution(Bank.RewardDistribution); + } } public void ServerReadCrew(IReadMessage msg, Client sender) { - int[] pendingHires = null; + UInt16[] pendingHires = null; bool updatePending = msg.ReadBoolean(); if (updatePending) { ushort pendingHireLength = msg.ReadUInt16(); - pendingHires = new int[pendingHireLength]; + pendingHires = new UInt16[pendingHireLength]; for (int i = 0; i < pendingHireLength; i++) { - pendingHires[i] = msg.ReadInt32(); + pendingHires[i] = msg.ReadUInt16(); } } bool validateHires = msg.ReadBoolean(); bool renameCharacter = msg.ReadBoolean(); - int renamedIdentifier = -1; + UInt16 renamedIdentifier = 0; string newName = null; bool existingCrewMember = false; if (renameCharacter) { - renamedIdentifier = msg.ReadInt32(); + renamedIdentifier = msg.ReadUInt16(); newName = msg.ReadString(); existingCrewMember = msg.ReadBoolean(); } bool fireCharacter = msg.ReadBoolean(); int firedIdentifier = -1; - if (fireCharacter) { firedIdentifier = msg.ReadInt32(); } + if (fireCharacter) { firedIdentifier = msg.ReadUInt16(); } Location location = map?.CurrentLocation; CharacterInfo firedCharacter = null; @@ -1209,7 +1244,7 @@ namespace Barotrauma { if (fireCharacter) { - firedCharacter = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.GetIdentifier() == firedIdentifier); + firedCharacter = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.ID == firedIdentifier); if (firedCharacter != null && (firedCharacter.Character?.IsBot ?? true)) { CrewManager.FireCharacter(firedCharacter); @@ -1225,11 +1260,11 @@ namespace Barotrauma CharacterInfo characterInfo = null; if (existingCrewMember && CrewManager != null) { - characterInfo = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.GetIdentifierUsingOriginalName() == renamedIdentifier); + characterInfo = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.ID == renamedIdentifier); } else if(!existingCrewMember && location.HireManager != null) { - characterInfo = location.HireManager.AvailableCharacters.FirstOrDefault(info => info.GetIdentifierUsingOriginalName() == renamedIdentifier); + characterInfo = location.HireManager.AvailableCharacters.FirstOrDefault(info => info.ID == renamedIdentifier); } if (characterInfo != null && (characterInfo.Character?.IsBot ?? true)) @@ -1262,9 +1297,9 @@ namespace Barotrauma if (updatePending) { List pendingHireInfos = new List(); - foreach (int identifier in pendingHires) + foreach (UInt16 identifier in pendingHires) { - CharacterInfo match = location.GetHireableCharacters().FirstOrDefault(info => info.GetIdentifierUsingOriginalName() == identifier); + CharacterInfo match = location.GetHireableCharacters().FirstOrDefault(info => info.ID == identifier); if (match == null) { DebugConsole.ThrowError($"Tried to add a character that doesn't exist ({identifier}) to pending hires"); @@ -1311,7 +1346,7 @@ namespace Barotrauma /// 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((int id, string newName) renamedCrewMember = default, CharacterInfo firedCharacter = null) + public void SendCrewState((ushort id, string newName) renamedCrewMember = default, CharacterInfo firedCharacter = null, bool createNotification = true) { List availableHires = new List(); List pendingHires = new List(); @@ -1327,6 +1362,8 @@ namespace Barotrauma IWriteMessage msg = new WriteOnlyMessage(); msg.WriteByte((byte)ServerPacketHeader.CREW); + msg.WriteBoolean(createNotification); + msg.WriteUInt16((ushort)availableHires.Count); foreach (CharacterInfo hire in availableHires) { @@ -1337,7 +1374,7 @@ namespace Barotrauma msg.WriteUInt16((ushort)pendingHires.Count); foreach (CharacterInfo pendingHire in pendingHires) { - msg.WriteInt32(pendingHire.GetIdentifierUsingOriginalName()); + msg.WriteUInt16(pendingHire.ID); } var hiredCharacters = CrewManager.GetCharacterInfos().Where(ci => ci.IsNewHire); @@ -1348,16 +1385,16 @@ namespace Barotrauma msg.WriteInt32(info.Salary); } - bool validRenaming = renamedCrewMember.id > -1 && !string.IsNullOrEmpty(renamedCrewMember.newName); + bool validRenaming = renamedCrewMember.id > 0 && !string.IsNullOrEmpty(renamedCrewMember.newName); msg.WriteBoolean(validRenaming); if (validRenaming) { - msg.WriteInt32(renamedCrewMember.id); + msg.WriteUInt16(renamedCrewMember.id); msg.WriteString(renamedCrewMember.newName); } msg.WriteBoolean(firedCharacter != null); - if (firedCharacter != null) { msg.WriteInt32(firedCharacter.GetIdentifier()); } + if (firedCharacter != null) { msg.WriteUInt16(firedCharacter.ID); } GameMain.Server.ServerPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } @@ -1475,5 +1512,57 @@ namespace Barotrauma lastSaveID++; DebugConsole.Log("Campaign saved, save ID " + lastSaveID); } + + /// + /// Load the current character save file and add/replace a single character's data with a new version immediately. + /// + /// New character to insert. If it matches one existing in the save, that will get replaced. + /// By default, replaced characters will be temporarily backed up, but that might be unwanted + /// eg. when using this method to save a character itself restored from the backup. + public void SaveSingleCharacter(CharacterCampaignData newData, bool skipBackup = false) + { + string characterDataPath = GetCharacterDataSavePath(); + if (!File.Exists(characterDataPath)) + { + DebugConsole.ThrowError($"Failed to load the character data for the campaign. Could not find the file \"{characterDataPath}\"."); + } + else + { + var loadedCharacterData = XMLExtensions.TryLoadXml(characterDataPath); + if (loadedCharacterData?.Root == null) { return; } + var oldData = loadedCharacterData.Root.Elements() + .FirstOrDefault(subElement => new CharacterCampaignData(subElement).IsDuplicate(newData)); + + if (oldData != null) + { + if (!skipBackup) + { + replacedCharacterDataBackup.Add(new CharacterCampaignData(oldData)); + } + oldData.Remove(); + } + loadedCharacterData.Root.Add(newData.Save()); + + try + { + loadedCharacterData.SaveSafe(characterDataPath); + } + catch (Exception e) + { + DebugConsole.ThrowError("Saving multiplayer campaign characters to \"" + characterDataPath + "\" failed!", e); + } + } + } + + public CharacterCampaignData RestoreSingleCharacterFromBackup(Client client) + { + if (replacedCharacterDataBackup.Find(cd => cd.MatchesClient(client)) is CharacterCampaignData characterToRestore) + { + replacedCharacterDataBackup.Remove(characterToRestore); + return characterToRestore; + } + + return default; + } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs index 255635fb7..df75c930e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System; using System.Collections.Generic; @@ -22,6 +22,36 @@ namespace Barotrauma private readonly List afflictionSubscribers = new(); + public void Update(float deltaTime) + { + processAfflictionChangesTimer -= deltaTime; + if (processAfflictionChangesTimer <= 0.0f) + { + foreach (var character in charactersWithAfflictionChanges) + { + ImmutableArray afflictions = GetAllAfflictions(character.CharacterHealth); + foreach (AfflictionSubscriber sub in afflictionSubscribers.ToList()) + { + if (sub.Expiry < DateTimeOffset.Now) + { + afflictionSubscribers.Remove(sub); + continue; + } + + if (sub.Target == character.Info) + { + ServerSend(new NetCrewMember(character.Info, afflictions), + header: NetworkHeader.AFFLICTION_UPDATE, + deliveryMethod: DeliveryMethod.Unreliable, + targetClient: sub.Subscriber); + } + } + } + charactersWithAfflictionChanges.Clear(); + processAfflictionChangesTimer = ProcessAfflictionChangesInterval; + } + } + public void ServerRead(IReadMessage inc, Client sender) { NetworkHeader header = (NetworkHeader)inc.ReadByte(); @@ -141,7 +171,7 @@ namespace Barotrauma if (foundInfo is { Character.CharacterHealth: { } health }) { pendingAfflictions = GetAllAfflictions(health); - infoId = foundInfo.GetIdentifierUsingOriginalName(); + infoId = foundInfo.ID; } INetSerializableStruct writeCrewMember = new NetCrewMember diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs index 25d808849..f3dbfeb14 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs @@ -18,10 +18,13 @@ namespace Barotrauma.Items.Components private bool needsServerInitialization; /// - /// When in multiplayer and the circuit box is loaded from the players inventory, - /// We only load the components from XML on server side since only the server has access to CharacterCampaignData - /// and then send a network event syncing the loaded properties. But circuit box properties are too complex to - /// sync using the existing syncing logic so we instead send the state using . + /// When in multiplayer and the circuit box are loaded from the player inventory, + /// We only load the components from XML on the server side + /// since only the server has access to CharacterCampaignData + /// and then send a network event syncing the loaded properties. + /// But circuit box properties are too complex to + /// sync using the existing syncing logic, + /// so we instead send the state using . /// public void MarkServerRequiredInitialization() => needsServerInitialization = true; @@ -280,6 +283,15 @@ namespace Barotrauma.Items.Components CreateServerEvent(data with { Size = Vector2.Max(data.Size, CircuitBoxLabelNode.MinSize) }); break; } + case CircuitBoxOpcode.RenameConnections: + { + var data = INetSerializableStruct.Read(msg); + if (!CanAccessAndUnlocked(c)) { break; } + + RenameConnectionLabelsInternal(data.Type, data.Override.ToDictionary()); + CreateServerEvent(data); + break; + } default: throw new ArgumentOutOfRangeException(nameof(header), header, "This opcode cannot be handled using entity events"); } @@ -327,6 +339,7 @@ namespace Barotrauma.Items.Components Components: Components.Select(EventFromComponent).ToImmutableArray(), Wires: Wires.Select(EventFromWire).ToImmutableArray(), Labels: Labels.Select(EventFromLabel).ToImmutableArray(), + LabelOverrides: InputOutputNodes.Select(EventFromLabelOverride).ToImmutableArray(), InputPos: inputPos, OutputPos: outputPos); @@ -347,6 +360,9 @@ namespace Barotrauma.Items.Components static CircuitBoxServerAddLabelEvent EventFromLabel(CircuitBoxLabelNode label) => new(label.ID, label.Position, label.Size, label.Color, label.HeaderText, label.BodyText); + + static CircuitBoxRenameConnectionLabelsEvent EventFromLabelOverride(CircuitBoxInputOutputNode node) + => new(node.NodeType, node.ConnectionLabelOverrides.ToNetDictionary()); } // we don't care about updating the view on server diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs index 3370c6afd..276ecd2cb 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs @@ -98,7 +98,7 @@ namespace Barotrauma.Items.Components //existing wire not in the list of new wires -> disconnect it if (!wires[i].Contains(existingWire)) { - if (existingWire.Locked) + if (existingWire.Locked || existingWire.Item.IsLayerHidden) { //this should not be possible unless the client is running a modified version of the game GameServer.Log(GameServer.CharacterLogName(c.Character) + " attempted to disconnect a locked wire from " + @@ -166,18 +166,6 @@ namespace Barotrauma.Items.Components } } - foreach (Wire disconnectedWire in DisconnectedWires.ToList()) - { - if (disconnectedWire.Connections[0] == null && - disconnectedWire.Connections[1] == null && - !clientSideDisconnectedWires.Contains(disconnectedWire) && - disconnectedWire.Item.ParentInventory == null) - { - disconnectedWire.Item.Drop(c.Character); - GameServer.Log(GameServer.CharacterLogName(c.Character) + " dropped " + disconnectedWire.Name, ServerLog.MessageType.Inventory); - } - } - //go through new wires for (int i = 0; i < Connections.Count; i++) { @@ -205,6 +193,18 @@ namespace Barotrauma.Items.Components } } } + + foreach (Wire disconnectedWire in DisconnectedWires.ToList()) + { + if (disconnectedWire.Connections[0] == null && + disconnectedWire.Connections[1] == null && + !clientSideDisconnectedWires.Contains(disconnectedWire) && + disconnectedWire.Item.ParentInventory == null) + { + disconnectedWire.Item.Drop(c.Character); + GameServer.Log(GameServer.CharacterLogName(c.Character) + " dropped " + disconnectedWire.Name, ServerLog.MessageType.Inventory); + } + } } public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs index 3d42626e3..6748365aa 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs @@ -11,153 +11,42 @@ namespace Barotrauma { private readonly Dictionary[]> receivedItemIds = new Dictionary[]>(); - public void ServerEventRead(IReadMessage msg, Client c) + public void ServerEventRead(IReadMessage msg, Client sender) { - if (!receivedItemIds.TryGetValue(c, out List[] receivedItemIdsFromClient)) + // if the dictionary doesn't contain the client entry, create a new one + if (!receivedItemIds.TryGetValue(sender, out List[] receivedItemIdsFromClient)) { receivedItemIdsFromClient = new List[capacity]; - receivedItemIds.Add(c, receivedItemIdsFromClient); + receivedItemIds.Add(sender, receivedItemIdsFromClient); } + // Read some item ids from the message. readyToApply waits for all the data from possible multiple messages. SharedRead(msg, receivedItemIdsFromClient, out bool readyToApply); if (!readyToApply) { return; } - if (c == null || c.Character == null) { return; } - - bool accessible = c.Character.CanAccessInventory(this); - if (this is CharacterInventory characterInventory && accessible) + if (sender == null || sender.Character == null) { return; } + + if (!IsInventoryAccessible()) { - if (Owner == null || Owner is not Character ownerCharacter) - { - accessible = false; - } - else if (!characterInventory.AccessibleWhenAlive && !ownerCharacter.IsDead && !characterInventory.AccessibleByOwner) - { - accessible = false; - } - } - - if (!accessible) - { - //create a network event to correct the client's inventory state - //otherwise they may have an item in their inventory they shouldn't have been able to pick up, - //and receiving an event for that inventory later will cause the item to be dropped - CreateNetworkEvent(); - for (int i = 0; i < capacity; i++) - { - foreach (ushort id in receivedItemIdsFromClient[i]) - { - if (Entity.FindEntityByID(id) is not Item item) { continue; } - item.PositionUpdateInterval = 0.0f; - if (item.ParentInventory != null && item.ParentInventory != this) - { - item.ParentInventory.CreateNetworkEvent(); - } - } - } + CreateCorrectiveNetworkEvent(); return; } - - //we need to check which of the items the client can access at this point, before we start shuffling things around - //otherwise if you're e.g. holding an item to access a cabinet, and picking up an item from the cabinet unequips the item you're holding, - //you would fail to pick up the item because it gets unequipped before checking whether you can access the cabinet. - Dictionary canAccessItem = new Dictionary(); - for (int i = 0; i < capacity; i++) - { - foreach (ushort id in receivedItemIdsFromClient[i]) - { - if (Entity.FindEntityByID(id) is not Item item) { continue; } - canAccessItem[item] = item.CanClientAccess(c); - } - } - + List prevItems = new List(AllItems.Distinct()); List prevItemInventories = new List() { this }; - for (int i = 0; i < capacity; i++) - { - foreach (Item item in slots[i].Items.ToList()) - { - if (!receivedItemIdsFromClient[i].Contains(item.ID) && item.IsInteractable(c.Character)) - { - Item droppedItem = item; - Entity prevOwner = Owner; - Inventory previousInventory = droppedItem.ParentInventory; - droppedItem.Drop(null); - droppedItem.PreviousParentInventory = previousInventory; - var previousCharacterInventory = prevOwner switch - { - Item itemInventory => itemInventory.FindParentInventory(inventory => inventory is CharacterInventory) as CharacterInventory, - Character character => character.Inventory, - _ => null - }; + //we need to check which of the items the client (sender) can access at this point, before we start shuffling things around + //otherwise if you're e.g. holding an item to access a cabinet, and picking up an item from the cabinet unequips the item you're holding, + //you would fail to pick up the item because it gets unequipped before checking whether you can access the cabinet. + var itemAccessibility = GetItemAccessibility(); + + HandleRemovedItems(); - if (previousCharacterInventory != null && previousCharacterInventory != c.Character?.Inventory) - { - GameMain.Server?.KarmaManager.OnItemTakenFromPlayer(previousCharacterInventory, c, droppedItem); - } - - if (droppedItem.body != null && prevOwner != null) - { - droppedItem.body.SetTransform(prevOwner.SimPosition, 0.0f); - } - } - } + HandleAddedItems(); - foreach (ushort id in receivedItemIdsFromClient[i]) - { - Item newItem = id == 0 ? null : Entity.FindEntityByID(id) as Item; - prevItemInventories.Add(newItem?.ParentInventory); - } - } + EnsureItemsInBothHands(sender.Character); - for (int i = 0; i < capacity; i++) - { - foreach (ushort id in receivedItemIdsFromClient[i]) - { - if (Entity.FindEntityByID(id) is not Item item || slots[i].Contains(item)) { continue; } - - if (item.GetComponent() is not Pickable pickable || - (pickable.IsAttached && !pickable.PickingDone) || item.AllowedSlots.None() || !item.IsInteractable(c.Character)) - { - DebugConsole.AddWarning($"Client {c.Name} tried to pick up a non-pickable item \"{item}\" (parent inventory: {item.ParentInventory?.Owner.ToString() ?? "null"})", - item.Prefab.ContentPackage); - continue; - } - - if (GameMain.Server != null) - { - var holdable = item.GetComponent(); - if (holdable != null && !holdable.CanBeDeattached()) { continue; } - - if (!prevItems.Contains(item) && !canAccessItem[item] && - (c.Character == null || item.PreviousParentInventory == null || !c.Character.CanAccessInventory(item.PreviousParentInventory))) - { - #if DEBUG || UNSTABLE - DebugConsole.NewMessage($"Client {c.Name} failed to pick up item \"{item}\" (parent inventory: {item.ParentInventory?.Owner.ToString() ?? "null"}). No access.", Color.Yellow); - #endif - if (item.body != null && !c.PendingPositionUpdates.Contains(item)) - { - c.PendingPositionUpdates.Enqueue(item); - } - item.PositionUpdateInterval = 0.0f; - continue; - } - } - TryPutItem(item, i, true, true, c.Character, false); - for (int j = 0; j < capacity; j++) - { - if (slots[j].Contains(item) && !receivedItemIdsFromClient[j].Contains(item.ID)) - { - slots[j].RemoveItem(item); - } - } - } - } - - EnsureItemsInBothHands(c.Character); - - receivedItemIds.Remove(c); + receivedItemIds.Remove(sender); CreateNetworkEvent(); foreach (Inventory prevInventory in prevItemInventories.Distinct()) @@ -165,43 +54,211 @@ namespace Barotrauma if (prevInventory != this) { prevInventory?.CreateNetworkEvent(); } } - foreach (Item item in AllItems.DistinctBy(it => it.Prefab)) + ServerLogAddedItems(); + + ServerLogRemovedItems(); + + #region local functions + bool IsInventoryAccessible() => sender.Character.CanAccessInventory(this, IsDragAndDropGiveAllowed ? CharacterInventory.AccessLevel.Allowed : CharacterInventory.AccessLevel.Limited); + + void CreateCorrectiveNetworkEvent() { - if (item == null) { continue; } - if (!prevItems.Contains(item)) + // create a network event to correct the client's inventory state. + // Otherwise they may have an item in their inventory they shouldn't have been able to pick up, + // and receiving an event for that inventory later will cause the item to be dropped + CreateNetworkEvent(); + for (int i = 0; i < capacity; i++) { - int amount = AllItems.Count(it => it.Prefab == item.Prefab && !prevItems.Contains(it)); - string amountText = amount > 1 ? $"x{amount} " : string.Empty; - if (Owner == c.Character) + foreach (ushort itemId in receivedItemIdsFromClient[i]) { - HumanAIController.ItemTaken(item, c.Character); - GameServer.Log($"{GameServer.CharacterLogName(c.Character)} picked up {amountText}{item.Name}", ServerLog.MessageType.Inventory); + if (Entity.FindEntityByID(itemId) is not Item item) { continue; } + item.PositionUpdateInterval = 0.0f; + if (item.ParentInventory != null && item.ParentInventory != this) + { + item.ParentInventory.CreateNetworkEvent(); + } + } + } + } + + Dictionary GetItemAccessibility() + { + Dictionary itemAccessibility = new Dictionary(); + + for (int i = 0; i < capacity; i++) + { + // for every item that the new inventory state contains + foreach (ushort itemId in receivedItemIdsFromClient[i]) + { + // if there is no such item, skip + if (Entity.FindEntityByID(itemId) is not Item item) { continue; } + // add entry: can the sender access the item? + itemAccessibility[item] = item.CanClientAccess(sender); + } + } + + // we now have accessibility for every item in the new inventory state + // but not for the items that were in the inventory before and perhaps dropped, so let's add those as well + foreach (var item in prevItems) + { + if (!itemAccessibility.ContainsKey(item)) + { + itemAccessibility[item] = item.CanClientAccess(sender); + } + } + + return itemAccessibility; + } + + void HandleRemovedItems() + { + for (int slotIndex = 0; slotIndex < capacity; slotIndex++) + { + foreach (Item item in slots[slotIndex].Items.ToList()) + { + bool shouldBeRemoved = !receivedItemIdsFromClient[slotIndex].Contains(item.ID) && + item.IsInteractable(sender.Character); // item is interactable to sender: not hidden and player team + if (shouldBeRemoved) + { + bool itemAccessDenied = prevItems.Contains(item) && // if the item was in the inventory before + !itemAccessibility[item] && // and the sender is not allowed to access it + (item.PreviousParentInventory == null || // and either the item has no previous inventory + !sender.Character.CanAccessInventory(item.PreviousParentInventory)); // or the sender can't access the previous inventory + + if (itemAccessDenied) + { +#if DEBUG || UNSTABLE + DebugConsole.NewMessage($"Client {sender.Name} failed to drop item \"{item}\" (parent inventory: {item.ParentInventory?.Owner.ToString() ?? "null"}). No access.", Color.Yellow); +#endif + continue; + } + + Item droppedItem = item; + Entity prevOwner = Owner; + Inventory previousInventory = droppedItem.ParentInventory; + droppedItem.Drop(null); + droppedItem.PreviousParentInventory = previousInventory; + + var previousCharacterInventory = prevOwner switch + { + Item itemInventory => itemInventory.FindParentInventory(inventory => inventory is CharacterInventory) as CharacterInventory, + Character character => character.Inventory, + _ => null + }; + + if (previousCharacterInventory != null && previousCharacterInventory != sender.Character?.Inventory) + { + GameMain.Server?.KarmaManager.OnItemTakenFromPlayer(previousCharacterInventory, sender, droppedItem); + } + + if (droppedItem.body != null && prevOwner != null) + { + droppedItem.body.SetTransform(prevOwner.SimPosition, 0.0f); + } + } + } + + foreach (ushort id in receivedItemIdsFromClient[slotIndex]) + { + Item newItem = id == 0 ? null : Entity.FindEntityByID(id) as Item; + prevItemInventories.Add(newItem?.ParentInventory); + } + } + } + + void HandleAddedItems() + { + for (int slotIndex = 0; slotIndex < capacity; slotIndex++) + { + foreach (ushort id in receivedItemIdsFromClient[slotIndex]) + { + if (Entity.FindEntityByID(id) is not Item item || slots[slotIndex].Contains(item)) { continue; } + + if (item.GetComponent() is not Pickable pickable || + (pickable.IsAttached && !pickable.PickingDone) || item.AllowedSlots.None() || !item.IsInteractable(sender.Character)) + { + DebugConsole.AddWarning($"Client {sender.Name} tried to pick up a non-pickable item \"{item}\" (parent inventory: {item.ParentInventory?.Owner.ToString() ?? "null"})", + item.Prefab.ContentPackage); + continue; + } + + if (GameMain.Server != null) + { + var holdable = item.GetComponent(); + if (holdable != null && !holdable.CanBeDeattached()) { continue; } + + bool itemAccessDenied = !prevItems.Contains(item) && !itemAccessibility[item] && + (sender.Character == null || item.PreviousParentInventory == null || !sender.Character.CanAccessInventory(item.PreviousParentInventory)); + + if (itemAccessDenied) + { +#if DEBUG || UNSTABLE + DebugConsole.NewMessage($"Client {sender.Name} failed to pick up item \"{item}\" (parent inventory: {item.ParentInventory?.Owner.ToString() ?? "null"}). No access.", Color.Yellow); +#endif + if (item.body != null && !sender.PendingPositionUpdates.Contains(item)) + { + sender.PendingPositionUpdates.Enqueue(item); + } + item.PositionUpdateInterval = 0.0f; + continue; + } + } + TryPutItem(item, slotIndex, true, true, sender.Character, false); + for (int j = 0; j < capacity; j++) + { + if (slots[j].Contains(item) && !receivedItemIdsFromClient[j].Contains(item.ID)) + { + slots[j].RemoveItem(item); + } + } + } + } + } + + void ServerLogAddedItems() + { + foreach (Item item in AllItems.DistinctBy(it => it.Prefab)) + { + if (item == null) { continue; } + if (!prevItems.Contains(item)) + { + int amount = AllItems.Count(it => it.Prefab == item.Prefab && !prevItems.Contains(it)); + string amountText = amount > 1 ? $"x{amount} " : string.Empty; + if (Owner == sender.Character) + { + HumanAIController.ItemTaken(item, sender.Character); + GameServer.Log($"{GameServer.CharacterLogName(sender.Character)} picked up {amountText}{item.Name}", ServerLog.MessageType.Inventory); + } + else + { + GameServer.Log($"{GameServer.CharacterLogName(sender.Character)} placed {amountText}{item.Name} in the inventory of {Owner}", ServerLog.MessageType.Inventory); + } + } + } + } + + void ServerLogRemovedItems() + { + var droppedItems = prevItems.Where(it => it != null && !AllItems.Contains(it)); + foreach (Item item in droppedItems.DistinctBy(it => it.Prefab)) + { + var matchingItems = prevItems.Where(it => it.Prefab == item.Prefab && !AllItems.Contains(it)); + int amount = matchingItems.Count(); + string amountText = amount > 1 ? $"x{amount} " : string.Empty; + if (Owner == sender.Character) + { + GameServer.Log($"{GameServer.CharacterLogName(sender.Character)} dropped {amountText}{item.Name}", ServerLog.MessageType.Inventory); } else { - GameServer.Log($"{GameServer.CharacterLogName(c.Character)} placed {amountText}{item.Name} in {Owner}", ServerLog.MessageType.Inventory); + GameServer.Log($"{GameServer.CharacterLogName(sender.Character)} removed {amountText}{item.Name} from the inventory of {Owner}", ServerLog.MessageType.Inventory); } + item.CreateDroppedStack(matchingItems, allowClientExecute: true); } } - - var droppedItems = prevItems.Where(it => it != null && !AllItems.Contains(it)); - foreach (Item item in droppedItems.DistinctBy(it => it.Prefab)) - { - var matchingItems = prevItems.Where(it => it.Prefab == item.Prefab && !AllItems.Contains(it)); - int amount = matchingItems.Count(); - string amountText = amount > 1 ? $"x{amount} " : string.Empty; - if (Owner == c.Character) - { - GameServer.Log($"{GameServer.CharacterLogName(c.Character)} dropped {amountText}{item.Name}", ServerLog.MessageType.Inventory); - } - else - { - GameServer.Log($"{GameServer.CharacterLogName(c.Character)} removed {amountText}{item.Name} from {Owner}", ServerLog.MessageType.Inventory); - } - item.CreateDroppedStack(matchingItems, allowClientExecute: true); - } + #endregion } - + private void EnsureItemsInBothHands(Character character) { if (this is not CharacterInventory charInv) { return; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs index 93151a357..f95371efe 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs @@ -90,7 +90,7 @@ namespace Barotrauma.Networking if (c.Character == null || c.Character.SpeechImpediment >= 100.0f || c.Character.IsDead) { return; } if (orderMsg.Order.IsReport) { - HumanAIController.ReportProblem(orderMsg.Sender, orderMsg.Order); + HumanAIController.ReportProblem(orderMsg.Sender as Character, orderMsg.Order); } if (order != null) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs index f8f8d2bbd..c5e231d82 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs @@ -1,4 +1,5 @@ -using System; +using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; using System.Linq; @@ -8,6 +9,8 @@ namespace Barotrauma.Networking { public bool VoiceEnabled = true; + public VoipServerDecoder VoipServerDecoder; + public UInt16 LastRecvClientListUpdate = NetIdUtils.GetIdOlderThan(GameMain.Server.LastClientListUpdateID); @@ -15,7 +18,7 @@ namespace Barotrauma.Networking = NetIdUtils.GetIdOlderThan(GameMain.Server.ServerSettings.LastUpdateIdForFlag[ServerSettings.NetFlags.Properties]); public UInt16 LastRecvServerSettingsUpdate = NetIdUtils.GetIdOlderThan(GameMain.Server.ServerSettings.LastUpdateIdForFlag[ServerSettings.NetFlags.Properties]); - + public UInt16 LastRecvLobbyUpdate = NetIdUtils.GetIdOlderThan(GameMain.NetLobbyScreen.LastUpdateID); @@ -129,6 +132,7 @@ namespace Barotrauma.Networking JobPreferences = new List(); VoipQueue = new VoipQueue(SessionId, true, true); + VoipServerDecoder = new VoipServerDecoder(VoipQueue, this); GameMain.Server.VoipServer.RegisterQueue(VoipQueue); //initialize to infinity, gets set to a proper value when initializing midround syncing @@ -277,5 +281,47 @@ namespace Barotrauma.Networking { return Permissions.HasFlag(permission); } + + public bool TryTakeOverBot(Character botCharacter) + { + if (GameMain.Server == null) + { + DebugConsole.ThrowError($"TryTakeOverBot: Client {Name} requested to take over a bot but GameMain.Server is null!"); + return false; + } + if (GameMain.NetworkMember is not { ServerSettings.RespawnMode: RespawnMode.Permadeath }) + { + DebugConsole.ThrowError($"Client {Name} requested to take over a bot but Permadeath is not enabled!"); + GameMain.Server.SendConsoleMessage($"Permadeath mode is not enabled, cannot take over a bot.", this, Color.Red); + return false; + } + if (CharacterInfo == null) + { + DebugConsole.ThrowError($"Permadeath: Client {Name} requested to take over a bot, but they don't seem to have a character at all yet."); + GameMain.Server.SendConsoleMessage($"Permadeath: Taking over a bot requires having a character that died first.", this, Color.Red); + return false; + } + if (CharacterInfo is not { PermanentlyDead: true }) + { + DebugConsole.ThrowError($"Permadeath: Client {Name} requested to take over a bot, but their character has not been permanently killed."); + GameMain.Server.SendConsoleMessage($"Permadeath: Could not take over the bot, previous character not permanently killed.", this, Color.Red); + return false; + } + if (!botCharacter.IsBot) + { + DebugConsole.ThrowError($"Permadeath: {Name} requested to take over a bot character, but the target character is not a bot!"); + GameMain.Server.SendConsoleMessage($"Permadeath: Could not take over the target character because it is not a bot.", this, Color.Red); + return false; + } + + // Now that the old permanently killed character will be replaced, we can fully discard it + if (GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign) + { + mpCampaign.DiscardClientCharacterData(this); + } + GameMain.Server.SetClientCharacter(this, botCharacter); + SpectateOnly = false; + return true; + } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 85e4b7fb8..14cd1795f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -58,7 +58,10 @@ namespace Barotrauma.Networking private bool wasReadyToStartAutomatically; private bool autoRestartTimerRunning; - private float endRoundTimer; + public float EndRoundTimer { get; private set; } + public float EndRoundDelay { get; private set; } + + public float EndRoundTimeRemaining => EndRoundTimer > 0 ? EndRoundDelay - EndRoundTimer : 0; /// /// Chat messages that get sent to the owner of the server when the owner is determined @@ -354,6 +357,10 @@ namespace Barotrauma.Networking if (ServerSettings.VoiceChatEnabled) { VoipServer.SendToClients(connectedClients); + foreach (var c in connectedClients) + { + c.VoipServerDecoder.DebugUpdate(deltaTime); + } } if (GameStarted) @@ -361,6 +368,7 @@ namespace Barotrauma.Networking RespawnManager?.Update(deltaTime); entityEventManager.Update(connectedClients); + bool permadeathMode = ServerSettings.RespawnMode == RespawnMode.Permadeath; //go through the characters backwards to give rejoining clients control of the latest created character for (int i = Character.CharacterList.Count - 1; i >= 0; i--) @@ -371,13 +379,15 @@ namespace Barotrauma.Networking Client owner = connectedClients.Find(c => (c.Character == null || c.Character == character) && character.IsClientOwner(c)); bool canOwnerTakeControl = owner != null && owner.InGame && !owner.NeedsMidRoundSync && - (!ServerSettings.AllowSpectating || !owner.SpectateOnly); + (!ServerSettings.AllowSpectating || !owner.SpectateOnly || + (permadeathMode && (!character.IsDead || character.CauseOfDeath?.Type == CauseOfDeathType.Disconnected))); if (!character.IsDead) { character.KillDisconnectedTimer += deltaTime; character.SetStun(1.0f); - if ((OwnerConnection == null || owner?.Connection != OwnerConnection) && character.KillDisconnectedTimer > ServerSettings.KillDisconnectedTime) + if ((OwnerConnection == null || owner?.Connection != OwnerConnection) && + character.KillDisconnectedTimer > (permadeathMode ? ServerSettings.DespawnDisconnectedPermadeathTime : ServerSettings.KillDisconnectedTime)) { character.Kill(CauseOfDeathType.Disconnected, null); continue; @@ -402,8 +412,10 @@ namespace Barotrauma.Networking Voting.Update(deltaTime); - bool isCrewDead = + bool isCrewDown = connectedClients.All(c => !c.UsingFreeCam && (c.Character == null || c.Character.IsDead || c.Character.IsIncapacitated)); + bool isSomeoneIncapacitatedNotDead = + connectedClients.Any(c => !c.UsingFreeCam && c.Character is { IsDead: false, IsIncapacitated: true }); bool subAtLevelEnd = false; if (Submarine.MainSub != null && GameMain.GameSession.GameMode is not PvPMode) @@ -432,45 +444,58 @@ namespace Barotrauma.Networking } } - float endRoundDelay = 1.0f; - if (ServerSettings.AutoRestart && isCrewDead) + EndRoundDelay = 1.0f; + if (permadeathMode && isCrewDown) { - endRoundDelay = 5.0f; - endRoundTimer += deltaTime; + if (EndRoundTimer <= 0.0f) + { + CreateEntityEvent(RespawnManager); + } + EndRoundDelay = 120.0f; + EndRoundTimer += deltaTime; + } + else if (ServerSettings.AutoRestart && isCrewDown) + { + EndRoundDelay = isSomeoneIncapacitatedNotDead ? 120.0f : 5.0f; + EndRoundTimer += deltaTime; } else if (subAtLevelEnd && GameMain.GameSession?.GameMode is not CampaignMode) { - endRoundDelay = 5.0f; - endRoundTimer += deltaTime; + EndRoundDelay = 5.0f; + EndRoundTimer += deltaTime; } - else if (isCrewDead && (RespawnManager == null || !RespawnManager.CanRespawnAgain)) + else if (isCrewDown && (RespawnManager == null || !RespawnManager.CanRespawnAgain)) { #if !DEBUG - if (endRoundTimer <= 0.0f) + if (EndRoundTimer <= 0.0f) { - SendChatMessage(TextManager.GetWithVariable("CrewDeadNoRespawns", "[time]", "60").Value, ChatMessageType.Server); + SendChatMessage(TextManager.GetWithVariable("CrewDeadNoRespawns", "[time]", "120").Value, ChatMessageType.Server); } - endRoundDelay = 60.0f; - endRoundTimer += deltaTime; + EndRoundDelay = 120.0f; + EndRoundTimer += deltaTime; #endif } - else if (isCrewDead && (GameMain.GameSession?.GameMode is CampaignMode)) + else if (isCrewDown && (GameMain.GameSession?.GameMode is CampaignMode)) { #if !DEBUG - endRoundDelay = 2.0f; - endRoundTimer += deltaTime; + EndRoundDelay = isSomeoneIncapacitatedNotDead ? 120.0f : 2.0f; + EndRoundTimer += deltaTime; #endif } else { - endRoundTimer = 0.0f; + EndRoundTimer = 0.0f; } - if (endRoundTimer >= endRoundDelay) + if (EndRoundTimer >= EndRoundDelay) { - if (ServerSettings.AutoRestart && isCrewDead) + if (permadeathMode && isCrewDown) { - Log("Ending round (entire crew dead)", ServerLog.MessageType.ServerMessage); + Log("Ending round (entire crew dead or down and did not acquire new characters in time)", ServerLog.MessageType.ServerMessage); + } + else if (ServerSettings.AutoRestart && isCrewDown) + { + Log("Ending round (entire crew down)", ServerLog.MessageType.ServerMessage); } else if (subAtLevelEnd) { @@ -478,11 +503,11 @@ namespace Barotrauma.Networking } else if (RespawnManager == null) { - Log("Ending round (no living players left and respawning is not enabled during this round)", ServerLog.MessageType.ServerMessage); + Log("Ending round (no players left standing and respawning is not enabled during this round)", ServerLog.MessageType.ServerMessage); } else { - Log("Ending round (no living players left)", ServerLog.MessageType.ServerMessage); + Log("Ending round (no players left standing)", ServerLog.MessageType.ServerMessage); } EndGame(wasSaved: false); return; @@ -824,7 +849,7 @@ namespace Barotrauma.Networking #endif return; } - connectedClient.VoipQueue.Read(inc); + VoipServer.Read(inc, connectedClient); } break; case ClientPacketHeader.SERVER_SETTINGS: @@ -842,6 +867,9 @@ namespace Barotrauma.Networking case ClientPacketHeader.REWARD_DISTRIBUTION: ReadRewardDistributionMessage(inc, connectedClient); break; + case ClientPacketHeader.RESET_REWARD_DISTRIBUTION: + ResetRewardDistribution(connectedClient); + break; case ClientPacketHeader.MEDICAL: ReadMedicalMessage(inc, connectedClient); break; @@ -854,6 +882,9 @@ namespace Barotrauma.Networking case ClientPacketHeader.READY_TO_SPAWN: ReadReadyToSpawnMessage(inc, connectedClient); break; + case ClientPacketHeader.TAKEOVERBOT: + ReadTakeOverBotMessage(inc, connectedClient); + break; case ClientPacketHeader.FILE_REQUEST: if (ServerSettings.AllowFileTransfers) { @@ -1307,6 +1338,14 @@ namespace Barotrauma.Networking mpCampaign.ServerReadRewardDistribution(inc, sender); } } + + private void ResetRewardDistribution(Client client) + { + if (GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign) + { + mpCampaign.ResetSalaries(client); + } + } private void ReadMedicalMessage(IReadMessage inc, Client sender) { @@ -1342,6 +1381,80 @@ namespace Barotrauma.Networking } } + private void ReadTakeOverBotMessage(IReadMessage inc, Client sender) + { + UInt16 botId = inc.ReadUInt16(); + if (GameMain.GameSession?.GameMode is not MultiPlayerCampaign campaign) { return; } + + if (ServerSettings.IronmanMode) + { + DebugConsole.ThrowError($"Client {sender.Name} has requested to take over a bot in Ironman mode!"); + return; + } + + if (campaign.CurrentLocation.GetHireableCharacters().FirstOrDefault(c => c.ID == botId) is CharacterInfo hireableCharacter) + { + if (campaign.TryHireCharacter(campaign.CurrentLocation, hireableCharacter, takeMoney: true, sender)) + { + campaign.CurrentLocation.RemoveHireableCharacter(hireableCharacter); + SpawnAndTakeOverBot(campaign, hireableCharacter, sender); + campaign.SendCrewState(createNotification: false); + } + else + { + SendConsoleMessage($"Could not hire the bot {hireableCharacter.Name}.", sender, Color.Red); + DebugConsole.ThrowError($"Client {sender.Name} failed to hire the bot {hireableCharacter.Name}."); + } + } + else + { + CharacterInfo botInfo = GameMain.GameSession.CrewManager?.GetCharacterInfos()?.FirstOrDefault(i => i.ID == botId); + + if (botInfo is { IsNewHire: true, Character: null }) + { + SpawnAndTakeOverBot(campaign, botInfo, sender); + } + else if (botInfo?.Character == null || !botInfo.Character.IsBot) + { + SendConsoleMessage($"Could not find a bot with the id {botId}.", sender, Color.Red); + DebugConsole.ThrowError($"Client {sender.Name} failed to take over a bot (Could not find a bot with the id {botId})."); + return; + } + else if (ServerSettings.AllowBotTakeoverOnPermadeath) + { + sender.TryTakeOverBot(botInfo.Character); + } + else + { + SendConsoleMessage($"Failed to take over a bot (taking control of bots is disallowed).", sender, Color.Red); + DebugConsole.ThrowError($"Client {sender.Name} failed to take over a bot (taking control of bots is disallowed)."); + } + } + } + + private static void SpawnAndTakeOverBot(CampaignMode campaign, CharacterInfo botInfo, Client client) + { + var mainSubSpawnpoint = WayPoint.SelectCrewSpawnPoints(botInfo.ToEnumerable().ToList(), Submarine.MainSub).FirstOrDefault(); + var spawnWaypoint = campaign.CrewManager.GetOutpostSpawnpoints()?.FirstOrDefault() ?? mainSubSpawnpoint; + if (spawnWaypoint == null) + { + DebugConsole.ThrowError("SpawnAndTakeOverBot: Unable to find any spawn waypoints inside the sub"); + return; + } + Entity.Spawner.AddCharacterToSpawnQueue(botInfo.SpeciesName, spawnWaypoint.WorldPosition, botInfo, onSpawn: newCharacter => + { + if (newCharacter == null) + { + DebugConsole.ThrowError("SpawnAndTakeOverBot: newCharacter is null somehow"); + return; + } + campaign.CrewManager.RemoveCharacterInfo(botInfo); + newCharacter.TeamID = CharacterTeamType.Team1; + campaign.CrewManager.InitializeCharacter(newCharacter, mainSubSpawnpoint, spawnWaypoint); + client.TryTakeOverBot(newCharacter); + }); + } + private void ClientReadServerCommand(IReadMessage inc) { Client sender = ConnectedClients.Find(x => x.Connection == inc.Sender); @@ -1450,9 +1563,8 @@ namespace Barotrauma.Networking if (mpCampaign != null && Level.IsLoadedFriendlyOutpost && save) { mpCampaign.SavePlayers(); + mpCampaign.HandleSaveAndQuit(); GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); - mpCampaign.UpdateStoreStock(); - GameMain.GameSession?.EventManager?.RegisterEventHistory(registerFinishedOnly: true); SaveUtil.SaveGame(GameMain.GameSession.SavePath); } else @@ -1686,6 +1798,8 @@ namespace Barotrauma.Networking outmsg.WriteBoolean(GameStarted); outmsg.WriteBoolean(ServerSettings.AllowSpectating); + outmsg.WriteBoolean(ServerSettings.RespawnMode == RespawnMode.Permadeath); + outmsg.WriteBoolean(ServerSettings.IronmanMode); c.WritePermissions(outmsg); } @@ -1776,6 +1890,7 @@ namespace Barotrauma.Networking IWriteMessage outmsg = new WriteOnlyMessage(); outmsg.WriteByte((byte)ServerPacketHeader.UPDATE_INGAME); outmsg.WriteSingle((float)NetTime.Now); + outmsg.WriteSingle(EndRoundTimeRemaining); using (var segmentTable = SegmentTableWriter.StartWriting(outmsg)) { @@ -1858,7 +1973,8 @@ namespace Barotrauma.Networking { outmsg = new WriteOnlyMessage(); outmsg.WriteByte((byte)ServerPacketHeader.UPDATE_INGAME); - outmsg.WriteSingle((float)Lidgren.Network.NetTime.Now); + outmsg.WriteSingle((float)NetTime.Now); + outmsg.WriteSingle(EndRoundTimeRemaining); using (var segmentTable = SegmentTableWriter.StartWriting(outmsg)) { @@ -2320,17 +2436,18 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Failure; } - bool missionAllowRespawn = !(GameMain.GameSession.GameMode is MissionMode missionMode) || !missionMode.Missions.Any(m => !m.AllowRespawn); + bool missionAllowRespawn = GameMain.GameSession.GameMode is not MissionMode missionMode || !missionMode.Missions.Any(m => !m.AllowRespawn); bool isOutpost = campaign != null && campaign.NextLevel?.Type == LevelData.LevelType.Outpost; - if (ServerSettings.AllowRespawn && missionAllowRespawn) + if (ServerSettings.RespawnMode != RespawnMode.BetweenRounds && missionAllowRespawn) { RespawnManager = new RespawnManager(this, ServerSettings.UseRespawnShuttle && !isOutpost ? selectedShuttle : null); } if (campaign != null) { campaign.CargoManager.CreatePurchasedItems(); - campaign.SendCrewState(); + //midround-joining clients need to be informed of pending/new hires at outposts + if (isOutpost) { campaign.SendCrewState(); } } Level.Loaded?.SpawnNPCs(); @@ -2371,6 +2488,8 @@ namespace Barotrauma.Networking } //always allow the server owner to spectate even if it's disallowed in server settings teamClients.RemoveAll(c => c.Connection == OwnerConnection && c.SpectateOnly); + // Clients with last character permanently dead spectate regardless of server settings + teamClients.RemoveAll(c => c.CharacterInfo != null && c.CharacterInfo.PermanentlyDead); //if (!teamClients.Any() && n > 0) { continue; } @@ -2439,6 +2558,7 @@ namespace Barotrauma.Networking wp.Submarine == Level.Loaded.StartOutpost && wp.CurrentHull?.OutpostModuleTags != null && wp.CurrentHull.OutpostModuleTags.Contains("airlock".ToIdentifier())); + while (spawnWaypoints.Count > characterInfos.Count) { spawnWaypoints.RemoveAt(Rand.Int(spawnWaypoints.Count)); @@ -2486,13 +2606,17 @@ namespace Barotrauma.Networking characterData.ApplyWalletData(spawnedCharacter); spawnedCharacter.GiveIdCardTags(mainSubWaypoints[i]); spawnedCharacter.LoadTalents(); - characterData.HasSpawned = true; } if (GameMain.GameSession?.GameMode is MultiPlayerCampaign mpCampaign && spawnedCharacter.Info != null) { spawnedCharacter.Info.SetExperience(Math.Max(spawnedCharacter.Info.ExperiencePoints, mpCampaign.GetSavedExperiencePoints(teamClients[i]))); mpCampaign.ClearSavedExperiencePoints(teamClients[i]); + + if (spawnedCharacter.Info.LastRewardDistribution.TryUnwrap(out int salary)) + { + spawnedCharacter.Wallet.SetRewardDistribution(salary); + } } spawnedCharacter.SetOwnerClient(teamClients[i]); @@ -2584,11 +2708,12 @@ namespace Barotrauma.Networking msg.WriteInt32(seed); msg.WriteIdentifier(gameSession.GameMode.Preset.Identifier); bool missionAllowRespawn = GameMain.GameSession.GameMode is not MissionMode missionMode || !missionMode.Missions.Any(m => !m.AllowRespawn); - msg.WriteBoolean(ServerSettings.AllowRespawn && missionAllowRespawn); + msg.WriteBoolean(ServerSettings.RespawnMode != RespawnMode.BetweenRounds && missionAllowRespawn); msg.WriteBoolean(ServerSettings.AllowDisguises); msg.WriteBoolean(ServerSettings.AllowRewiring); msg.WriteBoolean(ServerSettings.AllowImmediateItemDelivery); msg.WriteBoolean(ServerSettings.AllowFriendlyFire); + msg.WriteBoolean(ServerSettings.AllowDragAndDropGive); msg.WriteBoolean(ServerSettings.LockAllDefaultWires); msg.WriteBoolean(ServerSettings.AllowLinkingWifiToChat); msg.WriteInt32(ServerSettings.MaximumMoneyTransferRequest); @@ -2696,7 +2821,7 @@ namespace Barotrauma.Networking GameMain.GameSession.CrewManager?.ServerWriteActiveOrders(msg); } - public void EndGame(CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None, bool wasSaved = false) + public void EndGame(CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None, bool wasSaved = false, IEnumerable missions = null) { if (GameStarted) { @@ -2712,14 +2837,14 @@ namespace Barotrauma.Networking } string endMessage = TextManager.FormatServerMessage("RoundSummaryRoundHasEnded"); - List missions = GameMain.GameSession.Missions.ToList(); + missions ??= GameMain.GameSession.Missions.ToList(); if (GameMain.GameSession is { IsRunning: true }) { GameMain.GameSession.EndRound(endMessage); } TraitorManager.TraitorResults? traitorResults = traitorManager?.GetEndResults() ?? null; - endRoundTimer = 0.0f; + EndRoundTimer = 0.0f; if (ServerSettings.AutoRestart) { @@ -2755,7 +2880,7 @@ namespace Barotrauma.Networking msg.WriteByte((byte)transitionType); msg.WriteBoolean(wasSaved); msg.WriteString(endMessage); - msg.WriteByte((byte)missions.Count); + msg.WriteByte((byte)missions.Count()); foreach (Mission mission in missions) { msg.WriteBoolean(mission.Completed); @@ -2799,6 +2924,12 @@ namespace Barotrauma.Networking { logMsg = message.TextWithSender; } + + if (message.Sender is Character sender) + { + sender.TextChatVolume = 1f; + } + Log(logMsg, ServerLog.MessageType.Chat); } @@ -3318,24 +3449,24 @@ namespace Barotrauma.Networking public void SendOrderChatMessage(OrderChatMessage message) { - if (message.Sender == null || message.Sender.SpeechImpediment >= 100.0f) { return; } + if (message.SenderCharacter == null || message.SenderCharacter.SpeechImpediment >= 100.0f) { return; } //check which clients can receive the message and apply distance effects foreach (Client client in ConnectedClients) { - if (message.Sender != null && client.Character != null && !client.Character.IsDead) + if (message.SenderCharacter != null && client.Character != null && !client.Character.IsDead) { //too far to hear the msg -> don't send - if (!client.Character.CanHearCharacter(message.Sender)) { continue; } + if (!client.Character.CanHearCharacter(message.SenderCharacter)) { continue; } } SendDirectChatMessage(new OrderChatMessage(message.Order, message.Text, message.TargetCharacter, message.Sender, isNewOrder: message.IsNewOrder), client); } if (!string.IsNullOrWhiteSpace(message.Text)) { AddChatMessage(new OrderChatMessage(message.Order, message.Text, message.TargetCharacter, message.Sender, isNewOrder: message.IsNewOrder)); - if (ChatMessage.CanUseRadio(message.Sender, out var senderRadio)) + if (ChatMessage.CanUseRadio(message.SenderCharacter, out var senderRadio)) { //send to chat-linked wifi components - Signal s = new Signal(message.Text, sender: message.Sender, source: senderRadio.Item); + Signal s = new Signal(message.Text, sender: message.SenderCharacter, source: senderRadio.Item); senderRadio.TransmitSignal(s, sentFromChat: true); } } @@ -3679,6 +3810,14 @@ namespace Barotrauma.Networking } } + // If a CharacterInfo for this Client already exists on the server, make sure it is used, and prevent the Client from replacing it + var existingCampaignData = (GameMain.GameSession?.Campaign as MultiPlayerCampaign)?.GetClientCharacterData(sender); + if (existingCampaignData != null) + { + sender.CharacterInfo = existingCampaignData.CharacterInfo; + return; + } + sender.CharacterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, newName); sender.CharacterInfo.RecreateHead( @@ -3841,7 +3980,7 @@ namespace Barotrauma.Networking foreach (Client c in unassigned) { //find all jobs that are still available - var remainingJobs = jobList.FindAll(jp => assignedClientCount[jp] < jp.MaxNumber && c.Karma >= jp.MinKarma); + var remainingJobs = jobList.FindAll(jp => !jp.HiddenJob && assignedClientCount[jp] < jp.MaxNumber && c.Karma >= jp.MinKarma); //all jobs taken, give a random job if (remainingJobs.Count == 0) @@ -3884,9 +4023,15 @@ namespace Barotrauma.Networking public void AssignBotJobs(List bots, CharacterTeamType teamID) { + //shuffle first so the parts where we go through the prefabs + //and find ones there's too few of don't always pick the same job + List shuffledPrefabs = JobPrefab.Prefabs.Where(static jp => !jp.HiddenJob).ToList(); + shuffledPrefabs.Shuffle(); + Dictionary assignedPlayerCount = new Dictionary(); - foreach (JobPrefab jp in JobPrefab.Prefabs) + foreach (JobPrefab jp in shuffledPrefabs) { + if (jp.HiddenJob) { continue; } assignedPlayerCount.Add(jp, 0); } @@ -3905,53 +4050,55 @@ namespace Barotrauma.Networking } List unassignedBots = new List(bots); - - List spawnPoints = WayPoint.WayPointList.FindAll(wp => - wp.SpawnType == SpawnType.Human && - wp.Submarine != null && wp.Submarine.TeamID == teamID) - .OrderBy(sp => Rand.Int(int.MaxValue)) - .OrderBy(sp => sp.AssignedJob == null ? 0 : 1) - .ToList(); - - bool canAssign = false; - do + while (unassignedBots.Count > 0) { - canAssign = false; - foreach (WayPoint spawnPoint in spawnPoints) + //if there's any jobs left that must be included in the crew, assign those + var jobsBelowMinNumber = shuffledPrefabs.Where(jp => assignedPlayerCount[jp] < jp.MinNumber); + if (jobsBelowMinNumber.Any()) { - if (unassignedBots.Count == 0) { break; } - - JobPrefab jobPrefab = spawnPoint.AssignedJob ?? JobPrefab.Prefabs.GetRandomUnsynced(); - if (assignedPlayerCount[jobPrefab] >= jobPrefab.MaxNumber) { continue; } - - var variant = Rand.Range(0, jobPrefab.Variants, Rand.RandSync.ServerAndClient); - unassignedBots[0].Job = new Job(jobPrefab, Rand.RandSync.ServerAndClient, variant); - assignedPlayerCount[jobPrefab]++; - unassignedBots.Remove(unassignedBots[0]); - canAssign = true; + AssignJob(unassignedBots[0], jobsBelowMinNumber.GetRandomUnsynced()); } - } while (unassignedBots.Count > 0 && canAssign); + else + { + //if there's any jobs left that are below the normal number of bots initially in the crew, assign those + var jobsBelowInitialCount = shuffledPrefabs.Where(jp => assignedPlayerCount[jp] < jp.InitialCount); + if (jobsBelowInitialCount.Any()) + { + AssignJob(unassignedBots[0], jobsBelowInitialCount.GetRandomUnsynced()); + } + else + { + //no "must-have-jobs" left, break and start assigning randomly + break; + } + } + } - //find a suitable job for the rest of the bots - foreach (CharacterInfo c in unassignedBots) + foreach (CharacterInfo c in unassignedBots.ToList()) { //find all jobs that are still available - var remainingJobs = JobPrefab.Prefabs.Where(jp => assignedPlayerCount[jp] < jp.MaxNumber); + var remainingJobs = shuffledPrefabs.Where(jp => assignedPlayerCount[jp] < jp.MaxNumber); //all jobs taken, give a random job if (remainingJobs.None()) { DebugConsole.ThrowError("Failed to assign a suitable job for bot \"" + c.Name + "\" (all jobs already have the maximum numbers of players). Assigning a random job..."); - c.Job = Job.Random(Rand.RandSync.ServerAndClient); - assignedPlayerCount[c.Job.Prefab]++; + AssignJob(c, shuffledPrefabs.GetRandomUnsynced()); } - else //some jobs still left, choose one of them by random - { - var job = remainingJobs.GetRandomUnsynced(); - var variant = Rand.Range(0, job.Variants); - c.Job = new Job(job, Rand.RandSync.Unsynced, variant); - assignedPlayerCount[c.Job.Prefab]++; + else + { + //some jobs still left, choose one of them by random (preferring ones there's the least of in the crew) + var selectedJob = remainingJobs.GetRandomByWeight(jp => 1.0f / Math.Max(assignedPlayerCount[jp], 0.01f), Rand.RandSync.Unsynced); + AssignJob(c, selectedJob); } } + + void AssignJob(CharacterInfo bot, JobPrefab job) + { + int variant = Rand.Range(0, job.Variants); + bot.Job = new Job(job, Rand.RandSync.Unsynced, variant); + assignedPlayerCount[bot.Job.Prefab]++; + unassignedBots.Remove(bot); + } } private Client FindClientWithJobPreference(List clients, JobPrefab job, bool forceAssign = false) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs index ceb81bd03..05cbbc7db 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs @@ -458,15 +458,35 @@ namespace Barotrauma.Networking RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.AuthenticationFailed)); } + if (authenticators is null && + GameMain.Server.ServerSettings.RequireAuthentication) + { + DebugConsole.NewMessage( + "The server is configured to require authentication from clients, but there are no authenticators available. " + + $"If you're for example trying to host a server in a local network without being connected to Steam or Epic Online Services, please set {nameof(GameMain.Server.ServerSettings.RequireAuthentication)} to false in the server settings.", + Microsoft.Xna.Framework.Color.Yellow); + } + if (authenticators is null || !packet.AuthTicket.TryUnwrap(out var authTicket) || !authenticators.TryGetValue(authTicket.Kind, out var authenticator)) - { + { #if DEBUG - DebugConsole.NewMessage($"Debug server accepts unauthenticated connections", Microsoft.Xna.Framework.Color.Yellow); - acceptClient(new AccountInfo(packet.AccountId)); + DebugConsole.NewMessage("Debug server accepts unauthenticated connections", Microsoft.Xna.Framework.Color.Yellow); + acceptClient(new AccountInfo(new UnauthenticatedAccountId(packet.Name))); #else - rejectClient(); + if (GameMain.Server.ServerSettings.RequireAuthentication) + { + DebugConsole.NewMessage( + "A client attempted to join without an authentication ticket, but the server is configured to require authentication. " + + $"If you're for example trying to host a server in a local network without being connected to Steam or Epic Online Services, please set {nameof(GameMain.Server.ServerSettings.RequireAuthentication)} to false in the server settings.", + Microsoft.Xna.Framework.Color.Yellow); + rejectClient(); + } + else + { + acceptClient(new AccountInfo(new UnauthenticatedAccountId(packet.Name))); + } #endif return; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index 2cae1d3ef..f0b0e7a80 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -15,6 +15,8 @@ namespace Barotrauma.Networking private int pendingRespawnCount, requiredRespawnCount; private int prevPendingRespawnCount, prevRequiredRespawnCount; + public bool IsShuttleInsideLevel => RespawnShuttle != null && RespawnShuttle.WorldPosition.Y < Level.Loaded.Size.Y; + private IEnumerable GetClientsToRespawn() { MultiPlayerCampaign campaign = GameMain.GameSession.GameMode as MultiPlayerCampaign; @@ -24,18 +26,27 @@ 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 respawn if the client already has a character (they'll regain control once they're in sync) var matchingData = campaign?.GetClientCharacterData(c); + + //don't allow respawn if the client already has a character (they'll regain control once they're in sync) if (matchingData != null && matchingData.HasSpawned && Character.CharacterList.Any(c => c.Info == matchingData.CharacterInfo && !c.IsDead)) { continue; } - - if (UseRespawnPrompt) + + // Respawning might also be needed in permadeath mode for disconnected characters, but never for permanently dead ones + if (GameMain.NetworkMember?.ServerSettings is { RespawnMode: RespawnMode.Permadeath } && + (matchingData?.CharacterInfo is { PermanentlyDead: true } || c.Character is { IsDead: true })) + { + continue; + } + + if (campaign != null) { if (matchingData != null && matchingData.HasSpawned) { + //in the campaign mode, wait for the client to choose whether they want to spawn if (!c.WaitForNextRoundRespawn.HasValue || c.WaitForNextRoundRespawn.Value) { continue; } } } @@ -44,9 +55,9 @@ namespace Barotrauma.Networking } } - private static bool IsRespawnPromptPendingForClient(Client c) + private static bool IsRespawnDecisionPendingForClient(Client c) { - if (!UseRespawnPrompt || !(GameMain.GameSession.GameMode is MultiPlayerCampaign campaign)) { return false; } + if (Level.Loaded == null || GameMain.GameSession.GameMode is not MultiPlayerCampaign campaign) { return false; } if (!c.InGame) { return false; } if (c.SpectateOnly && (GameMain.Server.ServerSettings.AllowSpectating || GameMain.Server.OwnerConnection == c.Connection)) { return false; } @@ -55,7 +66,9 @@ namespace Barotrauma.Networking var matchingData = campaign.GetClientCharacterData(c); if (matchingData != null && matchingData.HasSpawned) { - if (Character.CharacterList.Any(c => c.Info == matchingData.CharacterInfo && !c.IsDead)) + if (Character.CharacterList.Any(c => + c.Info == matchingData.CharacterInfo && + (!c.IsDead || c.CauseOfDeath is { Type: CauseOfDeathType.Disconnected }))) { return false; } @@ -180,22 +193,27 @@ namespace Barotrauma.Networking shuttleSteering.TargetVelocity = Vector2.Zero; } - GameServer.Log("Dispatching the respawn shuttle.", ServerLog.MessageType.Spawning); - Vector2 spawnPos = FindSpawnPos(); - RespawnCharacters(spawnPos); - - CoroutineManager.StopCoroutines("forcepos"); - if (spawnPos.Y > Level.Loaded.Size.Y) + RespawnCharacters(spawnPos, out bool anyCharacterSpawnedInShuttle); + if (anyCharacterSpawnedInShuttle) { - CoroutineManager.StartCoroutine(ForceShuttleToPos(Level.Loaded.StartPosition - Vector2.UnitY * Level.ShaftHeight, 100.0f), "forcepos"); + GameServer.Log("Dispatching the respawn shuttle.", ServerLog.MessageType.Spawning); + CoroutineManager.StopCoroutines("forcepos"); + if (spawnPos.Y > Level.Loaded.Size.Y) + { + CoroutineManager.StartCoroutine(ForceShuttleToPos(Level.Loaded.StartPosition - Vector2.UnitY * Level.ShaftHeight, 100.0f), "forcepos"); + } + else + { + RespawnShuttle.SetPosition(spawnPos); + RespawnShuttle.Velocity = Vector2.Zero; + RespawnShuttle.NeutralizeBallast(); + RespawnShuttle.EnableMaintainPosition(); + } } else { - RespawnShuttle.SetPosition(spawnPos); - RespawnShuttle.Velocity = Vector2.Zero; - RespawnShuttle.NeutralizeBallast(); - RespawnShuttle.EnableMaintainPosition(); + GameServer.Log("Respawning everyone in main sub.", ServerLog.MessageType.Spawning); } } else @@ -204,7 +222,7 @@ namespace Barotrauma.Networking GameServer.Log("Respawning everyone in main sub.", ServerLog.MessageType.Spawning); GameMain.Server.CreateEntityEvent(this); - RespawnCharacters(null); + RespawnCharacters(shuttlePos: null, out _); } } @@ -244,7 +262,7 @@ namespace Barotrauma.Networking } } - if (RespawnShuttle.WorldPosition.Y > Level.Loaded.Size.Y || DateTime.Now > despawnTime) + if (!IsShuttleInsideLevel || DateTime.Now > despawnTime) { CoroutineManager.StopCoroutines("forcepos"); @@ -289,7 +307,10 @@ namespace Barotrauma.Networking if (DateTime.Now > ReturnTime) { - GameServer.Log("The respawn shuttle is leaving.", ServerLog.MessageType.ServerMessage); + if (IsShuttleInsideLevel) + { + GameServer.Log("The respawn shuttle is leaving.", ServerLog.MessageType.ServerMessage); + } CurrentState = State.Returning; GameMain.Server.CreateEntityEvent(this); @@ -311,8 +332,8 @@ namespace Barotrauma.Networking } return shuttleEmptyTimer > 1.0f; } - - partial void RespawnCharactersProjSpecific(Vector2? shuttlePos) + + private void RespawnCharacters(Vector2? shuttlePos, out bool anyCharacterSpawnedInShuttle) { respawnedCharacters.Clear(); @@ -337,7 +358,7 @@ namespace Barotrauma.Networking //all characters are in Team 1 in game modes/missions with only one team. //if at some point we add a game mode with multiple teams where respawning is possible, this needs to be reworked c.TeamID = CharacterTeamType.Team1; - if (c.CharacterInfo == null) { c.CharacterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, c.Name); } + c.CharacterInfo ??= new CharacterInfo(CharacterPrefab.HumanSpeciesName, c.Name); } List characterInfos = clients.Select(c => c.CharacterInfo).ToList(); @@ -378,6 +399,8 @@ namespace Barotrauma.Networking var cargoSp = WayPoint.WayPointList.Find(wp => wp.Submarine == respawnSub && wp.SpawnType == SpawnType.Cargo); + anyCharacterSpawnedInShuttle = false; + for (int i = 0; i < characterInfos.Count; i++) { bool bot = i >= clients.Count; @@ -412,10 +435,19 @@ namespace Barotrauma.Networking } } + if (!forceSpawnInMainSub) + { + anyCharacterSpawnedInShuttle = true; + } + var character = Character.Create(characterInfos[i], (forceSpawnInMainSub ? mainSubSpawnPoints[i] : shuttleSpawnPoints[i]).WorldPosition, characterInfos[i].Name, isRemotePlayer: !bot, hasAi: bot); characterCampaignData?.ApplyWalletData(character); character.TeamID = CharacterTeamType.Team1; character.LoadTalents(); + if (characterInfos[i].LastRewardDistribution.TryUnwrap(out int salary)) + { + character.Wallet.SetRewardDistribution(salary); + } respawnedCharacters.Add(character); @@ -446,7 +478,7 @@ namespace Barotrauma.Networking $"Respawning {GameServer.ClientLogName(clients[i])} ({clients[i].Connection.Endpoint}) as {characterInfos[i].Job.Name}", ServerLog.MessageType.Spawning); } - if (RespawnShuttle != null) + if (RespawnShuttle != null && anyCharacterSpawnedInShuttle) { List newRespawnItems = new List(); Vector2 pos = cargoSp?.Position ?? character.Position; @@ -567,9 +599,7 @@ namespace Barotrauma.Networking foreach (Skill skill in characterInfo.Job.GetSkills()) { - var skillPrefab = characterInfo.Job.Prefab.Skills.Find(s => skill.Identifier == s.Identifier); - if (skillPrefab == null || skill.Level < skillPrefab.LevelRange.End) { continue; } - skill.Level = MathHelper.Lerp(skill.Level, skillPrefab.LevelRange.End, skillLossPercentage / 100.0f); + skill.Level = GetReducedSkill(characterInfo, skill, skillLossPercentage); } } @@ -585,14 +615,10 @@ namespace Barotrauma.Networking msg.WriteSingle((float)(ReturnTime - DateTime.Now).TotalSeconds); break; case State.Waiting: - MultiPlayerCampaign campaign = GameMain.GameSession.GameMode as MultiPlayerCampaign; - var matchingData = campaign?.GetClientCharacterData(c); - bool forceSpawnInMainSub = matchingData != null && !matchingData.HasSpawned; msg.WriteUInt16((ushort)pendingRespawnCount); msg.WriteUInt16((ushort)requiredRespawnCount); - msg.WriteBoolean(IsRespawnPromptPendingForClient(c)); + msg.WriteBoolean(IsRespawnDecisionPendingForClient(c)); msg.WriteBoolean(RespawnCountdownStarted); - msg.WriteBoolean(forceSpawnInMainSub); msg.WriteSingle((float)(RespawnTime - DateTime.Now).TotalSeconds); break; case State.Returning: diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServer.cs index 7a3e9a43c..ee068ffb8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServer.cs @@ -123,5 +123,21 @@ namespace Barotrauma.Networking return garbleAmount < 1.0f; } } + + public static void Read(IReadMessage inc, Client connectedClient) + { + var queue = connectedClient.VoipQueue; + if (queue.Read(inc, discardData: false)) + { + connectedClient.VoipServerDecoder.OnNewVoiceReceived(); + } + +#if DEBUG + var msg = new WriteOnlyMessage().WithHeader(ServerPacketHeader.VOICE_AMPLITUDE_DEBUG); + msg.WriteRangedSingle(connectedClient.VoipServerDecoder.Amplitude, min: 0, max: 1, bitCount: 8); + + GameMain.Server?.ServerPeer?.Send(msg, connectedClient.Connection, DeliveryMethod.Unreliable); +#endif + } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServerDecoder.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServerDecoder.cs new file mode 100644 index 000000000..092e11456 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServerDecoder.cs @@ -0,0 +1,230 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using Barotrauma.IO; +using System.Text; +using Barotrauma.Networking; +using Concentus.Structs; + +namespace Barotrauma +{ + internal sealed class VoipServerDecoder + { + private readonly OpusDecoder decoder; + private readonly VoipQueue queue; + private int lastRetrievedBufferID; + + public float Amplitude { get; private set; } + + private readonly Client ownerClient; + + public VoipServerDecoder(VoipQueue q, Client owner) + { + ownerClient = owner; + decoder = VoipConfig.CreateDecoder(); + queue = q; + lastRetrievedBufferID = q.LatestBufferID; + } + + private static bool debugVoip; + /// + /// When set to true the server will write VOIP into an audio file for debugging purposes. + /// Useful if you're modifying this part of the code and want to be able to hear what the server "hears" + /// + public static bool DebugVoip + { + get => debugVoip; + set + { +#if !DEBUG + debugVoip = false; + if (value) + { + DebugConsole.ThrowError("DebugVoip is only available in debug builds of the game"); + } +#else + + debugVoip = value; + + if (!value) + { + if (GameMain.Server is null) { return; } + foreach (var c in GameMain.Server.ConnectedClients) + { + c.VoipServerDecoder.ClearStoredDebugSamples(); + } + } +#endif + } + } + + private readonly List debugStoredSamples = new(); + + private float debugWriteTimerBacking; + private float DebugWriteTimer + { + get => debugWriteTimerBacking; + set => debugWriteTimerBacking = Math.Clamp(value, min: 0, max: DebugWriteTimeout); + } + + private bool shouldWriteDebugFile; + private const float DebugWriteTimeout = 3f; // 3 seconds of no data before writing to file + + public void OnNewVoiceReceived() + { + float amplitude = 0.0f; + for (int i = lastRetrievedBufferID + 1; i <= queue.LatestBufferID; i++) + { + queue.RetrieveBuffer(i, out int compressedSize, out byte[] compressedBuffer); + if (compressedSize <= 0) { continue; } + + short[] buffer = new short[VoipConfig.BUFFER_SIZE]; + decoder.Decode(compressedBuffer, 0, compressedSize, buffer, 0, VoipConfig.BUFFER_SIZE); + amplitude = Math.Max(amplitude, GetAmplitude(buffer)); + lastRetrievedBufferID = i; + + if (!DebugVoip) { continue; } + lock (debugStoredSamples) { debugStoredSamples.Add(buffer); } + } + + Amplitude = amplitude; + + if (DebugVoip) + { + DebugWriteTimer = DebugWriteTimeout; + } + } + + public void DebugUpdate(float deltaTime) + { + if (!DebugVoip) { return; } + + if (DebugWriteTimer > 0) + { + DebugWriteTimer -= deltaTime; + if (DebugWriteTimer <= 0) + { + shouldWriteDebugFile = true; + } + return; + } + + if (!shouldWriteDebugFile) { return; } + + lock (debugStoredSamples) + { +#if DEBUG + WriteSamplesToWaveFile(debugStoredSamples, + filename: $"voip_{ownerClient.Name}_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}.wav", + sampleRate: VoipConfig.FREQUENCY, + channels: 1); +#endif + + debugStoredSamples.Clear(); + shouldWriteDebugFile = false; + } + } + + private static float GetAmplitude(short[] values) + { + float max = 0; + foreach (short v in values) + { + max = Math.Max(max, ToolBox.ShortAudioSampleToFloat(v)); + } + return max; + } + + /// + /// Writes the given audio samples to a wave file. + /// + /// The audio samples to write. + /// The name of the wave file to create. + /// The sample rate of the audio. + /// The number of channels in the audio. + private static void WriteSamplesToWaveFile(IReadOnlyList samples, string filename, int sampleRate, short channels) + { + if (!samples.Any()) { return; } + + var path = Path.Combine(Path.GetFullPath("AudioDebug")); + if (!Directory.Exists(path)) + { + var dir = Directory.CreateDirectory(path); + if (dir is not { Exists: true }) { return; } + } + + using var outFile = File.Create(Path.Combine(path, ToolBox.RemoveInvalidFileNameChars(filename))); + + if (outFile is null) + { + DebugConsole.ThrowError("Failed to create audio debug file"); + return; + } + + // wave file format: https://docs.fileformat.com/audio/wav/ + using var writer = new System.IO.BinaryWriter(outFile); + + const short pcmFormat = 1; // PCM + const short bitsPerSample = 16; // 16 bits in a short + int byteRate = sampleRate * bitsPerSample * channels / 8; + short blockAlign = (short)(bitsPerSample * channels / 8); + + // === FILE INFO === // + writer.Write(Encoding.ASCII.GetBytes("RIFF")); + long sizePos = outFile.Position; + writer.Write(0); // size of file, will be written later + + writer.Write(Encoding.ASCII.GetBytes("WAVE")); + writer.Write(Encoding.ASCII.GetBytes("fmt ")); // trailing space is required, not a typo + + writer.Write(16); // length of format header + + // === AUDIO FORMAT === // + writer.Write(pcmFormat); + writer.Write(channels); + writer.Write(sampleRate); + writer.Write(byteRate); + writer.Write(blockAlign); + writer.Write(bitsPerSample); + + // === SAMPLE DATA === // + writer.Write(Encoding.ASCII.GetBytes("data")); + writer.Flush(); + + long dataPos = outFile.Position; + writer.Write(0); // temporary data size + + foreach (var sample in samples) + { + foreach (var s in sample) + { + writer.Write(s); + } + } + + writer.Flush(); + + // write the file size + writer.Seek((int)sizePos, System.IO.SeekOrigin.Begin); + writer.Write((int)(outFile.Length - 8)); // spec says to subtract 8 bytes from the file size + + // write the data size + writer.Seek((int)dataPos, System.IO.SeekOrigin.Begin); + writer.Write((int)(outFile.Length - dataPos)); // size of the data only + + writer.Flush(); + } + + private void ClearStoredDebugSamples() + { + lock (debugStoredSamples) + { + debugStoredSamples.Clear(); + } + DebugWriteTimer = 0; + shouldWriteDebugFile = false; + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs index bfcefab09..0288536df 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs @@ -68,13 +68,13 @@ namespace Barotrauma.Steam foreach (var contentPackage in contentPackages) { Steamworks.SteamServer.SetKey( - $"contentpackage{index}", + $"contentpackage{index}", new ServerListContentPackageInfo(contentPackage).ToString()); index++; } return; } - + Steamworks.SteamServer.SetKey(key.Value.ToLowerInvariant(), value.ToString()); } diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 6b8b6d61c..401e3d30a 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.4.6.0 + 1.5.7.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer @@ -70,6 +70,7 @@ + @@ -77,6 +78,7 @@ + diff --git a/Barotrauma/BarotraumaShared/Data/permissionpresets.xml b/Barotrauma/BarotraumaShared/Data/permissionpresets.xml index e42ed985a..038059a5d 100644 --- a/Barotrauma/BarotraumaShared/Data/permissionpresets.xml +++ b/Barotrauma/BarotraumaShared/Data/permissionpresets.xml @@ -33,6 +33,7 @@ + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs index 76a6d12de..852d07842 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs @@ -184,6 +184,20 @@ namespace Barotrauma } } + /// + /// Is some condition met (e.g. entity null, indetectable, outside level) that prevents anyone from detecting the target? + /// + public bool ShouldBeIgnored() + { + if (InDetectable) { return true; } + if (Entity == null) { return true; } + if (Level.Loaded != null && WorldPosition.Y > Level.Loaded.Size.Y) + { + return true; + } + return false; + } + public AITarget(Entity e, XElement element) : this(e) { SightRange = element.GetAttributeFloat("sightrange", 0.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 0c94adf1f..37ef8f1b9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -216,7 +216,7 @@ namespace Barotrauma private bool IsAttackingOwner(Character other) => PetBehavior != null && PetBehavior.Owner != null && - !other.IsUnconscious && !other.IsArrested && + !other.IsUnconscious && !other.IsHandcuffed && other.AIController is HumanAIController humanAI && humanAI.ObjectiveManager.CurrentObjective is AIObjectiveCombat combat && combat.Enemy != null && combat.Enemy == PetBehavior.Owner; @@ -2694,13 +2694,8 @@ namespace Barotrauma float maxModifier = 5; foreach (AITarget aiTarget in AITarget.List) { - if (aiTarget.InDetectable) { continue; } - if (aiTarget.Entity == null) { continue; } + if (aiTarget.ShouldBeIgnored()) { continue; } if (ignoredTargets.Contains(aiTarget)) { continue; } - if (Level.Loaded != null && aiTarget.WorldPosition.Y > Level.Loaded.Size.Y) - { - continue; - } if (aiTarget.Type == AITarget.TargetType.HumanOnly) { continue; } if (!TargetOutposts) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 706a8fd0a..8dd5113ae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -67,6 +67,12 @@ namespace Barotrauma private readonly float reportProblemsInterval = 1.0f; private float reportProblemsTimer; + /// + /// Affects how far the character can hear sounds created by AI targets with the tag ProvocativeToHumanAI. + /// Used as a multiplier on the sound range of the target, e.g. a value of 0.5 would mean a target with a sound range of 1000 would need to be within 500 units for this character to hear it. + /// Only affects the "fight intruders" objective, which makes the character go and inspect noises. + /// + public float Hearing { get; set; } = 1.0f; /// /// How far other characters can hear reports done by this character (e.g. reports for fires, intruders). Defaults to infinity. @@ -339,39 +345,8 @@ namespace Barotrauma enemyCheckTimer -= deltaTime; if (enemyCheckTimer < 0) { + CheckEnemies(); 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; } - if (EnemyAIController.IsLatchedToSomeoneElse(c, Character)) { continue; } - var head = Character.AnimController.GetLimb(LimbType.Head); - if (head == null) { continue; } - float rotation = head.body.TransformedRotation; - Vector2 forward = VectorExtensions.Forward(rotation); - float angle = MathHelper.ToDegrees(VectorExtensions.Angle(toTarget, forward)); - if (angle > 70) { continue; } - if (!Character.CanSeeTarget(c)) { continue; } - if (dist < closestDistance || closestEnemy == null) - { - closestEnemy = c; - closestDistance = dist; - } - } - if (closestEnemy != null) - { - AddCombatObjective(AIObjectiveCombat.CombatMode.Defensive, closestEnemy); - } - } } } bool useInsideSteering = !isOutside || isBlocked || HasValidPath() || IsCloseEnoughToTarget(steeringBuffer); @@ -586,6 +561,42 @@ namespace Barotrauma ShipCommandManager?.Update(deltaTime); } + private void CheckEnemies() + { + //already in combat, no need to check + if (objectiveManager.IsCurrentObjective()) { return; } + + 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; } + if (EnemyAIController.IsLatchedToSomeoneElse(c, Character)) { continue; } + var head = Character.AnimController.GetLimb(LimbType.Head); + if (head == null) { continue; } + float rotation = head.body.TransformedRotation; + Vector2 forward = VectorExtensions.Forward(rotation); + float angle = MathHelper.ToDegrees(VectorExtensions.Angle(toTarget, forward)); + if (angle > 70) { continue; } + if (!Character.CanSeeTarget(c)) { continue; } + if (dist < closestDistance || closestEnemy == null) + { + closestEnemy = c; + closestDistance = dist; + } + } + if (closestEnemy != null) + { + AddCombatObjective(AIObjectiveCombat.CombatMode.Defensive, closestEnemy); + } + } + private void UnequipUnnecessaryItems() { if (Character.LockHands) { return; } @@ -632,7 +643,9 @@ namespace Barotrauma isCurrentObjectiveFindSafety || Character.AnimController.InWater || Character.AnimController.HeadInWater || + Character.IsClimbing || Character.Submarine == null || + Character.Submarine.Info.HasTag(SubmarineTag.Shuttle) || (!Character.IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID) && !Character.IsEscorted) || ObjectiveManager.CurrentOrders.Any(o => o.Objective.KeepDivingGearOnAlsoWhenInactive) || ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn) || @@ -845,8 +858,9 @@ namespace Barotrauma { // In the campaign mode, undocking happens after leaving the outpost, so we can't use that. campaign.BeforeLevelLoading += Relocate; + campaign.OnSaveAndQuit += Relocate; + campaign.ItemsRelocatedToMainSub = true; } - campaign.ItemsRelocatedToMainSub = true; #if CLIENT HintManager.OnItemMarkedForRelocation(); #endif @@ -1011,6 +1025,7 @@ namespace Barotrauma { Order newOrder = null; Hull targetHull = null; + // for now, escorted characters use the report system to get targets but do not speak. escort-character specific dialogue could be implemented bool speak = Character.SpeechImpediment < 100 && !Character.IsEscorted; if (Character.CurrentHull != null) @@ -1024,7 +1039,7 @@ namespace Barotrauma if (target.CurrentHull != hull || !target.Enabled) { continue; } if (AIObjectiveFightIntruders.IsValidTarget(target, Character, false)) { - if (!target.IsArrested && AddTargets(Character, target) && newOrder == null) + if (!target.IsHandcuffed && AddTargets(Character, target) && newOrder == null) { var orderPrefab = OrderPrefab.Prefabs["reportintruders"]; newOrder = new Order(orderPrefab, hull, null, orderGiver: Character); @@ -1436,10 +1451,7 @@ namespace Barotrauma { return AIObjectiveCombat.CombatMode.Offensive; } - return - humanAI.ObjectiveManager.IsCurrentOrder() || - humanAI.ObjectiveManager.Objectives.Any(o => o is AIObjectiveFightIntruders) ? - AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Defensive; + return humanAI.ObjectiveManager.HasObjectiveOrOrder() ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Defensive; } else { @@ -1479,28 +1491,36 @@ namespace Barotrauma { // The guards don't react to player's aggressions when there's an instigator around isAttackerFightingEnemy = true; - return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : (instigator.CombatAction != null ? instigator.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat); + return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : instigator.CombatAction?.WitnessReaction ?? AIObjectiveCombat.CombatMode.Retreat; } if (attacker.TeamID == CharacterTeamType.FriendlyNPC && !eitherIsMentallyUnstable) { if (c.IsSecurity) { - return attacker.CombatAction != null ? attacker.CombatAction.GuardReaction : AIObjectiveCombat.CombatMode.Offensive; + return attacker.CombatAction?.GuardReaction ?? AIObjectiveCombat.CombatMode.Offensive; } else { - return attacker.CombatAction != null ? attacker.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat; + return attacker.CombatAction?.WitnessReaction ?? AIObjectiveCombat.CombatMode.Retreat; } } else { - if (humanAI.ObjectiveManager.GetLastActiveObjective()?.Enemy == attacker) + if (humanAI.ObjectiveManager.GetLastActiveObjective() is AIObjectiveCombat currentCombatObjective && currentCombatObjective.Enemy == attacker) { // Already targeting the attacker -> treat as a more serious threat. cumulativeDamage *= 2; + currentCombatObjective.AllowHoldFire = false; + c.IsCriminal = true; + } + if (c.IsCriminal) + { + // Always react if the attacker has been misbehaving earlier. + cumulativeDamage = Math.Max(cumulativeDamage, minorDamageThreshold); } if (cumulativeDamage > majorDamageThreshold) { + c.IsCriminal = true; if (c.IsSecurity) { return AIObjectiveCombat.CombatMode.Offensive; @@ -1544,7 +1564,7 @@ namespace Barotrauma } } - public void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character target, float delay = 0, Func abortCondition = null, Action onAbort = null, Action onCompleted = null, bool allowHoldFire = false) + public void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character target, float delay = 0, Func abortCondition = null, Action onAbort = null, Action onCompleted = null, bool allowHoldFire = false, bool speakWarnings = false) { if (mode == AIObjectiveCombat.CombatMode.None) { return; } if (Character.IsDead || Character.IsIncapacitated || Character.Removed) { return; } @@ -1579,6 +1599,7 @@ namespace Barotrauma HoldPosition = Character.Info?.Job?.Prefab.Identifier == "watchman", AbortCondition = abortCondition, AllowHoldFire = allowHoldFire, + SpeakWarnings = speakWarnings }; if (onAbort != null) { @@ -1777,7 +1798,7 @@ namespace Barotrauma } if (!otherCharacter.CanSeeTarget(character, seeThroughWindows: true)) { continue; } - if (!otherHumanAI.structureDamageAccumulator.ContainsKey(character)) { otherHumanAI.structureDamageAccumulator.Add(character, 0.0f); } + otherHumanAI.structureDamageAccumulator.TryAdd(character, 0.0f); float prevAccumulatedDamage = otherHumanAI.structureDamageAccumulator[character]; otherHumanAI.structureDamageAccumulator[character] += MathHelper.Clamp(damageAmount, -MaxDamagePerFrame, MaxDamagePerFrame); float accumulatedDamage = Math.Max(otherHumanAI.structureDamageAccumulator[character], maxAccumulatedDamage); @@ -1789,27 +1810,36 @@ namespace Barotrauma GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss, Reputation.MaxReputationLossFromWallDamage); } - if (accumulatedDamage <= WarningThreshold) { return; } - - if (accumulatedDamage > WarningThreshold && prevAccumulatedDamage <= WarningThreshold && - !someoneSpoke && !character.IsIncapacitated && character.Stun <= 0.0f) + if (!character.IsCriminal) { - //if the damage is still fairly low, wait and see if the character keeps damaging the walls to the point where we need to intervene - if (accumulatedDamage < ArrestThreshold) + if (accumulatedDamage <= WarningThreshold) { return; } + + if (accumulatedDamage > WarningThreshold && prevAccumulatedDamage <= WarningThreshold && + !someoneSpoke && !character.IsIncapacitated && character.Stun <= 0.0f) { - if (otherHumanAI.ObjectiveManager.IsCurrentObjective()) + //if the damage is still fairly low, wait and see if the character keeps damaging the walls to the point where we need to intervene + if (accumulatedDamage < ArrestThreshold) { - (otherHumanAI.ObjectiveManager.CurrentObjective as AIObjectiveIdle)?.FaceTargetAndWait(character, 5.0f); + if (otherHumanAI.ObjectiveManager.CurrentObjective is AIObjectiveIdle idleObjective) + { + idleObjective.FaceTargetAndWait(character, 5.0f); + } } + otherCharacter.Speak(TextManager.Get("dialogdamagewallswarning").Value, null, Rand.Range(0.5f, 1.0f), "damageoutpostwalls".ToIdentifier(), 10.0f); + someoneSpoke = true; } - otherCharacter.Speak(TextManager.Get("dialogdamagewallswarning").Value, null, Rand.Range(0.5f, 1.0f), "damageoutpostwalls".ToIdentifier(), 10.0f); - someoneSpoke = true; } + // React if we are security - if ((accumulatedDamage > ArrestThreshold && prevAccumulatedDamage <= ArrestThreshold) || + if (character.IsCriminal || + (accumulatedDamage > ArrestThreshold && prevAccumulatedDamage <= ArrestThreshold) || (accumulatedDamage > KillThreshold && prevAccumulatedDamage <= KillThreshold)) { var combatMode = accumulatedDamage > KillThreshold ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Arrest; + if (combatMode == AIObjectiveCombat.CombatMode.Offensive) + { + character.IsCriminal = true; + } if (!TriggerSecurity(otherHumanAI, combatMode)) { // Else call the others @@ -1830,17 +1860,18 @@ namespace Barotrauma if (humanAI == null) { return false; } if (!humanAI.Character.IsSecurity) { return false; } if (humanAI.ObjectiveManager.IsCurrentObjective()) { return false; } - humanAI.AddCombatObjective(combatMode, character, delay: GetReactionTime(), allowHoldFire: true, onCompleted: () => - { - //if the target is arrested successfully, reset the damage accumulator - foreach (Character anyCharacter in Character.CharacterList) - { - if (anyCharacter.AIController is HumanAIController anyAI) + humanAI.AddCombatObjective(combatMode, character, delay: GetReactionTime(), + onCompleted: () => + { + //if the target is arrested successfully, reset the damage accumulator + foreach (Character anyCharacter in Character.CharacterList) { - anyAI.structureDamageAccumulator?.Remove(character); + if (anyCharacter.AIController is HumanAIController anyAI) + { + anyAI.structureDamageAccumulator?.Remove(character); + } } - } - }); + }); return true; } } @@ -1848,11 +1879,14 @@ namespace Barotrauma public static void ItemTaken(Item item, Character thief) { if (item == null || thief == null || item.GetComponent() != null) { return; } - bool someoneSpoke = false; - bool stolenItemsInside = item.OwnInventory?.FindAllItems(it => it.SpawnedInCurrentOutpost && !it.AllowStealing, recursive: true).Any() ?? false; - - if ((item.SpawnedInCurrentOutpost && !item.AllowStealing || stolenItemsInside) && thief.TeamID != CharacterTeamType.FriendlyNPC && !item.HasTag(Tags.HandLockerItem)) + if (item.Illegitimate && item.GetRootInventoryOwner() is Character itemOwner && itemOwner != thief && itemOwner.TeamID == thief.TeamID) + { + // The player attempts to use a bot as a mule or get them arrested -> just arrest the player instead. + thief.IsCriminal = true; + } + bool foundIllegitimateItems = item.Illegitimate || item.OwnInventory?.FindItem(it => it.Illegitimate, recursive: true) != null; + if (foundIllegitimateItems && thief.TeamID != CharacterTeamType.FriendlyNPC) { foreach (Character otherCharacter in Character.CharacterList) { @@ -1872,19 +1906,24 @@ namespace Barotrauma if (item.HasTag(Tags.FireExtinguisher) && connectedHulls.Any(h => h.FireSources.Any())) { continue; } if (item.HasTag(Tags.DivingGear) && connectedHulls.Any(h => h.ConnectedGaps.Any(g => AIObjectiveFixLeaks.IsValidTarget(g, thief)))) { continue; } } - if (!someoneSpoke) + if (item.HasTag(Tags.Handcuffs) && thief.HasEquippedItem(item)) { - if (!item.StolenDuringRound) - { - ApplyStealingReputationLoss(item); - item.StolenDuringRound = true; - } - otherCharacter.Speak(TextManager.Get("dialogstealwarning").Value, null, Rand.Range(0.5f, 1.0f), "thief".ToIdentifier(), 10.0f); - someoneSpoke = true; + // Handcuffed -> don't react. + continue; + } + if (!item.StolenDuringRound) + { + item.StolenDuringRound = true; + ApplyStealingReputationLoss(item); #if CLIENT HintManager.OnStoleItem(thief, item); #endif } + if (!someoneSpoke) + { + otherCharacter.Speak(TextManager.Get("dialogstealwarning").Value, null, Rand.Range(0.5f, 1.0f), "thief".ToIdentifier(), 10.0f); + someoneSpoke = true; + } // React if we are security if (!TriggerSecurity(otherHumanAI)) { @@ -1900,7 +1939,7 @@ namespace Barotrauma } } } - else if (item.OwnInventory?.FindItem(it => it.SpawnedInCurrentOutpost && !item.AllowStealing, true) is { } foundItem) + else if (item.OwnInventory?.FindItem(it => it.Illegitimate, true) is { } foundItem) { ItemTaken(foundItem, thief); } @@ -1914,11 +1953,12 @@ namespace Barotrauma { findThieves.InspectEveryone(); } + bool isCriminal = thief.IsCriminal; humanAI.AddCombatObjective(AIObjectiveCombat.CombatMode.Arrest, thief, delay: GetReactionTime(), - abortCondition: obj => thief.Inventory.FindItem(it => it != null && it.StolenDuringRound, true) == null, + abortCondition: obj => !isCriminal && thief.Inventory.FindItem(it => it.Illegitimate, recursive: true) == null, onAbort: () => { - if (item != null && !item.Removed && humanAI != null && !humanAI.ObjectiveManager.IsCurrentObjective()) + if (!item.Removed && !humanAI.ObjectiveManager.IsCurrentObjective()) { humanAI.ObjectiveManager.AddObjective(new AIObjectiveGetItem(humanAI.Character, item, humanAI.ObjectiveManager, equip: false) { @@ -1926,7 +1966,8 @@ namespace Barotrauma }); } }, - allowHoldFire: true); + allowHoldFire: !isCriminal, + speakWarnings: !isCriminal); return true; } } @@ -2084,7 +2125,7 @@ namespace Barotrauma } bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishOrder && extinguishOrder.Priority > 0 || objectiveManager.HasActiveObjective(); bool ignoreOxygen = HasDivingGear(character); - bool ignoreEnemies = ObjectiveManager.IsCurrentOrder() || ObjectiveManager.IsCurrentObjective(); + bool ignoreEnemies = ObjectiveManager.HasObjectiveOrOrder(); float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater: false, ignoreOxygen, ignoreFire, ignoreEnemies); if (isCurrentHull) { @@ -2146,7 +2187,7 @@ namespace Barotrauma { if (!visibleHulls.Contains(c.CurrentHull)) { continue; } } - if (IsActive(c) && !IsFriendly(character, c) && !c.IsArrested) + if (IsActive(c) && !IsFriendly(character, c) && !c.IsHandcuffed) { enemyCount++; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index db44b0290..5ab7b6829 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -492,14 +492,20 @@ namespace Barotrauma var door = currentPath.CurrentNode.ConnectedDoor; float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 5, 0, 1)); float colliderHeight = collider.Height / 2 + collider.Radius; - float heightDiff = currentPath.CurrentNode.SimPosition.Y - collider.SimPosition.Y; - if (heightDiff < colliderHeight) + if (currentPath.CurrentNode.Stairs == null) { - //the waypoint is between the top and bottom of the collider, no need to move vertically. - diff.Y = 0.0f; + float heightDiff = currentPath.CurrentNode.SimPosition.Y - collider.SimPosition.Y; + if (heightDiff < colliderHeight) + { + // Original comment: + //the waypoint is between the top and bottom of the collider, no need to move vertically. + // Note that the waypoint can be below collider too! This might be incorrect. + diff.Y = 0.0f; + } } - if (currentPath.CurrentNode.Stairs != null) + else { + // In stairs bool isNextNodeInSameStairs = currentPath.NextNode?.Stairs == currentPath.CurrentNode.Stairs; if (!isNextNodeInSameStairs) { @@ -883,7 +889,18 @@ namespace Barotrauma //steer away from edges of the hull bool wander = false; bool inWater = character.AnimController.InWater; - var currentHull = character.CurrentHull; + Hull currentHull = character.CurrentHull; + // TODO: disabled for now, because seems to cause bots to walk towards walls/doors in some places. In some places it's because how the hulls are defined, but there is probably something else too, is it seems to happen also elsewhere. + // if (!inWater) + // { + // Vector2 colliderBottomPos = ConvertUnits.ToDisplayUnits(character.AnimController.GetColliderBottom()); + // if (Hull.FindHull(colliderBottomPos, guess: currentHull, useWorldCoordinates: false) is Hull lowestHull) + // { + // // Use the hull found at the collider bottom, if found. + // // Makes difference in some rooms that have multiple hulls, of which the lowest hull where the feet are might not be the same as where the center position of the main collider is. + // currentHull = lowestHull; + // } + // } if (currentHull != null && !inWater) { float roomWidth = currentHull.Rect.Width; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs index 963912fc7..a7cd0d8cd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs @@ -388,6 +388,7 @@ namespace Barotrauma character.TeleportTo(ConvertUnits.ToDisplayUnits(forceColliderSimPosition.Value)); } + // TODO: Shouldn't multiply by LimbScale here, because it's already applied in attachLimb.Scale! Vector2 transformedLocalAttachPos = localAttachPos * attachLimb.Scale * attachLimb.Params.Ragdoll.LimbScale; if (jointDir < 0.0f) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs index d099b1d21..c2d9bf373 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -15,7 +15,10 @@ namespace Barotrauma public virtual string DebugTag => Identifier.Value; public virtual bool ForceRun => false; public virtual bool IgnoreUnsafeHulls => false; - public virtual bool AbandonWhenCannotCompleteSubjectives => true; + public virtual bool AbandonWhenCannotCompleteSubObjectives => true; + /// + /// Should subobjectives be sorted according to their priority? + /// public virtual bool AllowSubObjectiveSorting => false; public virtual bool PrioritizeIfSubObjectivesActive => false; @@ -28,8 +31,7 @@ namespace Barotrauma /// Run the main objective with all subobjectives concurrently? /// If false, the main objective will continue only when all the subobjectives have been removed (done). /// - public virtual bool ConcurrentObjectives => false; - + protected virtual bool ConcurrentObjectives => false; public virtual bool KeepDivingGearOn => false; public virtual bool KeepDivingGearOnAlsoWhenInactive => false; @@ -37,10 +39,36 @@ namespace Barotrauma /// There's a separate property for diving suit and mask: KeepDivingGearOn. /// public virtual bool AllowAutomaticItemUnequipping => false; - public virtual bool AllowOutsideSubmarine => false; - public virtual bool AllowInFriendlySubs => false; - public virtual bool AllowInAnySub => false; - public virtual bool AllowWhileHandcuffed => true; + + // These booleans are used for defining whether the objective is allowed in different contexts. E.g. AllowOutsideSubmarine needs to be true or the objective cannot be active when the bot is swimming outside. + protected virtual bool AllowOutsideSubmarine => false; + /// + /// When true, the objective is allowed in the player subs (when in the same team) and on friendly outposts (regardless of the alignment). + /// Note: ignored when is true. + /// + protected virtual bool AllowInFriendlySubs => false; + protected virtual bool AllowInAnySub => false; + protected virtual bool AllowWhileHandcuffed => true; + + /// + /// Should the objective abandon when it's not allowed in the current context or should it just stay inactive with 0 priority? + /// Abandoned automatic objectives are removed and recreated automatically (when new orders are assigned or after a cooldown period). + /// Abandoned orders are removed, but the most recent order can be reissued by clicking the small order icon with the arrow in the crew manager panel. + /// + protected virtual bool AbandonIfDisallowed => true; + + public virtual bool CanBeCompleted => !Abandon; + + protected virtual float MaxDevotion => 10; + + /// + /// Which event action (if any) created this objective + /// + public EventAction SourceEventAction; + /// + /// Which objective (if any) created this objective. When this is a subobjective, the parent objective is used by default. + /// + public AIObjective SourceObjective; protected readonly List subObjectives = new List(); private float _cumulatedDevotion; @@ -50,8 +78,6 @@ namespace Barotrauma set { _cumulatedDevotion = MathHelper.Clamp(value, 0, MaxDevotion); } } - protected virtual float MaxDevotion => 10; - /// /// Final priority value after all calculations. /// @@ -100,17 +126,13 @@ namespace Barotrauma } } } - - public virtual bool CanBeCompleted => !Abandon; - - /// - /// When true, the objective is never completed, unless CanBeCompleted returns false. - /// - public virtual bool IsLoop { get; set; } + public IEnumerable SubObjectives => subObjectives; + public AIObjective CurrentSubObjective => subObjectives.FirstOrDefault(); private readonly List all = new List(); + public IEnumerable GetSubObjectivesRecursive(bool includingSelf = false) { all.Clear(); @@ -124,13 +146,11 @@ namespace Barotrauma } return all; } - -#pragma warning disable CS0649 + /// /// Aborts the objective when this condition is true. /// public Func AbortCondition; -#pragma warning restore CS0649 /// /// A single shot event. Automatically cleared after launching. Use OnCompleted method for implementing (internal) persistent behavior. @@ -186,6 +206,7 @@ namespace Barotrauma public void AddSubObjective(AIObjective objective, bool addFirst = false) { var type = objective.GetType(); + objective.SourceObjective = this; subObjectives.RemoveAll(o => o.GetType() == type); if (addFirst) { @@ -259,17 +280,21 @@ namespace Barotrauma return character.Submarine.Info.IsOutpost && character.Submarine.TeamID == CharacterTeamType.FriendlyNPC; } - protected void HandleNonAllowed() + protected void HandleDisallowed() { Priority = 0; - Abandon = !IsIgnoredAtOutpost(); + if (AbandonIfDisallowed && !IsIgnoredAtOutpost()) + { + // Never abandon objectives inside a friendly outpost, because otherwise we'd have to reassign most orders every round. + Abandon = true; + } } protected virtual float GetPriority() { if (!IsAllowed) { - HandleNonAllowed(); + HandleDisallowed(); return Priority; } if (objectiveManager.IsOrder(this)) @@ -360,7 +385,7 @@ namespace Barotrauma /// /// Checks if the subobjectives in the given collection are removed from the subobjectives. And if so, removes it also from the dictionary. /// - protected void SyncRemovedObjectives(Dictionary dictionary, IEnumerable collection) where T2 : AIObjective + protected virtual void SyncRemovedObjectives(Dictionary dictionary, IEnumerable collection) where T2 : AIObjective { foreach (T1 key in collection) { @@ -398,6 +423,7 @@ namespace Barotrauma { if (objective.AllowMultipleInstances) { + objective.SourceObjective = this; subObjectives.Add(objective); } else @@ -482,6 +508,9 @@ namespace Barotrauma } } + /// + /// Check whether the objective should be aborted (and abandon if it should), and return whether the objective is completed or not. + /// private bool Check() { if (AbortCondition != null && AbortCondition(this)) @@ -492,6 +521,9 @@ namespace Barotrauma return CheckObjectiveSpecific(); } + /// + /// Should return whether the objective is completed or not. + /// protected abstract bool CheckObjectiveSpecific(); private bool CheckState() @@ -527,7 +559,7 @@ namespace Barotrauma DebugConsole.NewMessage($"{character.Name}: Removing SUBobjective {subObjective.DebugTag} of {DebugTag}, because it cannot be completed.", Color.Red); #endif subObjectives.Remove(subObjective); - if (AbandonWhenCannotCompleteSubjectives) + if (AbandonWhenCannotCompleteSubObjectives) { if (objectiveManager.IsOrder(this)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs index e43bb2622..83f884c60 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs @@ -16,7 +16,7 @@ namespace Barotrauma public AIObjectiveChargeBatteries(Character character, AIObjectiveManager objectiveManager, Identifier option, float priorityModifier) : base(character, objectiveManager, priorityModifier, option) { } - protected override bool Filter(PowerContainer battery) + protected override bool IsValidTarget(PowerContainer battery) { if (battery == null) { return false; } if (battery.OutputDisabled) { return false; } @@ -37,7 +37,7 @@ namespace Barotrauma return true; } - protected override float TargetEvaluation() + protected override float GetTargetPriority() { if (Targets.None()) { return 0; } if (Option == "charge") @@ -80,7 +80,6 @@ namespace Barotrauma protected override AIObjective ObjectiveConstructor(PowerContainer battery) => new AIObjectiveOperateItem(battery, character, objectiveManager, Option, false, priorityModifier: PriorityModifier) { - IsLoop = false, Override = !character.IsDismissed, completionCondition = () => IsReady(battery) }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCheckStolenItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCheckStolenItems.cs index cd63e7856..741fe381d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCheckStolenItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCheckStolenItems.cs @@ -8,8 +8,8 @@ namespace Barotrauma class AIObjectiveCheckStolenItems : AIObjective { public override Identifier Identifier { get; set; } = "check stolen items".ToIdentifier(); - public override bool AllowOutsideSubmarine => false; - public override bool AllowInAnySub => false; + protected override bool AllowOutsideSubmarine => false; + protected override bool AllowInAnySub => false; public float FindStolenItemsProbability = 1.0f; @@ -21,36 +21,38 @@ namespace Barotrauma Done } - private float inspectDelay; - private float warnDelay; + private const float InspectTime = 5.0f; + private const float NormalWarnDelay = 5.0f; + private const float CriminalWarnDelay = 3.0f; + private float inspectTimer; + private float warnTimer; + private float currentWarnDelay; private State currentState; - public readonly Character TargetCharacter; + public readonly Character Target; private AIObjectiveGoTo? goToObjective; private readonly List stolenItems = new List(); - public AIObjectiveCheckStolenItems(Character character, Character targetCharacter, AIObjectiveManager objectiveManager, float priorityModifier = 1) : + public AIObjectiveCheckStolenItems(Character character, Character target, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { - TargetCharacter = targetCharacter; - inspectDelay = 5.0f; - warnDelay = 5.0f; - } - - public override bool IsLoop - { - get => false; - set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace()); + Target = target; + InitTimers(); } protected override bool CheckObjectiveSpecific() => false; protected override float GetPriority() { - if (!Abandon && !IsCompleted && objectiveManager.IsOrder(this)) + if (character.IsClimbing) + { + // Target is climbing -> stop following the objective (soft abandon, without ignoring the target). + Priority = 0; + } + else if (!Abandon && !IsCompleted && objectiveManager.IsOrder(this)) { Priority = objectiveManager.GetOrderPriority(this); } @@ -70,22 +72,32 @@ namespace Barotrauma { switch (currentState) { + case State.Done: + IsCompleted = true; + break; case State.GotoTarget: TryAddSubObjective(ref goToObjective, - constructor: () => + constructor: () => new AIObjectiveGoTo(Target, character, objectiveManager, repeat: false) { - return new AIObjectiveGoTo(TargetCharacter, character, objectiveManager, repeat: false) - { - SpeakIfFails = false - }; + SpeakIfFails = false }, onCompleted: () => { RemoveSubObjective(ref goToObjective); - currentState = State.Inspect; - stolenItems.Clear(); - TargetCharacter.Inventory.FindAllItems(it => it.SpawnedInCurrentOutpost && !it.AllowStealing, recursive: true, stolenItems); - character.Speak(TextManager.Get("dialogcheckstolenitems").Value); + if (character.IsClimbing) + { + // Shouldn't start inspecting characters when they climb, nor get here, because the priority should be 0, + // but if this still happens, we'll have to abandon the objective + // because it's not currently possible to hold to characters and ladders at the same time. + Abandon = true; + } + else + { + currentState = State.Inspect; + stolenItems.Clear(); + Target.Inventory.FindAllItems(it => it.Illegitimate, recursive: true, stolenItems); + character.Speak(TextManager.Get(Target.IsCriminal ? "dialogcheckstolenitems.criminal" : "dialogcheckstolenitems").Value); + } }, onAbandon: () => { @@ -103,10 +115,23 @@ namespace Barotrauma private void Inspect(float deltaTime) { - if (inspectDelay > 0.0f) + if (inspectTimer > 0.0f) { - character.SelectCharacter(TargetCharacter); - inspectDelay -= deltaTime; + character.SelectCharacter(Target); + inspectTimer -= deltaTime; + if (inspectTimer < InspectTime - 1) + { + if (Target.AnimController.IsMovingFast) + { + ArrestFleeing(); + } + else if (Math.Abs(Target.AnimController.TargetMovement.X) > 1.0f) + { + // If the target moves, reset the inspect timer and tell to hold still + character.Speak(TextManager.Get("dialogcheckstolenitems.holdstill").Value, identifier: "holdstill".ToIdentifier(), minDurationBetweenSimilar: 3f); + inspectTimer = InspectTime; + } + } return; } @@ -118,7 +143,7 @@ namespace Barotrauma } else { - character.Speak(TextManager.Get("dialogcheckstolenitems.nostolenitems").Value); + character.Speak(TextManager.Get(Target.IsCriminal ? "dialogcheckstolenitems.nostolenitems.criminal" : "dialogcheckstolenitems.nostolenitems").Value); currentState = State.Done; IsCompleted = true; } @@ -127,16 +152,23 @@ namespace Barotrauma private void Warn(float deltaTime) { - if (warnDelay > 0.0f) + if (warnTimer > 0.0f) { - warnDelay -= deltaTime; + warnTimer -= deltaTime; + if (warnTimer < currentWarnDelay - 1) + { + if (Target.AnimController.IsMovingFast) + { + ArrestFleeing(); + } + } return; } - var stolenItemsOnCharacter = stolenItems.Where(it => it.GetRootInventoryOwner() == TargetCharacter); + var stolenItemsOnCharacter = stolenItems.Where(it => it.GetRootInventoryOwner() == Target); if (stolenItemsOnCharacter.Any()) { - character.Speak(TextManager.Get("dialogcheckstolenitems.arrest").Value); - HumanAIController.AddCombatObjective(AIObjectiveCombat.CombatMode.Arrest, TargetCharacter); + character.Speak(TextManager.Get(character.IsCriminal ? "dialogcheckstolenitems.arrest.criminal" : "dialogcheckstolenitems.arrest").Value); + Arrest(abortWhenItemsDropped: true, allowHoldFire: true); foreach (var stolenItem in stolenItemsOnCharacter) { HumanAIController.ApplyStealingReputationLoss(stolenItem); @@ -156,5 +188,44 @@ namespace Barotrauma currentState = State.Done; IsCompleted = true; } + + private void ArrestFleeing() + { + character.Speak(TextManager.Get("dialogcheckstolenitems.arrest").Value); + currentState = State.Done; + IsCompleted = true; + Arrest(abortWhenItemsDropped: false, allowHoldFire: false); + } + + private void Arrest(bool abortWhenItemsDropped, bool allowHoldFire) + { + bool isCriminal = Target.IsCriminal; + Func? abortCondition = null; + if (abortWhenItemsDropped && !isCriminal) + { + abortCondition = obj => Target.Inventory.FindItem(it => it.Illegitimate, recursive: true) == null; + } + HumanAIController.AddCombatObjective(AIObjectiveCombat.CombatMode.Arrest, Target, allowHoldFire: allowHoldFire && !isCriminal, speakWarnings: !isCriminal, abortCondition: abortCondition); + } + + public override void OnDeselected() + { + base.OnDeselected(); + character.DeselectCharacter(); + } + + public override void Reset() + { + base.Reset(); + currentState = State.GotoTarget; + InitTimers(); + } + + private void InitTimers() + { + inspectTimer = InspectTime; + currentWarnDelay = Target.IsCriminal ? CriminalWarnDelay : NormalWarnDelay; + warnTimer = currentWarnDelay; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs index c200a173c..e1f1e3ae9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs @@ -12,7 +12,7 @@ namespace Barotrauma public override Identifier Identifier { get; set; } = "cleanup item".ToIdentifier(); public override bool KeepDivingGearOn => true; public override bool AllowAutomaticItemUnequipping => false; - public override bool AllowWhileHandcuffed => false; + protected override bool AllowWhileHandcuffed => false; public readonly Item item; public bool IsPriority { get; set; } @@ -24,7 +24,7 @@ namespace Barotrauma /// /// Allows decontainObjective to be interrupted if this objective gets abandoned (e.g. due to the item no longer being eligible for cleanup) /// - public override bool ConcurrentObjectives => true; + protected override bool ConcurrentObjectives => true; public AIObjectiveCleanupItem(Item item, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) @@ -36,7 +36,7 @@ namespace Barotrauma { if (!IsAllowed) { - HandleNonAllowed(); + HandleDisallowed(); return Priority; } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs index 8ac1bc914..78c98414a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs @@ -31,7 +31,7 @@ namespace Barotrauma this.prioritizedItems.AddRange(prioritizedItems.Where(i => i != null)); } - protected override float TargetEvaluation() + protected override float GetTargetPriority() { if (Targets.None()) { return 0; } if (objectiveManager.IsOrder(this)) @@ -47,7 +47,7 @@ namespace Barotrauma return AIObjectiveManager.RunPriority - 0.5f; } - protected override bool Filter(Item target) + protected override bool IsValidTarget(Item target) { System.Diagnostics.Debug.Assert(target.GetComponent() is { } pickable && !pickable.IsAttached, "Invalid target in AIObjectiveCleanUpItems - the the objective should only be checking pickable, non-attached items."); System.Diagnostics.Debug.Assert(target.Prefab.PreferredContainers.Any(), "Invalid target in AIObjectiveCleanUpItems - the the objective should only be checking items that have preferred containers defined."); @@ -100,7 +100,7 @@ namespace Barotrauma { if (item == null) { return false; } if (item.DontCleanUp) { return false; } - if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; } + if (item.Illegitimate == character.IsOnPlayerTeam) { return false; } if (item.ParentInventory != null) { if (item.Container == null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index a31e90903..313448642 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -16,23 +16,23 @@ namespace Barotrauma public override bool KeepDivingGearOn => true; public override bool IgnoreUnsafeHulls => true; - public override bool AllowOutsideSubmarine => true; - public override bool AllowInAnySub => true; + protected override bool AllowOutsideSubmarine => true; + protected override bool AllowInAnySub => true; private readonly CombatMode initialMode; private float checkWeaponsTimer; - private const float checkWeaponsInterval = 1; + private const float CheckWeaponsInterval = 1; private float ignoreWeaponTimer; - private const float ignoredWeaponsClearTime = 10; + private const float IgnoredWeaponsClearTime = 10; - private const float goodWeaponPriority = 30; - - private const float arrestHoldFireTime = 8; + private const float GoodWeaponPriority = 30; + private float holdFireTimer; private bool hasAimed; private bool isLethalWeapon; - private bool AllowCoolDown => !IsOffensiveOrArrest || Mode != initialMode || character.TeamID == Enemy.TeamID; + private bool AllowCoolDown => allowCooldown || !IsOffensiveOrArrest || Mode != initialMode || character.TeamID == Enemy.TeamID; + private bool allowCooldown; public Character Enemy { get; private set; } public bool HoldPosition { get; set; } @@ -45,7 +45,6 @@ namespace Barotrauma { _weapon = value; _weaponComponent = null; - hasAimed = false; } } private ItemComponent _weaponComponent; @@ -58,8 +57,8 @@ namespace Barotrauma } } - public override bool ConcurrentObjectives => true; - public override bool AbandonWhenCannotCompleteSubjectives => false; + protected override bool ConcurrentObjectives => true; + public override bool AbandonWhenCannotCompleteSubObjectives => false; private readonly AIObjectiveFindSafety findSafety; private readonly HashSet weapons = new HashSet(); @@ -72,6 +71,9 @@ namespace Barotrauma private Hull retreatTarget; private float coolDownTimer; + private float pathBackTimer; + private const float DefaultCoolDown = 10.0f; + private const float PathBackCheckTime = 1.0f; private IEnumerable myBodies; private float aimTimer; private float reloadTimer; @@ -79,17 +81,25 @@ namespace Barotrauma private bool canSeeTarget; private float visibilityCheckTimer; - private const float visibilityCheckInterval = 0.2f; + private const float VisibilityCheckInterval = 0.2f; private float sqrDistance; - private const float maxDistance = 2000; - private const float distanceCheckInterval = 0.2f; + private const float MaxDistance = 2000; + private const float DistanceCheckInterval = 0.2f; private float distanceTimer; - private const float closeDistanceThreshold = 300; - private const float floorHeightApproximate = 100; + private const float CloseDistanceThreshold = 300; + private const float FloorHeightApproximate = 100; public bool AllowHoldFire; + public bool SpeakWarnings; + private bool firstWarningTriggered; + private bool lastWarningTriggered; + + public float ArrestHoldFireTime { get; init; } = 10; + + private const float ArrestTargetDistance = 100; + private bool arrestingRegistered; /// /// Don't start using a weapon if this condition is true @@ -123,7 +133,7 @@ namespace Barotrauma public CombatMode Mode { get; private set; } private bool IsOffensiveOrArrest => initialMode is CombatMode.Offensive or CombatMode.Arrest; - private bool TargetEliminated => IsEnemyDisabled || Enemy.IsUnconscious && Enemy.Params.Health.ConstantHealthRegeneration <= 0.0f || Enemy.IsArrested && !character.IsInstigator; + private bool TargetEliminated => IsEnemyDisabled || (Enemy.IsUnconscious && Enemy.Params.Health.ConstantHealthRegeneration <= 0.0f) || (!character.IsInstigator && Enemy.IsHandcuffed && Enemy.IsKnockedDown); private bool IsEnemyDisabled => Enemy == null || Enemy.Removed || Enemy.IsDead; private float AimSpeed => HumanAIController.AimSpeed; @@ -141,7 +151,7 @@ namespace Barotrauma if (character.CurrentHull != null && Enemy.CurrentHull != null && character.CurrentHull != Enemy.CurrentHull) { // Inside, not in the same hull with the enemy - if (Math.Abs(toEnemy.Y) > floorHeightApproximate) + if (Math.Abs(toEnemy.Y) > FloorHeightApproximate) { // Different floor return false; @@ -156,7 +166,7 @@ namespace Barotrauma return Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition) < margin * margin; } - public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1, float coolDown = 10.0f) + public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1, float coolDown = DefaultCoolDown) : base(character, objectiveManager, priorityModifier) { if (mode == CombatMode.None) @@ -187,48 +197,31 @@ namespace Barotrauma protected override float GetPriority() { - if (Enemy == null || Enemy.Removed) - { - Priority = 0; - Abandon = true; - return Priority; - } - if (character.TeamID == CharacterTeamType.FriendlyNPC) - { - if (Enemy.Submarine == null || (Enemy.Submarine.TeamID != character.TeamID && Enemy.Submarine != character.Submarine)) - { - Priority = 0; - Abandon = true; - return Priority; - } - } if (TargetEliminated) { Priority = 0; + return Priority; } - else + // 91-100 + const float minPriority = AIObjectiveManager.EmergencyObjectivePriority + 1; + const float maxPriority = AIObjectiveManager.MaxObjectivePriority; + const float priorityScale = maxPriority - minPriority; + float xDist = Math.Abs(character.WorldPosition.X - Enemy.WorldPosition.X); + float yDist = Math.Abs(character.WorldPosition.Y - Enemy.WorldPosition.Y); + if (HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull)) { - // 91-100 - const float minPriority = AIObjectiveManager.EmergencyObjectivePriority + 1; - const float maxPriority = AIObjectiveManager.MaxObjectivePriority; - const float priorityScale = maxPriority - minPriority; - float xDist = Math.Abs(character.WorldPosition.X - Enemy.WorldPosition.X); - float yDist = Math.Abs(character.WorldPosition.Y - Enemy.WorldPosition.Y); - if (HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull)) + xDist /= 2; + yDist /= 2; + } + float distanceFactor = MathUtils.InverseLerp(3000, 0, xDist + yDist * 5); + float devotion = CumulatedDevotion / 100; + float additionalPriority = MathHelper.Lerp(0, priorityScale, Math.Clamp(devotion + distanceFactor, 0, 1)); + Priority = Math.Min((minPriority + additionalPriority) * PriorityModifier, maxPriority); + if (Priority > 0) + { + if (EnemyAIController.IsLatchedToSomeoneElse(Enemy, character)) { - xDist /= 2; - yDist /= 2; - } - float distanceFactor = MathUtils.InverseLerp(3000, 0, xDist + yDist * 5); - float devotion = CumulatedDevotion / 100; - float additionalPriority = MathHelper.Lerp(0, priorityScale, Math.Clamp(devotion + distanceFactor, 0, 1)); - Priority = Math.Min((minPriority + additionalPriority) * PriorityModifier, maxPriority); - if (Priority > 0) - { - if (EnemyAIController.IsLatchedToSomeoneElse(Enemy, character)) - { - Priority = 0; - } + Priority = 0; } } return Priority; @@ -246,7 +239,7 @@ namespace Barotrauma if (ignoreWeaponTimer < 0) { ignoredWeapons.Clear(); - ignoreWeaponTimer = ignoredWeaponsClearTime; + ignoreWeaponTimer = IgnoredWeaponsClearTime; } bool isFightingIntruders = objectiveManager.IsCurrentObjective(); if (findSafety != null && isFightingIntruders) @@ -258,7 +251,7 @@ namespace Barotrauma distanceTimer -= deltaTime; if (distanceTimer < 0) { - distanceTimer = distanceCheckInterval; + distanceTimer = DistanceCheckInterval; sqrDistance = Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition); } } @@ -266,16 +259,62 @@ namespace Barotrauma protected override bool CheckObjectiveSpecific() { - if (character.Submarine is not { TeamID: CharacterTeamType.FriendlyNPC }) + if (character.Submarine is { TeamID: CharacterTeamType.FriendlyNPC } && character.Submarine == Enemy.Submarine) { - // Can't lose the target in friendly outposts. - if (sqrDistance > maxDistance * maxDistance) + // Target still in the outpost + if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsSecurity) { - // The target escaped from us. - return true; + // Outpost guards shouldn't lose the target in friendly outposts, + // However, if we are not a guard, let's ensure that we allow the cooldown. + allowCooldown = true; } } - return IsEnemyDisabled || (AllowCoolDown && coolDownTimer <= 0); + else + { + if ((Enemy.Submarine == null && character.Submarine != null) || sqrDistance > MaxDistance * MaxDistance) + { + // The target escaped from us. + Abandon = true; + if (character.TeamID == CharacterTeamType.FriendlyNPC && IsOffensiveOrArrest) + { + Enemy.IsCriminal = true; + } + return false; + } + if (Enemy.Submarine != null && character.Submarine != null && character.TeamID == CharacterTeamType.FriendlyNPC) + { + if (Enemy.Submarine.TeamID != character.TeamID) + { + allowCooldown = true; + // Target not in the outpost anymore. + if (character.CanSeeTarget(Enemy)) + { + allowCooldown = false; + coolDownTimer = DefaultCoolDown; + } + else if (pathBackTimer <= 0) + { + // Check once per sec during the cooldown whether we can find a path back to the docking port + pathBackTimer = PathBackCheckTime; + foreach ((Submarine sub, DockingPort dockingPort) in character.Submarine.ConnectedDockingPorts) + { + if (sub.TeamID != character.TeamID) { continue; } + var path = PathSteering.PathFinder.FindPath(character.SimPosition, character.GetRelativeSimPosition(dockingPort.Item), character.Submarine, nodeFilter: node => node.Waypoint.CurrentHull != null); + if (path.Unreachable) + { + allowCooldown = false; + coolDownTimer = DefaultCoolDown; + } + } + } + if (IsOffensiveOrArrest) + { + Enemy.IsCriminal = true; + } + } + } + } + return TargetEliminated || (AllowCoolDown && coolDownTimer <= 0); } protected override void Act(float deltaTime) @@ -288,6 +327,10 @@ namespace Barotrauma if (AllowCoolDown) { coolDownTimer -= deltaTime; + if (pathBackTimer > 0) + { + pathBackTimer -= deltaTime; + } } if (seekAmmunitionObjective == null && seekWeaponObjective == null) { @@ -303,27 +346,6 @@ namespace Barotrauma { Move(deltaTime); } - switch (Mode) - { - case CombatMode.Offensive: - if (TargetEliminated && objectiveManager.IsCurrentOrder()) - { - character.Speak(TextManager.Get("DialogTargetDown").Value, null, 3.0f, "targetdown".ToIdentifier(), 30.0f); - } - break; - case CombatMode.Arrest: - if (HumanAIController.HasItem(Enemy, Tags.HandLockerItem, out _, requireEquipped: true)) - { - IsCompleted = true; - } - else if (Enemy.IsKnockedDown && - !objectiveManager.IsCurrentObjective() && - !HumanAIController.HasItem(character, Tags.HandLockerItem, out _, requireEquipped: false)) - { - IsCompleted = true; - } - break; - } } } @@ -389,7 +411,7 @@ namespace Barotrauma && !character.IsInstigator); // Instigators (= aggressive NPCs spawned with events) shouldn't seek new weapons, because we don't want them to grab e.g. an smg, if they spawn with a wrench or something. if (checkWeaponsTimer < 0) { - checkWeaponsTimer = checkWeaponsInterval; + checkWeaponsTimer = CheckWeaponsInterval; // First go through all weapons and try to reload without seeking ammunition HashSet allWeapons = FindWeaponsFromInventory(); while (allWeapons.Any()) @@ -412,7 +434,7 @@ namespace Barotrauma // All good, the weapon is loaded break; } - bool seekAmmo = isAllowedToSeekWeapons && seekAmmunitionObjective == null && !IsEnemyClose(closeDistanceThreshold); + bool seekAmmo = isAllowedToSeekWeapons && seekAmmunitionObjective == null && !IsEnemyClose(CloseDistanceThreshold); if (Reload(seekAmmo: seekAmmo)) { // All good, we can use the weapon. @@ -458,7 +480,7 @@ namespace Barotrauma Mode = CombatMode.Retreat; } } - else if (seekAmmunitionObjective == null && (WeaponComponent == null || (WeaponComponent.CombatPriority < goodWeaponPriority && !IsEnemyClose(closeDistanceThreshold)))) + else if (seekAmmunitionObjective == null && (WeaponComponent == null || (WeaponComponent.CombatPriority < GoodWeaponPriority && !IsEnemyClose(CloseDistanceThreshold)))) { // No weapon or only a poor weapon equipped -> try to find better. RemoveSubObjective(ref retreatObjective); @@ -485,7 +507,7 @@ namespace Barotrauma if (range is > 0 and < float.PositiveInfinity) { // Y distance is irrelevant when we are on the same floor. If we are on a different floor, let's double it. - float yDiff = Math.Abs(toItem.Y) > floorHeightApproximate ? toItem.Y * 2 : 0; + float yDiff = Math.Abs(toItem.Y) > FloorHeightApproximate ? toItem.Y * 2 : 0; Vector2 adjustedDiff = new Vector2(toItem.X, yDiff); if (adjustedDiff.LengthSquared() > MathUtils.Pow2(range)) { @@ -501,7 +523,7 @@ namespace Barotrauma } if (i.CurrentHull != null && !HumanAIController.VisibleHulls.Contains(i.CurrentHull)) { - if (Math.Abs(toItem.Y) > floorHeightApproximate && Math.Abs(toEnemy.Y) > floorHeightApproximate) + if (Math.Abs(toItem.Y) > FloorHeightApproximate && Math.Abs(toEnemy.Y) > FloorHeightApproximate) { if (Math.Sign(toItem.Y) == Math.Sign(toEnemy.Y)) { @@ -522,7 +544,7 @@ namespace Barotrauma SpeakNoWeapons(); Mode = CombatMode.Retreat; } - else if (!objectiveManager.HasActiveObjective()) + else if (!objectiveManager.HasObjectiveOrOrder()) { // Poor weapon equipped Mode = CombatMode.Defensive; @@ -642,7 +664,7 @@ namespace Barotrauma priority /= 2; } } - else if (Enemy.IsKnockedDown) + else if (Enemy.IsKnockedDown && Mode != CombatMode.Arrest) { // Enemy is stunned, reduce the priority of stunner weapons. Attack attack = GetAttackDefinition(weapon); @@ -775,7 +797,7 @@ namespace Barotrauma float bestPriority = 0; float lethalDmg = -1; bool prioritizeMelee = IsEnemyClose(50) || EnemyAIController.IsLatchedTo(Enemy, character); - bool isCloseToEnemy = prioritizeMelee || IsEnemyClose(closeDistanceThreshold); + bool isCloseToEnemy = prioritizeMelee || IsEnemyClose(CloseDistanceThreshold); foreach (var weapon in weaponList) { float priority = GetWeaponPriority(weapon, prioritizeMelee, canSeekAmmo: !isCloseToEnemy, out lethalDmg); @@ -801,9 +823,28 @@ namespace Barotrauma } isLethalWeapon = lethalDmg > 1; } - if (AllowHoldFire && !hasAimed && holdFireTimer <= 0) + if (AllowHoldFire) { - holdFireTimer = arrestHoldFireTime * Rand.Range(0.75f, 1.25f); + if (!hasAimed && holdFireTimer <= 0) + { + holdFireTimer = ArrestHoldFireTime * Rand.Range(0.9f, 1.1f); + } + else + { + if (SpeakWarnings) + { + if (!lastWarningTriggered && holdFireTimer < ArrestHoldFireTime * 0.3f) + { + FriendlyGuardSpeak("dialogarrest.lastwarning".ToIdentifier(), delay: 0, minDurationBetweenSimilar: 0f); + lastWarningTriggered = true; + } + else if (!firstWarningTriggered && holdFireTimer < ArrestHoldFireTime * 0.8f) + { + FriendlyGuardSpeak("dialogarrest.firstwarning".ToIdentifier(), delay: 0, minDurationBetweenSimilar: 0f); + firstWarningTriggered = true; + } + } + } } } return weaponComponent.Item; @@ -909,9 +950,10 @@ namespace Barotrauma private void Retreat(float deltaTime) { - if (!Enemy.IsHuman) + if (!Enemy.IsHuman && !character.IsInFriendlySub) { - SpeakRetreating(); + // Only relevant when we are retreating from monsters and are not inside a friendly sub. + PlayerCrewSpeak("dialogcombatretreating".ToIdentifier(), delay: Rand.Range(0f, 1f), minDurationBetweenSimilar: 20); } RemoveFollowTarget(); RemoveSubObjective(ref seekAmmunitionObjective); @@ -931,7 +973,7 @@ namespace Barotrauma { RemoveSubObjective(ref retreatObjective); } - if (character.Submarine == null && sqrDistance < MathUtils.Pow2(maxDistance)) + if (character.Submarine == null && sqrDistance < MathUtils.Pow2(MaxDistance)) { // Swim away SteeringManager.Reset(); @@ -1017,6 +1059,16 @@ namespace Barotrauma } return; } + if (character.TeamID == CharacterTeamType.FriendlyNPC && character.Submarine != null && character.Submarine.TeamID != character.TeamID) + { + // An outpost guard following the target (possibly a player) to another sub -> don't go further, unless can see the enemy. + if (!character.IsClimbing && !character.CanSeeTarget(Enemy)) + { + SteeringManager.Reset(); + RemoveFollowTarget(); + return; + } + } if (followTargetObjective != null && followTargetObjective.Target != Enemy) { RemoveFollowTarget(); @@ -1045,32 +1097,27 @@ namespace Barotrauma } }); if (followTargetObjective == null) { return; } - if (Mode == CombatMode.Arrest && Enemy.IsKnockedDown) + if (Mode == CombatMode.Arrest && Enemy.IsKnockedDown && !arrestingRegistered) { - if (HumanAIController.HasItem(character, Tags.HandLockerItem, out _)) + bool hasHandCuffs = HumanAIController.HasItem(character, Tags.HandLockerItem, out _); + if (!hasHandCuffs && character.TeamID == CharacterTeamType.FriendlyNPC) { - if (!arrestingRegistered) + // Spawn handcuffs + ItemPrefab prefab = ItemPrefab.Find(null, "handcuffs".ToIdentifier()); + if (prefab != null) { - arrestingRegistered = true; - followTargetObjective.Completed += OnArrestTargetReached; - followTargetObjective.CloseEnough = 100; - } - } - else - { - if (character.TeamID == CharacterTeamType.FriendlyNPC) - { - ItemPrefab prefab = ItemPrefab.Find(null, "handcuffs".ToIdentifier()); - if (prefab != null) + Entity.Spawner.AddItemToSpawnQueue(prefab, character.Inventory, onSpawned: i => { - Entity.Spawner.AddItemToSpawnQueue(prefab, character.Inventory, onSpawned: (Item i) => i.SpawnedInCurrentOutpost = true); - } + i.SpawnedInCurrentOutpost = true; + i.AllowStealing = false; + }); } - RemoveFollowTarget(); - SteeringManager.Reset(); } + arrestingRegistered = true; + followTargetObjective.Completed += OnArrestTargetReached; + followTargetObjective.CloseEnough = ArrestTargetDistance; } - if (!arrestingRegistered && followTargetObjective != null) + if (!arrestingRegistered) { followTargetObjective.CloseEnough = WeaponComponent switch @@ -1083,8 +1130,6 @@ namespace Barotrauma } } - private bool arrestingRegistered; - private void RemoveFollowTarget() { if (followTargetObjective != null) @@ -1110,9 +1155,9 @@ namespace Barotrauma // Confiscate stolen goods and all weapons foreach (var item in Enemy.Inventory.AllItemsMod) { - if (character.TeamID == CharacterTeamType.FriendlyNPC && item.StolenDuringRound || - item.HasTag(Tags.Weapon) || item.HasTag(Tags.Poison) || - GetWeaponComponent(item) is { CombatPriority: > 0 }) + // Ignore handcuffs already on the target. + if (item.HasTag(Tags.HandLockerItem) && Enemy.HasEquippedItem(item)) { continue; } + if (item.Illegitimate || item.HasTag(Tags.Weapon) || item.HasTag(Tags.Poison) || GetWeaponComponent(item) is { CombatPriority: > 0 }) { item.Drop(character); character.Inventory.TryPutItem(item, character, CharacterInventory.AnySlot); @@ -1255,7 +1300,7 @@ namespace Barotrauma if (visibilityCheckTimer <= 0.0f) { canSeeTarget = character.CanSeeTarget(Enemy); - visibilityCheckTimer = visibilityCheckInterval; + visibilityCheckTimer = VisibilityCheckInterval; } if (!canSeeTarget) { @@ -1267,7 +1312,7 @@ namespace Barotrauma character.SetInput(InputType.Aim, hit: false, held: true); } hasAimed = true; - if (holdFireTimer > 0) + if (AllowHoldFire && holdFireTimer > 0) { holdFireTimer -= deltaTime; return; @@ -1280,7 +1325,7 @@ namespace Barotrauma if (reloadTimer > 0) { return; } if (holdFireCondition != null && holdFireCondition()) { return; } sqrDistance = Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition); - distanceTimer = distanceCheckInterval; + distanceTimer = DistanceCheckInterval; if (WeaponComponent is MeleeWeapon meleeWeapon) { bool closeEnough = true; @@ -1354,8 +1399,8 @@ namespace Barotrauma private void UseWeapon(float deltaTime) { - // Never allow to attack characters with deadly weapons while trying to arrest. - if (Mode == CombatMode.Arrest && isLethalWeapon) { return; } + // Never allow friendly crew (bots) to attack with deadly weapons. + if (Mode == CombatMode.Arrest && isLethalWeapon && character.IsOnPlayerTeam && Enemy.IsOnPlayerTeam) { return; } character.SetInput(InputType.Shoot, hit: false, held: true); Weapon.Use(deltaTime, user: character); SetReloadTime(WeaponComponent); @@ -1408,6 +1453,36 @@ namespace Barotrauma protected override void OnCompleted() { base.OnCompleted(); + if (Enemy != null) + { + switch (Mode) + { + case CombatMode.Offensive when Enemy.IsUnconscious && objectiveManager.HasObjectiveOrOrder(): + character.Speak(TextManager.Get("DialogTargetDown").Value, null, 3.0f, "targetdown".ToIdentifier(), 30.0f); + break; + case CombatMode.Arrest when IsCompleted: + if (!HumanAIController.IsTrueForAnyBotInTheCrew(bot => + (bot != HumanAIController && bot.ObjectiveManager.CurrentObjective is AIObjectiveCombat { Mode: CombatMode.Arrest } combatObj && combatObj.Enemy == Enemy) || + bot.ObjectiveManager.CurrentObjective is AIObjectiveGoTo { SourceObjective: AIObjectiveCombat combatObjective } && combatObjective.Enemy == Enemy)) + { + // Go to the target and confiscate any stolen items, unless someone is already on it. + // Added on the root level, because the lifetime of the new objective exceeds the lifetime of this objective. + RemoveFollowTarget(); + var approachArrestTarget = new AIObjectiveGoTo(Enemy, character, objectiveManager, repeat: false, getDivingGearIfNeeded: false, closeEnough: ArrestTargetDistance) + { + UsePathingOutside = false, + IgnoreIfTargetDead = true, + TargetName = Enemy.DisplayName, + AlwaysUseEuclideanDistance = false, + SpeakIfFails = false, + SourceObjective = this + }; + approachArrestTarget.Completed += OnArrestTargetReached; + objectiveManager.AddObjective(approachArrestTarget); + } + break; + } + } if (ShouldUnequipWeapon) { Unequip(); @@ -1424,11 +1499,23 @@ namespace Barotrauma } SteeringManager?.Reset(); } - + + public override void OnDeselected() + { + base.OnDeselected(); + if (character.TeamID == CharacterTeamType.FriendlyNPC && IsOffensiveOrArrest && (!AllowHoldFire || (hasAimed && holdFireTimer <= 0))) + { + // Remember that the target resisted or acted offensively (we've aimed or tried to arrest/attack) + Enemy.IsCriminal = true; + } + } + public override void Reset() { base.Reset(); hasAimed = false; + holdFireTimer = 0; + pathBackTimer = 0; isLethalWeapon = false; canSeeTarget = false; seekWeaponObjective = null; @@ -1436,20 +1523,43 @@ namespace Barotrauma retreatObjective = null; followTargetObjective = null; retreatTarget = null; + firstWarningTriggered = false; + lastWarningTriggered = false; } - private void SpeakNoWeapons() => Speak("dialogcombatnoweapons".ToIdentifier(), delay: 0, minDuration: 30); - private void SpeakRetreating() => Speak("dialogcombatretreating".ToIdentifier(), delay: Rand.Range(0f, 1f), minDuration: 20); - - private void Speak(Identifier textIdentifier, float delay, float minDuration) + /// + /// Speak that we don't have weapons. But only outside of friendly subs (not that relevant there, reduces spam). + /// + private void SpeakNoWeapons() { - if (character.IsOnPlayerTeam && !character.IsInFriendlySub) + if (!character.IsInFriendlySub) { - LocalizedString msg = TextManager.Get(textIdentifier); - if (!msg.IsNullOrEmpty()) - { - character.Speak(msg.Value, identifier: textIdentifier, delay: delay, minDurationBetweenSimilar: minDuration); - } + PlayerCrewSpeak("dialogcombatnoweapons".ToIdentifier(), delay: 0, minDurationBetweenSimilar: 30); + } + } + + private void PlayerCrewSpeak(Identifier textIdentifier, float delay, float minDurationBetweenSimilar) + { + if (character.IsOnPlayerTeam) + { + Speak(textIdentifier, delay, minDurationBetweenSimilar); + } + } + + private void FriendlyGuardSpeak(Identifier textIdentifier, float delay, float minDurationBetweenSimilar) + { + if (character.TeamID == CharacterTeamType.FriendlyNPC && character.IsSecurity) + { + Speak(textIdentifier, delay, minDurationBetweenSimilar); + } + } + + private void Speak(Identifier textIdentifier, float delay, float minDurationBetweenSimilar) + { + LocalizedString msg = TextManager.Get(textIdentifier); + if (!msg.IsNullOrEmpty()) + { + character.Speak(msg.Value, identifier: textIdentifier, delay: delay, minDurationBetweenSimilar: minDurationBetweenSimilar); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs index 53db54d77..f6d2702e2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -11,7 +11,7 @@ namespace Barotrauma class AIObjectiveContainItem: AIObjective { public override Identifier Identifier { get; set; } = "contain item".ToIdentifier(); - public override bool AllowWhileHandcuffed => false; + protected override bool AllowWhileHandcuffed => false; public Func GetItemPriority; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItem.cs index 80115f5c3..5818d9a10 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItem.cs @@ -6,9 +6,9 @@ namespace Barotrauma class AIObjectiveDeconstructItem : AIObjective { public override Identifier Identifier { get; set; } = "deconstruct item".ToIdentifier(); - public override bool AllowWhileHandcuffed => false; + protected override bool AllowWhileHandcuffed => false; - public override bool AllowInFriendlySubs => true; + protected override bool AllowInFriendlySubs => true; public readonly Item Item; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItems.cs index fbb83b3be..3b25c239e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItems.cs @@ -11,7 +11,7 @@ namespace Barotrauma //Clear periodically, because we may ending up ignoring items when all deconstructors are full protected override float IgnoreListClearInterval => 30; - public override bool AllowInFriendlySubs => true; + protected override bool AllowInFriendlySubs => true; protected override int MaxTargets => 10; @@ -47,7 +47,7 @@ namespace Barotrauma checkedDeconstructorExists = false; } - protected override float TargetEvaluation() + protected override float GetTargetPriority() { if (Targets.None()) { return 0; } if (objectiveManager.IsOrder(this)) @@ -57,7 +57,7 @@ namespace Barotrauma return AIObjectiveManager.RunPriority - 0.5f; } - protected override bool Filter(Item target) + protected override bool IsValidTarget(Item target) { // If the target was selected as a valid target, we'll have to accept it so that the objective can be completed. // The validity changes when a character picks the item up. diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs index f3a5c130c..4293dc327 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs @@ -9,7 +9,7 @@ namespace Barotrauma class AIObjectiveDecontainItem : AIObjective { public override Identifier Identifier { get; set; } = "decontain item".ToIdentifier(); - public override bool AllowWhileHandcuffed => false; + protected override bool AllowWhileHandcuffed => false; public Func GetItemPriority; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveEscapeHandcuffs.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveEscapeHandcuffs.cs index 0a2a7839d..995f8bb8b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveEscapeHandcuffs.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveEscapeHandcuffs.cs @@ -8,8 +8,8 @@ namespace Barotrauma // Used for prisoner escorts to allow them to escape their binds public override Identifier Identifier { get; set; } = "escape handcuffs".ToIdentifier(); public override bool AllowAutomaticItemUnequipping => true; - public override bool AllowOutsideSubmarine => true; - public override bool AllowInAnySub => true; + protected override bool AllowOutsideSubmarine => true; + protected override bool AllowInAnySub => true; private int escapeProgress; private bool isBeingWatched; @@ -28,7 +28,6 @@ namespace Barotrauma } public override bool CanBeCompleted => true; - public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace()); } protected override bool CheckObjectiveSpecific() => false; // escape timer is set to 60 by default to allow players to locate prisoners in time diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs index b3d97d251..302f7a462 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs @@ -10,12 +10,10 @@ namespace Barotrauma { public override Identifier Identifier { get; set; } = "extinguish fire".ToIdentifier(); public override bool ForceRun => true; - public override bool ConcurrentObjectives => true; + protected override bool ConcurrentObjectives => true; public override bool KeepDivingGearOn => true; - - public override bool AllowInAnySub => true; - - public override bool AllowWhileHandcuffed => false; + protected override bool AllowInAnySub => true; + protected override bool AllowWhileHandcuffed => false; private readonly Hull targetHull; @@ -32,7 +30,7 @@ namespace Barotrauma { if (!IsAllowed) { - HandleNonAllowed(); + HandleDisallowed(); return Priority; } bool isOrder = objectiveManager.HasOrder(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs index a11672bcd..af532273c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs @@ -10,13 +10,13 @@ namespace Barotrauma { public override Identifier Identifier { get; set; } = "extinguish fires".ToIdentifier(); public override bool ForceRun => true; - public override bool AllowInAnySub => true; + protected override bool AllowInAnySub => true; public AIObjectiveExtinguishFires(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { } - protected override bool Filter(Hull hull) => IsValidTarget(hull, character); + protected override bool IsValidTarget(Hull hull) => IsValidTarget(hull, character); - protected override float TargetEvaluation() => + protected override float GetTargetPriority() => // If any target is visible -> 100 priority Targets.Any(t => t == character.CurrentHull || HumanAIController.VisibleHulls.Contains(t)) ? 100 : // Else based on the fire severity diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs index ad90d4b31..f59030353 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; +using Barotrauma.Extensions; +using System.Collections.Generic; using System.Linq; -using Barotrauma.Extensions; namespace Barotrauma { @@ -9,19 +9,17 @@ namespace Barotrauma public override Identifier Identifier { get; set; } = "fight intruders".ToIdentifier(); protected override float IgnoreListClearInterval => 30; public override bool IgnoreUnsafeHulls => true; - protected override float TargetUpdateTimeMultiplier => 0.2f; - public bool TargetCharactersInOtherSubs { get; init; } - public AIObjectiveFightIntruders(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) + public AIObjectiveFightIntruders(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { } - protected override bool Filter(Character target) => IsValidTarget(target, character, TargetCharactersInOtherSubs); + protected override bool IsValidTarget(Character target) => IsValidTarget(target, character, TargetCharactersInOtherSubs); protected override IEnumerable GetList() => Character.CharacterList; - protected override float TargetEvaluation() + protected override float GetTargetPriority() { if (Targets.None()) { return 0; } if (!character.IsOnPlayerTeam && !character.IsOriginallyOnPlayerTeam) { return 100; } @@ -68,14 +66,14 @@ namespace Barotrauma if (HumanAIController.IsFriendly(character, target)) { return false; } if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; } if (!targetCharactersInOtherSubs) - { + { if (character.Submarine.TeamID != target.Submarine.TeamID && character.OriginalTeamID != target.Submarine.TeamID) { return false; } } if (target.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) { return false; } - if (target.IsArrested) { return false; } + if (target.IsHandcuffed && target.IsKnockedDown) { return false; } if (EnemyAIController.IsLatchedToSomeoneElse(target, character)) { 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 3cfecafbe..a395f72d6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs @@ -11,8 +11,8 @@ namespace Barotrauma public override string DebugTag => $"{Identifier} ({gearTag})"; public override bool ForceRun => true; public override bool KeepDivingGearOn => true; - public override bool AbandonWhenCannotCompleteSubjectives => false; - public override bool AllowWhileHandcuffed => false; + public override bool AbandonWhenCannotCompleteSubObjectives => false; + protected override bool AllowWhileHandcuffed => false; private readonly Identifier gearTag; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs index 6ef4b1e76..41e4add54 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -13,18 +13,16 @@ namespace Barotrauma public override bool ForceRun => true; public override bool KeepDivingGearOn => true; public override bool IgnoreUnsafeHulls => true; - public override bool ConcurrentObjectives => true; - public override bool AllowOutsideSubmarine => true; - public override bool AllowInAnySub => true; - public override bool AbandonWhenCannotCompleteSubjectives => false; - public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace()); } + protected override bool ConcurrentObjectives => true; + protected override bool AllowOutsideSubmarine => true; + protected override bool AllowInAnySub => true; + public override bool AbandonWhenCannotCompleteSubObjectives => false; - // TODO: expose? - const float priorityIncrease = 100; - const float priorityDecrease = 10; - const float SearchHullInterval = 3.0f; + private const float PriorityIncrease = 100; + private const float PriorityDecrease = 10; + private const float SearchHullInterval = 3.0f; - private float currenthullSafety; + private float currentHullSafety; private float searchHullTimer; @@ -111,15 +109,15 @@ namespace Barotrauma } if (character.CurrentHull == null) { - currenthullSafety = 0; + currentHullSafety = 0; } else { - currenthullSafety = HumanAIController.CurrentHullSafety; - if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD) + currentHullSafety = HumanAIController.CurrentHullSafety; + if (currentHullSafety > HumanAIController.HULL_SAFETY_THRESHOLD) { - Priority -= priorityDecrease * deltaTime; - if (currenthullSafety >= 100 && !character.IsLowInOxygen) + Priority -= PriorityDecrease * deltaTime; + if (currentHullSafety >= 100 && !character.IsLowInOxygen) { // Reduce the priority to zero so that the bot can get switch to other objectives immediately, e.g. when entering the airlock. Priority = 0; @@ -127,8 +125,8 @@ namespace Barotrauma } else { - float dangerFactor = (100 - currenthullSafety) / 100; - Priority += dangerFactor * priorityIncrease * deltaTime; + float dangerFactor = (100 - currentHullSafety) / 100; + Priority += dangerFactor * PriorityIncrease * deltaTime; } Priority = MathHelper.Clamp(Priority, 0, AIObjectiveManager.MaxObjectivePriority); } @@ -192,7 +190,7 @@ namespace Barotrauma } if (divingGearObjective == null || !divingGearObjective.CanBeCompleted) { - if (currenthullSafety < HumanAIController.HULL_SAFETY_THRESHOLD) + if (currentHullSafety < HumanAIController.HULL_SAFETY_THRESHOLD) { searchHullTimer = Math.Min(1, searchHullTimer); } @@ -231,7 +229,7 @@ namespace Barotrauma }, onCompleted: () => { - if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD || + if (currentHullSafety > HumanAIController.HULL_SAFETY_THRESHOLD || HumanAIController.NeedsDivingGear(currentHull, out bool needsSuit) && (needsSuit ? HumanAIController.HasDivingSuit(character) : HumanAIController.HasDivingMask(character))) { resetPriority = true; @@ -299,7 +297,7 @@ namespace Barotrauma } foreach (Character enemy in Character.CharacterList) { - if (!HumanAIController.IsActive(enemy) || HumanAIController.IsFriendly(enemy) || enemy.IsArrested) { continue; } + if (!HumanAIController.IsActive(enemy) || HumanAIController.IsFriendly(enemy) || enemy.IsHandcuffed) { continue; } if (HumanAIController.VisibleHulls.Contains(enemy.CurrentHull)) { Vector2 dir = character.Position - enemy.Position; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindThieves.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindThieves.cs index 2d7081475..bcec87ea1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindThieves.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindThieves.cs @@ -2,6 +2,7 @@ using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Linq; +using Barotrauma.Items.Components; namespace Barotrauma { @@ -13,17 +14,33 @@ namespace Barotrauma protected override float TargetUpdateTimeMultiplier => 1.0f; - const float DefaultInspectDistance = 200.0f; + /// + /// How long the round must have ran before NPCs can start doing inspections + /// (prevents "unfair" inspections you have no chance to react to if you happen to spawn right next to a security NPC with stolen items on you) + /// + private const float DelayOnRoundStart = 30.0f; + + private const float DefaultInspectDistance = 200.0f; + /// + /// Used when something is stolen and when the guards decide to inspect everyone. + /// + private const float ExtendedInspectDistance = 400.0f; + /// + /// Used when the target is tagged as a criminal (= suspective). + /// + private const float CriminalInspectDistance = 500.0f; + + private const float CriminalInspectProbability = 1.0f; /// /// How close the NPC must be to the target to the inspect them? You can use high values to make the NPC /// systematically go through targets no matter where they are, and low values to check targets they happen to come across. /// - public float InspectDistance = DefaultInspectDistance; + private float inspectDistance = DefaultInspectDistance; private float? overrideInspectProbability; /// - /// Chance of inspecting a valid target. The NPC won't try to inspect that target again for + /// Chance of inspecting a valid target. The NPC won't try to inspect that target again for /// regardless if the target is inspected or not. /// public float InspectProbability @@ -53,18 +70,25 @@ namespace Barotrauma /// When did the character last inspect whether some other character has stolen items on them? /// private static readonly Dictionary lastInspectionTimes = new Dictionary(); - - private readonly float inspectionInterval = 120.0f; - + + private const float NormalInspectionInterval = 120.0f; + private const float CriminalInspectionInterval = 30.0f; + public AIObjectiveFindThieves(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { } - protected override bool Filter(Character target) + protected override bool IsValidTarget(Character target) { + if (GameMain.GameSession is not { RoundDuration: > DelayOnRoundStart }) + { + return false; + } if (!IsValidTarget(target, character)) { return false; } - if (Vector2.DistanceSquared(target.WorldPosition, character.WorldPosition) > InspectDistance * InspectDistance) { return false; } + float inspectDist = target.IsCriminal ? CriminalInspectDistance : inspectDistance; + if (Vector2.DistanceSquared(target.WorldPosition, character.WorldPosition) > inspectDist * inspectDist) { return false; } if (lastInspectionTimes.TryGetValue(target, out double lastInspectionTime)) { + float inspectionInterval = target.IsCriminal ? CriminalInspectionInterval : NormalInspectionInterval; if (Timing.TotalTime < lastInspectionTime + inspectionInterval) { return false; @@ -75,8 +99,14 @@ namespace Barotrauma protected override IEnumerable GetList() => Character.CharacterList; - protected override float TargetEvaluation() + protected override float GetTargetPriority() { + if (character.IsClimbing) + { + // Don't inspect while climbing, because cannot grab while holding the ladders. + // Can lead to abandoning the objective when we need to climb the ladders to get to the target, but I think that's acceptable. + return 0; + } return subObjectives.Any() ? 50 : 0; } @@ -84,13 +114,14 @@ namespace Barotrauma { lastInspectionTimes.Clear(); overrideInspectProbability = 1.0f; - InspectDistance = DefaultInspectDistance * 2; + inspectDistance = ExtendedInspectDistance; } protected override AIObjective ObjectiveConstructor(Character target) { var checkStolenItemsObjective = new AIObjectiveCheckStolenItems(character, target, objectiveManager); - if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Unsynced) >= InspectProbability) + float probabity = target.IsCriminal ? CriminalInspectProbability : InspectProbability; + if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Unsynced) >= probabity) { checkStolenItemsObjective.ForceComplete(); lastInspectionTimes[target] = Timing.TotalTime; @@ -104,26 +135,30 @@ namespace Barotrauma public override void Update(float deltaTime) { base.Update(deltaTime); - if (checkVisibleStolenItemsTimer > 0.0f) + if (checkVisibleStolenItemsTimer > 0.0f || character.IsClimbing) { checkVisibleStolenItemsTimer -= deltaTime; return; } + if (character.SelectedSecondaryItem?.GetComponent() != null) + { + // Might be e.g. sitting on a chair. + character.SelectedSecondaryItem = null; + } foreach (var target in Character.CharacterList) { if (!IsValidTarget(target, character)) { continue; } //if we spot someone wearing or holding stolen items, immediately check them (with 100% chance of spotting the stolen items) - if (target.Inventory.AllItems.Any(it => it.SpawnedInCurrentOutpost && !it.AllowStealing && target.HasEquippedItem(it)) && + if (target.Inventory.AllItems.Any(it => it.Illegitimate && target.HasEquippedItem(it)) && character.CanSeeTarget(target, seeThroughWindows: true)) { AIObjectiveCheckStolenItems? existingObjective = - objectiveManager.GetActiveObjectives().FirstOrDefault(o => o.TargetCharacter == target); + objectiveManager.GetActiveObjectives().FirstOrDefault(o => o.Target == target); if (existingObjective == null) { objectiveManager.AddObjective(new AIObjectiveCheckStolenItems(character, target, objectiveManager)); lastInspectionTimes[target] = Timing.TotalTime; } - } } checkVisibleStolenItemsTimer = CheckVisibleStolenItemsInterval; @@ -140,7 +175,17 @@ namespace Barotrauma if (target.Submarine != character.Submarine) { return false; } //only player's crew can steal, ignore other teams if (!target.IsOnPlayerTeam) { return false; } - if (target.IsArrested) { return false; } + if (target.IsHandcuffed) { return false; } + // Ignore targets that are climbing, because might need to use ladders to get to them. + if (target.IsClimbing) { return false; } + if (HumanAIController.IsTrueForAnyBotInTheCrew(bot => + bot != HumanAIController && + ((bot.ObjectiveManager.GetActiveObjective() is AIObjectiveCheckStolenItems checkObj && checkObj.Target == target) || + (bot.ObjectiveManager.GetActiveObjective() is AIObjectiveCombat combatObj && combatObj.Enemy == target)))) + { + // Already being inspected by someone or fighting with someone in our team. + return false; + } return true; } @@ -148,5 +193,11 @@ namespace Barotrauma { lastInspectionTimes[target] = Timing.TotalTime; } + + public override void OnDeselected() + { + base.OnDeselected(); + character.DeselectCharacter(); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index a065627fd..8709d29a6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -12,9 +12,9 @@ namespace Barotrauma public override Identifier Identifier { get; set; } = "fix leak".ToIdentifier(); public override bool ForceRun => true; public override bool KeepDivingGearOn => true; - public override bool AllowInFriendlySubs => true; - public override bool AllowInAnySub => true; - public override bool AllowWhileHandcuffed => false; + protected override bool AllowInFriendlySubs => true; + protected override bool AllowInAnySub => true; + protected override bool AllowWhileHandcuffed => false; public Gap Leak { get; private set; } @@ -37,7 +37,7 @@ namespace Barotrauma { if (!IsAllowed) { - HandleNonAllowed(); + HandleDisallowed(); return Priority; } float coopMultiplier = 1; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs index dff7dc2ab..660332f56 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs @@ -9,7 +9,7 @@ namespace Barotrauma public override Identifier Identifier { get; set; } = "fix leaks".ToIdentifier(); public override bool ForceRun => true; public override bool KeepDivingGearOn => true; - public override bool AllowInFriendlySubs => true; + protected override bool AllowInFriendlySubs => true; private Hull PrioritizedHull { get; set; } @@ -18,7 +18,7 @@ namespace Barotrauma PrioritizedHull = prioritizedHull; } - protected override bool Filter(Gap gap) => IsValidTarget(gap, character); + protected override bool IsValidTarget(Gap gap) => IsValidTarget(gap, character); public static float GetLeakSeverity(Gap leak) { @@ -37,7 +37,7 @@ namespace Barotrauma } } - protected override float TargetEvaluation() + protected override float GetTargetPriority() { int totalLeaks = Targets.Count; if (totalLeaks == 0) { return 0; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index 717653432..0aad3d587 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -13,9 +13,9 @@ namespace Barotrauma { public override Identifier Identifier { get; set; } = "get item".ToIdentifier(); - public override bool AbandonWhenCannotCompleteSubjectives => false; + public override bool AbandonWhenCannotCompleteSubObjectives => false; public override bool AllowMultipleInstances => true; - public override bool AllowWhileHandcuffed => false; + protected override bool AllowWhileHandcuffed => false; public HashSet ignoredItems = new HashSet(); @@ -444,7 +444,7 @@ namespace Barotrauma } if (!AllowStealing && character.IsOnPlayerTeam) { - if (item.SpawnedInCurrentOutpost && !item.AllowStealing) { continue; } + if (item.Illegitimate) { continue; } } if (!CheckItem(item)) { continue; } if (item.Container != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs index afd5287e7..f35f6f96b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs @@ -13,7 +13,7 @@ namespace Barotrauma public override string DebugTag => $"{Identifier}"; public override bool KeepDivingGearOn => true; public override bool AllowMultipleInstances => true; - public override bool AllowWhileHandcuffed => false; + protected override bool AllowWhileHandcuffed => false; public bool AllowStealing { get; set; } public bool TakeWholeStack { get; set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index 06f4b7b4f..1fab5c816 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -16,7 +16,7 @@ namespace Barotrauma private readonly bool repeat; //how long until the path to the target is declared unreachable private float waitUntilPathUnreachable; - private bool getDivingGearIfNeeded; + private readonly bool getDivingGearIfNeeded; /// /// Doesn't allow the objective to complete if this condition is false @@ -34,11 +34,6 @@ namespace Barotrauma public bool DebugLogWhenFails { get; set; } = true; public bool UsePathingOutside { get; set; } = true; - /// - /// Which event action created this objective (if any) - /// - public EventAction SourceEventAction; - public float ExtraDistanceWhileSwimming; public float ExtraDistanceOutsideSub; private float _closeEnoughMultiplier = 1; @@ -94,10 +89,10 @@ namespace Barotrauma /// public bool UseDistanceRelativeToAimSourcePos { get; set; } = false; - public override bool AbandonWhenCannotCompleteSubjectives => false; + public override bool AbandonWhenCannotCompleteSubObjectives => false; - public override bool AllowOutsideSubmarine => AllowGoingOutside; - public override bool AllowInAnySub => true; + protected override bool AllowOutsideSubmarine => AllowGoingOutside; + protected override bool AllowInAnySub => true; public Identifier DialogueIdentifier { get; set; } = "dialogcannotreachtarget".ToIdentifier(); public LocalizedString TargetName { get; set; } @@ -287,10 +282,12 @@ namespace Barotrauma if (waitUntilPathUnreachable < 0) { waitUntilPathUnreachable = pathWaitingTime; - if (repeat) + if (repeat && !IsCompleted) { - SpeakCannotReach(); - return; + if (!IsDoneFollowing()) + { + SpeakCannotReach(); + } } else { @@ -374,16 +371,10 @@ namespace Barotrauma return; } } - if (repeat && IsCloseEnough) + if (IsDoneFollowing()) { - if (requiredCondition == null || requiredCondition()) - { - if (character.CanSeeTarget(Target) && (!character.IsClimbing || IsFollowOrder)) - { - OnCompleted(); - return; - } - } + OnCompleted(); + return; } float maxGapDistance = 500; Character targetCharacter = Target as Character; @@ -653,6 +644,21 @@ namespace Barotrauma character.SetInput(InputType.Aim, false, true); character.SetInput(InputType.Shoot, false, true); } + + bool IsDoneFollowing() + { + if (repeat && IsCloseEnough) + { + if (requiredCondition == null || requiredCondition()) + { + if (character.CanSeeTarget(Target) && (!character.IsClimbing || IsFollowOrder)) + { + return true; + } + } + } + return false; + } } private bool useScooter; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index 4845307ca..07343eb5c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -12,7 +12,7 @@ namespace Barotrauma { public override Identifier Identifier { get; set; } = "idle".ToIdentifier(); public override bool AllowAutomaticItemUnequipping => true; - public override bool AllowInAnySub => true; + protected override bool AllowInAnySub => true; private BehaviorType behavior; public BehaviorType Behavior @@ -91,8 +91,6 @@ namespace Barotrauma protected override bool CheckObjectiveSpecific() => false; public override bool CanBeCompleted => true; - public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace()); } - public readonly HashSet PreferredOutpostModuleTypes = new HashSet(); public void CalculatePriority(float max = 0) @@ -266,7 +264,7 @@ namespace Barotrauma if (node.Waypoint.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(node.Waypoint.CurrentHull)) { return false; } return true; //don't stop at ladders when idling - }, endNodeFilter: node => node.Waypoint.Ladders == null && (!isCurrentHullAllowed || !IsForbidden(node.Waypoint.CurrentHull))); + }, endNodeFilter: node => node.Waypoint.Stairs == null && node.Waypoint.Ladders == null && (!isCurrentHullAllowed || !IsForbidden(node.Waypoint.CurrentHull))); if (path.Unreachable) { //can't go to this room, remove it from the list and try another room @@ -299,7 +297,7 @@ namespace Barotrauma { PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1, nodeFilter: node => node.Waypoint.CurrentHull != null, - endNodeFilter: node => node.Waypoint.Ladders == null); + endNodeFilter: node => node.Waypoint.Ladders == null && node.Waypoint.Stairs == null); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveInspectNoises.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveInspectNoises.cs new file mode 100644 index 000000000..7295391b4 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveInspectNoises.cs @@ -0,0 +1,126 @@ +using System; + +namespace Barotrauma +{ + class AIObjectiveInspectNoises : AIObjective + { + public override Identifier Identifier { get; set; } = "inspect noises".ToIdentifier(); + + private AIObjectiveGoTo inspectNoiseObjective; + + /// + /// Initial priority of the objective to check noises made by enemies + /// + const float InspectNoisePriority = 10.0f; + /// + /// How much the priority of the objective to check noises made by enemies increases per noise + /// + const float InspectNoisePriorityIncrease = 10.0f; + private const float InspectNoiseInterval = 1.0f; + private float inspectNoiseTimer; + + /// + /// If the character is not currently inspecting the noise (= if some other objective is taking priority) + /// it forgets about it after this delay runs out. Otherwise they might unnecessarily go and inspect some + /// noise that was emitted a long time ago once done with the higher-prio objective. + /// + private const float InspectNoiseExpirationDelay = 60.0f; + private float inspectNoiseExpirationTimer = 0.0f; + + protected override float GetPriority() => inspectNoiseObjective?.Priority ?? 0.0f; + + public AIObjectiveInspectNoises(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) + : base(character, objectiveManager, priorityModifier) + { + inspectNoiseTimer = Rand.Range(0.0f, InspectNoiseInterval); + } + + public override void Update(float deltaTime) + { + base.Update(deltaTime); + inspectNoiseTimer -= deltaTime; + if (inspectNoiseTimer <= 0.0f) + { + CheckEnemyNoises(); + inspectNoiseTimer = InspectNoiseInterval; + } + //if we're not currently inspecting the noise (something else taking priority), forget about it after a while + if (inspectNoiseObjective != null && objectiveManager.GetActiveObjective() != inspectNoiseObjective) + { + inspectNoiseExpirationTimer += deltaTime; + if (inspectNoiseExpirationTimer > InspectNoiseExpirationDelay) + { + inspectNoiseObjective.Abandon = true; + } + } + } + + /// + /// Check if there's any loud provocative items used by enemies nearby (= if someone fired a gun somewhere), and go inspect them + /// + private void CheckEnemyNoises() + { + if (character.CurrentHull == null) { return; } + + //forget about inspecting if we're doing another subobjective (= fighting something) + if (inspectNoiseObjective != null && + CurrentSubObjective != inspectNoiseObjective) + { + inspectNoiseObjective.Abandon = true; + } + + foreach (var aiTarget in AITarget.List) + { + if (aiTarget.ShouldBeIgnored()) { continue; } + if (!aiTarget.IsWithinSector(character.WorldPosition)) { continue; } + if (aiTarget.Entity is not Item item) { continue; } + if (!item.HasTag(Tags.ProvocativeToHumanAI)) { continue; } + if (item.GetRootInventoryOwner() is Character targetCharacter && + AIObjectiveFightIntruders.IsValidTarget(targetCharacter, character, targetCharactersInOtherSubs: false)) + { + float dist = character.CurrentHull.GetApproximateDistance(character.Position, targetCharacter.Position, targetCharacter.CurrentHull, aiTarget.SoundRange, distanceMultiplierPerClosedDoor: 2); + if (dist * HumanAIController.Hearing > aiTarget.SoundRange) { continue; } + + character.Speak(TextManager.Get("dialogheardenemy").Value, identifier: "heardenemy".ToIdentifier(), minDurationBetweenSimilar: 30.0f); + if (inspectNoiseObjective != null && subObjectives.Contains(inspectNoiseObjective)) + { + //priority of inspecting noises increases with each noise + //but orders still remain a higher priority + inspectNoiseObjective.Priority = Math.Min(inspectNoiseObjective.Priority + InspectNoisePriorityIncrease, AIObjectiveManager.LowestOrderPriority - 1); + //only refresh the target if the character hasn't yet started inspecting the noise + //(if it has, it should not switch the target, otherwise you could e.g. bounce an NPC back and forth by firing guns at different sides of an outpost) + if (objectiveManager.GetActiveObjective() != inspectNoiseObjective && + inspectNoiseObjective.Target != targetCharacter.CurrentHull) + { + CreateInspectNoiseObjective(targetCharacter.CurrentHull, priority: inspectNoiseObjective.Priority); + } + } + else + { + CreateInspectNoiseObjective(targetCharacter.CurrentHull, priority: InspectNoisePriority); + } + } + } + + void CreateInspectNoiseObjective(ISpatialEntity target, float priority) + { + RemoveSubObjective(ref inspectNoiseObjective); + inspectNoiseObjective = new AIObjectiveGoTo(target, character, objectiveManager) + { + Priority = priority, + SourceObjective = this + }; + inspectNoiseObjective.Completed += () => { inspectNoiseObjective = null; inspectNoiseExpirationTimer = 0.0f; }; + inspectNoiseObjective.Abandoned += () => { inspectNoiseObjective = null; inspectNoiseExpirationTimer = 0.0f; }; + AddSubObjective(inspectNoiseObjective); + } + } + + protected override void Act(float deltaTime) + { + } + + protected override bool CheckObjectiveSpecific() => false; + + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs index 1150c17a3..3649eb11c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs @@ -1,4 +1,4 @@ -using Barotrauma.Extensions; +using Barotrauma.Extensions; using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; @@ -11,13 +11,8 @@ namespace Barotrauma class AIObjectiveLoadItem : AIObjective { public override Identifier Identifier { get; set; } = "load item".ToIdentifier(); - public override bool IsLoop - { - get => true; - set => throw new Exception("Trying to set the value for AIObjectiveLoadItem.IsLoop from: " + Environment.StackTrace.CleanupStackTrace()); - } - public override bool AllowWhileHandcuffed => false; + protected override bool AllowWhileHandcuffed => false; private AIObjectiveLoadItems.ItemCondition TargetItemCondition { get; } private Item Container { get; } @@ -163,7 +158,7 @@ namespace Barotrauma { if (!IsAllowed) { - HandleNonAllowed(); + HandleDisallowed(); return Priority; } else if (!AIObjectiveLoadItems.IsValidTarget(Container, character, targetCondition: TargetItemCondition)) @@ -298,12 +293,14 @@ namespace Barotrauma if (item.Removed) { return false; } if (!ValidContainableItemIdentifiers.Contains(item.Prefab.Identifier)) { return false; } if (ignoredItems.Contains(item)) { return false; } + if ((item.Illegitimate) == character.IsOnPlayerTeam) { return false; } if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; } - var rootInventoryOwner = item.GetRootInventoryOwner(); - if (rootInventoryOwner is Character owner && owner != character) { return false; } - if (rootInventoryOwner is Item parentItem) + if (item.GetRootInventoryOwner() is Character owner && owner != character) { return false; } + Item parentItem = item.Container; + while (parentItem != null) { if (parentItem.HasTag(Tags.DontTakeItems)) { return false; } + parentItem = parentItem.Container; } if (!item.HasAccess(character)) { return false; } if (!character.HasItem(item) && !CanEquip(item, allowWearing: false)) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs index eeee31b23..185d057b1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs @@ -50,7 +50,7 @@ namespace Barotrauma TargetCondition = option == "turretammo" ? ItemCondition.Empty : ItemCondition.Full; } - protected override bool Filter(Item target) + protected override bool IsValidTarget(Item target) { //don't pass TargetContainerTags to the method (no need to filter by tags anymore, it's already done when populating TargetContainers) if (!IsValidTarget(target, character, null, TargetCondition)) { return false; } @@ -104,7 +104,7 @@ namespace Barotrauma protected override void OnObjectiveCompleted(AIObjective objective, Item target) => HumanAIController.RemoveTargets(character, target); - protected override float TargetEvaluation() + protected override float GetTargetPriority() { if (Targets.None()) { return 0; } if (objectiveManager.IsOrder(this)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs index 2a939ab76..04cc059e3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs @@ -4,21 +4,34 @@ using Microsoft.Xna.Framework; namespace Barotrauma { + /// + /// An objective that creates specific kinds of subobjectives for specific types of targets, and loops through those targets. + /// For example, a cleanup objective that loops through items that need to be cleaned up, or a "fix leaks" objective that loops through leaks that need welding. + /// abstract class AIObjectiveLoop : AIObjective { public HashSet Targets { get; private set; } = new HashSet(); public Dictionary Objectives { get; private set; } = new Dictionary(); protected HashSet ignoreList = new HashSet(); - private float ignoreListTimer; + private float ignoreListClearTimer; protected float targetUpdateTimer; protected virtual float TargetUpdateTimeMultiplier { get; } = 1; + /// + /// How often are the subobjectives synced based on the available targets? + /// private float syncTimer; private readonly float syncTime = 1; - // By default, doesn't clear the list automatically + /// + /// By default, doesn't clear the list automatically + /// protected virtual float IgnoreListClearInterval => 0; + /// + /// Contains targets that anyone in the same crew has reported about. Used for automatic the target has to be reported before it can be can be targeted, so characters don't magically know where e.g. enemies are. + /// Ignored on orders: a bot explicitly ordered to repair leaks or fight intruders can find targets that haven't been reported. + /// public HashSet ReportedTargets { get; private set; } = new HashSet(); public bool AddTarget(T target) @@ -28,7 +41,7 @@ namespace Barotrauma { return false; } - if (Filter(target)) + if (IsValidTarget(target)) { ReportedTargets.Add(target); return true; @@ -42,24 +55,27 @@ namespace Barotrauma protected override void Act(float deltaTime) { } protected override bool CheckObjectiveSpecific() => false; public override bool CanBeCompleted => true; - public override bool AbandonWhenCannotCompleteSubjectives => false; + public override bool AbandonWhenCannotCompleteSubObjectives => false; public override bool AllowSubObjectiveSorting => true; - public override bool AllowWhileHandcuffed => false; + protected override bool AllowWhileHandcuffed => false; + protected override bool AbandonIfDisallowed => false; - public virtual bool InverseTargetEvaluation => false; + /// + /// Makes the priority inversely proportional to the value returned by . + /// In other words, gives this objective a high priority when priority of the targets is low. + /// + public virtual bool InverseTargetPriority => false; protected virtual bool ResetWhenClearingIgnoreList => true; protected virtual bool ForceOrderPriority => true; protected virtual int MaxTargets => int.MaxValue; - public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace.CleanupStackTrace()); } - public override void Update(float deltaTime) { base.Update(deltaTime); if (IgnoreListClearInterval > 0) { - if (ignoreListTimer > IgnoreListClearInterval) + if (ignoreListClearTimer > IgnoreListClearInterval) { if (ResetWhenClearingIgnoreList) { @@ -68,12 +84,12 @@ namespace Barotrauma else { ignoreList.Clear(); - ignoreListTimer = 0; + ignoreListClearTimer = 0; } } else { - ignoreListTimer += deltaTime; + ignoreListClearTimer += deltaTime; } } if (targetUpdateTimer <= 0) @@ -104,14 +120,17 @@ namespace Barotrauma } } - // the timer is set between 1 and 10 seconds, depending on the priority modifier and a random +-25% + // + /// + /// The timer is set between 1 and 10 seconds, depending on the priority modifier and a random +-25% + /// private float CalculateTargetUpdateTimer() => targetUpdateTimer = 1 / MathHelper.Clamp(PriorityModifier * Rand.Range(0.75f, 1.25f), 0.1f, 1) * TargetUpdateTimeMultiplier; public override void Reset() { base.Reset(); ignoreList.Clear(); - ignoreListTimer = 0; + ignoreListClearTimer = 0; UpdateTargets(); } @@ -119,25 +138,25 @@ namespace Barotrauma { if (!IsAllowed) { - HandleNonAllowed(); + HandleDisallowed(); return Priority; } // Allow the target value to be more than 100. - float targetValue = TargetEvaluation(); - if (InverseTargetEvaluation) + float targetPriority = GetTargetPriority(); + if (InverseTargetPriority) { - targetValue = 100 - targetValue; + targetPriority = 100 - targetPriority; } var currentSubObjective = CurrentSubObjective; - if (currentSubObjective != null && currentSubObjective.Priority > targetValue) + if (currentSubObjective != null && currentSubObjective.Priority > targetPriority) { // If the priority is higher than the target value, let's just use it. // The priority calculation is more precise, but it takes into account things like distances, // so it's better not to use it if it's lower than the rougher targetValue. - targetValue = currentSubObjective.Priority; + targetPriority = currentSubObjective.Priority; } // If the target value is less than 1% of the max value, let's just treat it as zero. - if (targetValue < 1) + if (targetPriority < 1) { Priority = 0; } @@ -145,7 +164,7 @@ namespace Barotrauma { if (objectiveManager.IsOrder(this)) { - Priority = ForceOrderPriority ? objectiveManager.GetOrderPriority(this) : targetValue; + Priority = ForceOrderPriority ? objectiveManager.GetOrderPriority(this) : targetPriority; } else { @@ -155,7 +174,7 @@ namespace Barotrauma // Allow higher prio max = AIObjectiveManager.EmergencyObjectivePriority; } - float value = MathHelper.Clamp((CumulatedDevotion + (targetValue * PriorityModifier)) / 100, 0, 1); + float value = MathHelper.Clamp((CumulatedDevotion + (targetPriority * PriorityModifier)) / 100, 0, 1); Priority = MathHelper.Lerp(0, max, value); } } @@ -181,7 +200,7 @@ namespace Barotrauma bool ignore = this is AIObjectiveChargeBatteries || this is AIObjectivePumpWater || this is AIObjectiveFindThieves; if (!ignore && !ReportedTargets.Contains(target)) { continue; } } - if (!Filter(target)) { continue; } + if (!IsValidTarget(target)) { continue; } if (!ignoreList.Contains(target)) { Targets.Add(target); @@ -228,9 +247,13 @@ namespace Barotrauma /// protected abstract IEnumerable GetList(); - protected abstract float TargetEvaluation(); + /// + /// Returns a priority value based on the current targets (e.g. high prio when there's lots of severe fires or leaks). + /// The priority of this objective is based on the target priority. + /// + protected abstract float GetTargetPriority(); protected abstract AIObjective ObjectiveConstructor(T target); - protected abstract bool Filter(T target); + protected abstract bool IsValidTarget(T target); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index fa3b1d731..e59e3e0fb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -20,10 +20,27 @@ namespace Barotrauma MaxValue = 2 } + /// + /// Highest possible priority for any objective. Used in certain cases where the character needs to react immediately to survive, + /// such as finding a suit when under pressure or getting out of a burning room. + /// public const float MaxObjectivePriority = 100; + /// + /// Priority of objectives such as finding safety, rescuing someone in a critical state or defending against an attacker + /// (= objectives that are critical for saving the character's or someone else's life) + /// public const float EmergencyObjectivePriority = 90; + /// + /// Maximum priority of an order given to the character (forced order, or the leftmost order in the crew list) + /// public const float HighestOrderPriority = 70; + /// + /// Maximum priority of an order given to the character (rightmost order in the crew list) + /// public const float LowestOrderPriority = 60; + /// + /// Objectives with a priority equal to or higher than this make the character run. + /// public const float RunPriority = 50; // Constantly increases the priority of the selected objective, unless overridden public const float baseDevotion = 5; @@ -129,6 +146,7 @@ namespace Barotrauma return; #endif } + foreach (var delayedObjective in DelayedObjectives) { CoroutineManager.StopCoroutines(delayedObjective.Value); @@ -150,36 +168,46 @@ namespace Barotrauma AddObjective(newIdleObjective); int objectiveCount = Objectives.Count; - foreach (var autonomousObjective in character.Info.Job.Prefab.AutonomousObjectives) + if (character.Info?.Job != null) { - var orderPrefab = OrderPrefab.Prefabs[autonomousObjective.Identifier]; - if (orderPrefab == null) { throw new Exception($"Could not find a matching prefab by the identifier: '{autonomousObjective.Identifier}'"); } - Item item = null; - if (orderPrefab.MustSetTarget) + foreach (var autonomousObjective in character.Info.Job.Prefab.AutonomousObjectives) { - item = orderPrefab.GetMatchingItems(character.Submarine, mustBelongToPlayerSub: false, requiredTeam: character.Info.TeamID, interactableFor: character)?.GetRandomUnsynced(); - } - var order = new Order(orderPrefab, autonomousObjective.Option, item ?? character.CurrentHull as Entity, orderPrefab.GetTargetItemComponent(item), orderGiver: character); - if (order == null) { continue; } - if ((order.IgnoreAtOutpost || autonomousObjective.IgnoreAtOutpost) && - Level.IsLoadedFriendlyOutpost && character.TeamID != CharacterTeamType.FriendlyNPC && !character.IsFriendlyNPCTurnedHostile) - { - if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID)) + var orderPrefab = OrderPrefab.Prefabs[autonomousObjective.Identifier] ?? throw new Exception($"Could not find a matching prefab by the identifier: '{autonomousObjective.Identifier}'"); + Item item = null; + if (orderPrefab.MustSetTarget) + { + item = orderPrefab.GetMatchingItems(character.Submarine, mustBelongToPlayerSub: false, requiredTeam: character.Info.TeamID, interactableFor: character)?.GetRandomUnsynced(); + } + var order = new Order(orderPrefab, autonomousObjective.Option, item ?? character.CurrentHull as Entity, orderPrefab.GetTargetItemComponent(item), orderGiver: character); + if (order == null) { continue; } + if ((order.IgnoreAtOutpost || autonomousObjective.IgnoreAtOutpost) && + Level.IsLoadedFriendlyOutpost && character.TeamID != CharacterTeamType.FriendlyNPC && !character.IsFriendlyNPCTurnedHostile) + { + if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID)) + { + continue; + } + } + if (autonomousObjective.IgnoreAtNonOutpost && !Level.IsLoadedFriendlyOutpost) { continue; } - } - if (autonomousObjective.IgnoreAtNonOutpost && !Level.IsLoadedFriendlyOutpost) - { - continue; - } - var objective = CreateObjective(order, autonomousObjective.PriorityModifier); - if (objective != null && objective.CanBeCompleted) - { - AddObjective(objective, delay: Rand.Value() / 2); - objectiveCount++; - } - } + var objective = CreateObjective(order, autonomousObjective.PriorityModifier); + if (objective != null && objective.CanBeCompleted) + { + AddObjective(objective, delay: Rand.Value() / 2); + objectiveCount++; + } + } + } + else + { + string warningMsg = character.Info == null ? + $"The character {character.DisplayName} has been set to use human ai, but has no {nameof(CharacterInfo)}. This may cause issues with the AI. Consider adding {nameof(CharacterPrefab.HasCharacterInfo)}=\"True\" to the character config." : + $"The character {character.DisplayName} has been set to use human ai, but has no job. This may cause issues with the AI. Consider configuring some jobs for the character type."; + DebugConsole.AddWarning(warningMsg, character.Prefab.ContentPackage); + } + _waitTimer = Math.Max(_waitTimer, Rand.Range(0.5f, 1f) * objectiveCount); } @@ -492,7 +520,6 @@ namespace Barotrauma if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; } newObjective = new AIObjectiveOperateItem(targetPump, character, this, order.Option, false, priorityModifier: priorityModifier) { - IsLoop = false, Override = order.OrderGiver is { IsCommanding: true } }; newObjective.Completed += () => DismissSelf(order); @@ -522,7 +549,7 @@ namespace Barotrauma newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, order.Option, requireEquip: false, useController: order.UseController, controller: order.ConnectedController, priorityModifier: priorityModifier) { - IsLoop = true, + Repeat = true, // Don't override unless it's an order by a player Override = order.OrderGiver != null && order.OrderGiver.IsCommanding }; @@ -530,7 +557,6 @@ namespace Barotrauma case "setchargepct": newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, order.Option, false, priorityModifier: priorityModifier) { - IsLoop = false, Override = !character.IsDismissed, completionCondition = () => { @@ -594,7 +620,6 @@ namespace Barotrauma { prepareObjective = new AIObjectivePrepare(character, this, order.GetTargetItems(order.Option), order.RequireItems) { - KeepActiveWhenReady = false, CheckInventory = false, EvaluateCombatPriority = true, FindAllItems = false, @@ -612,13 +637,16 @@ namespace Barotrauma case "deconstructitems": newObjective = new AIObjectiveDeconstructItems(character, this, priorityModifier); break; + case "inspectnoises": + newObjective = new AIObjectiveInspectNoises(character, this, priorityModifier); + break; default: if (order.TargetItemComponent == null) { return null; } if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; } newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, order.Option, requireEquip: false, useController: order.UseController, controller: order.ConnectedController, priorityModifier: priorityModifier) { - IsLoop = true, + Repeat = true, // Don't override unless it's an order by a player Override = order.OrderGiver != null && order.OrderGiver.IsCommanding }; @@ -680,10 +708,16 @@ namespace Barotrauma /// Only checks the current order. Deprecated, use pattern matching instead. ///
public bool IsCurrentOrder() where T : AIObjective => CurrentOrder is T; + /// /// Checks the current objective (which can be an order too). Deprecated, use pattern matching instead. /// public bool IsCurrentObjective() where T : AIObjective => CurrentObjective is T; + + /// + /// Checks if any objectives or orders are of the specified type. Regardless of whether the objective is active or inactive. + /// + public bool HasObjectiveOrOrder() where T : AIObjective => Objectives.Any(o => o is T) || HasOrder(); public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective(); @@ -696,10 +730,23 @@ namespace Barotrauma /// Return the first order with the specified objective. Can return null. ///
public Order GetOrder(AIObjective objective) => CurrentOrders.FirstOrDefault(o => o.Objective == objective); - + + /// + /// Returns the last active objective of the specified objective type. + /// Should generally be used to get the active objective (or subobjective) of objectives that don't sort their subobjectives by priority (see . + /// + /// The last active objective of the specified type if found. + /// public T GetLastActiveObjective() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T; - + + /// + /// Returns the first active objective of the specified objective type. + /// Should generally be used to get the active objective (or subobjective) of objectives that sort their subobjectives by priority, such as those that inherit . + /// + /// + /// The first active objective of the specified type if found. + /// public T GetFirstActiveObjective() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).FirstOrDefault(so => so is T) as T; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs index d9bfcfe0f..be6674906 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -13,8 +13,8 @@ namespace Barotrauma public override bool AllowAutomaticItemUnequipping => true; public override bool AllowMultipleInstances => true; - public override bool AllowInAnySub => true; - public override bool AllowWhileHandcuffed => false; + protected override bool AllowInAnySub => true; + protected override bool AllowWhileHandcuffed => false; public override bool PrioritizeIfSubObjectivesActive => component != null && (component is Reactor || component is Turret); private readonly ItemComponent component, controller; @@ -29,7 +29,12 @@ namespace Barotrauma ///
public Func EndNodeFilter; - public bool Override { get; set; } = true; + public bool Override { get; init; } = true; + + /// + /// When true, the operate objective is never completed, unless it's abandoned. + /// + public bool Repeat { get; init; } public override bool CanBeCompleted => base.CanBeCompleted && (!useController || controller != null); @@ -50,7 +55,7 @@ namespace Barotrauma bool isOrder = objectiveManager.IsOrder(this); if (!IsAllowed) { - HandleNonAllowed(); + HandleDisallowed(); return Priority; } if (!isOrder && component.Item.ConditionPercentage <= 0) @@ -307,7 +312,7 @@ namespace Barotrauma } } - protected override bool CheckObjectiveSpecific() => isDoneOperating && !IsLoop; + protected override bool CheckObjectiveSpecific() => isDoneOperating && !Repeat; public override void Reset() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs index 180d0e963..95ea194d8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs @@ -13,7 +13,7 @@ namespace Barotrauma public override bool KeepDivingGearOn => true; public override bool KeepDivingGearOnAlsoWhenInactive => true; public override bool PrioritizeIfSubObjectivesActive => true; - public override bool AllowWhileHandcuffed => false; + protected override bool AllowWhileHandcuffed => false; private AIObjectiveGetItem getSingleItemObjective; private AIObjectiveGetItems getAllItemsObjective; @@ -22,7 +22,6 @@ namespace Barotrauma private readonly Item targetItem; private readonly ImmutableArray requiredItems; private readonly ImmutableArray optionalItems; - private readonly HashSet items = new HashSet(); public bool KeepActiveWhenReady { get; set; } public bool CheckInventory { get; set; } public bool FindAllItems { get; set; } @@ -61,12 +60,12 @@ namespace Barotrauma { if (!IsAllowed) { - HandleNonAllowed(); + HandleDisallowed(); return Priority; } Priority = objectiveManager.GetOrderPriority(this); var subObjective = GetSubObjective(); - if (subObjective != null && subObjective.IsCompleted) + if (subObjective is { IsCompleted: true }) { Priority = 0; } @@ -113,20 +112,7 @@ namespace Barotrauma }, onCompleted: () => { - if (KeepActiveWhenReady) - { - if (objectiveReference != null) - { - foreach (var item in objectiveReference.achievedItems) - { - if (item?.IsOwnedBy(character) != null) - { - items.Add(item); - } - } - } - } - else + if (!KeepActiveWhenReady) { IsCompleted = true; } @@ -165,22 +151,11 @@ namespace Barotrauma if (!TryAddSubObjective(ref getSingleItemObjective, getItemConstructor, onCompleted: () => { - if (KeepActiveWhenReady) - { - if (getSingleItemObjective != null) - { - var item = getSingleItemObjective?.TargetItem; - if (item?.IsOwnedBy(character) != null) - { - items.Add(item); - } - } - } - else + if (!KeepActiveWhenReady) { IsCompleted = true; } - }, + }, onAbandon: () => Abandon = true)) { Abandon = true; @@ -193,7 +168,6 @@ namespace Barotrauma public override void Reset() { base.Reset(); - items.Clear(); subObjectivesCreated = false; getMultipleItemsObjective = null; getSingleItemObjective = null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs index 80392c641..6c1a7a37b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs @@ -12,7 +12,7 @@ namespace Barotrauma public override Identifier Identifier { get; set; } = "pump water".ToIdentifier(); public override bool KeepDivingGearOn => true; public override bool AllowAutomaticItemUnequipping => true; - public override bool AllowWhileHandcuffed => false; + protected override bool AllowWhileHandcuffed => false; private List pumpList; @@ -25,7 +25,7 @@ namespace Barotrauma base.FindTargets(); } - protected override bool Filter(Pump pump) + protected override bool IsValidTarget(Pump pump) { if (pump?.Item == null || pump.Item.Removed) { return false; } if (pump.Item.IgnoreByAI(character)) { return false; } @@ -62,7 +62,7 @@ namespace Barotrauma return pumpList; } - protected override float TargetEvaluation() + protected override float GetTargetPriority() { if (Targets.None()) { return 0; } if (Option == "stoppumping") @@ -90,7 +90,6 @@ namespace Barotrauma protected override AIObjective ObjectiveConstructor(Pump pump) => new AIObjectiveOperateItem(pump, character, objectiveManager, Option, false) { - IsLoop = false, completionCondition = () => IsReady(pump) }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs index e9594d864..cb4becf62 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -10,9 +10,9 @@ namespace Barotrauma { public override Identifier Identifier { get; set; } = "repair item".ToIdentifier(); - public override bool AllowInFriendlySubs => true; + protected override bool AllowInFriendlySubs => true; public override bool KeepDivingGearOn => Item?.CurrentHull == null; - public override bool AllowWhileHandcuffed => false; + protected override bool AllowWhileHandcuffed => false; public Item Item { get; private set; } @@ -37,7 +37,7 @@ namespace Barotrauma protected override float GetPriority() { - if (!IsAllowed) { HandleNonAllowed(); } + if (!IsAllowed) { HandleDisallowed(); } if (Item.IgnoreByAI(character)) { Abandon = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs index 1403c14ae..3cd5544b2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs @@ -19,9 +19,9 @@ namespace Barotrauma public Item PrioritizedItem { get; private set; } public override bool AllowMultipleInstances => true; - public override bool AllowInFriendlySubs => true; + protected override bool AllowInFriendlySubs => true; - public readonly static float RequiredSuccessFactor = 0.4f; + public const float RequiredSuccessFactor = 0.4f; public override bool IsDuplicate(T otherObjective) => otherObjective is AIObjectiveRepairItems repairObjective && objectiveManager.IsOrder(repairObjective) == objectiveManager.IsOrder(this); @@ -62,7 +62,7 @@ namespace Barotrauma } } - protected override bool Filter(Item item) + protected override bool IsValidTarget(Item item) { if (!ViableForRepair(item, character, HumanAIController)) { return false; }; if (!Objectives.ContainsKey(item)) @@ -94,7 +94,7 @@ namespace Barotrauma return item.Repairables.All(r => !r.IsBelowRepairThreshold); } - protected override float TargetEvaluation() + protected override float GetTargetPriority() { var selectedItem = character.SelectedItem; if (selectedItem != null && AIObjectiveRepairItem.IsRepairing(character, selectedItem) && selectedItem.ConditionPercentage < 100) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index d62bb4e2e..2d6c385d1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -13,10 +13,9 @@ namespace Barotrauma public override Identifier Identifier { get; set; } = "rescue".ToIdentifier(); public override bool ForceRun => true; public override bool KeepDivingGearOn => true; - - public override bool AllowOutsideSubmarine => true; - public override bool AllowInAnySub => true; - public override bool AllowWhileHandcuffed => false; + protected override bool AllowOutsideSubmarine => true; + protected override bool AllowInAnySub => true; + protected override bool AllowWhileHandcuffed => false; const float TreatmentDelay = 0.5f; @@ -484,7 +483,7 @@ namespace Barotrauma protected override float GetPriority() { if (Target == null) { Abandon = true; } - if (!IsAllowed) { HandleNonAllowed(); } + if (!IsAllowed) { HandleDisallowed(); } if (Abandon) { return Priority; @@ -531,8 +530,8 @@ namespace Barotrauma public override void OnDeselected() { - character.SelectedCharacter = null; base.OnDeselected(); + character.DeselectCharacter(); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs index 4bfadb3f5..e27ce1933 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs @@ -9,9 +9,9 @@ namespace Barotrauma { public override Identifier Identifier { get; set; } = "rescue all".ToIdentifier(); public override bool ForceRun => true; - public override bool InverseTargetEvaluation => true; - public override bool AllowOutsideSubmarine => true; - public override bool AllowInAnySub => true; + public override bool InverseTargetPriority => true; + protected override bool AllowOutsideSubmarine => true; + protected override bool AllowInAnySub => true; private readonly HashSet charactersWithMinorInjuries = new HashSet(); @@ -32,7 +32,7 @@ namespace Barotrauma public AIObjectiveRescueAll(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { } - protected override bool Filter(Character target) + protected override bool IsValidTarget(Character target) { if (!IsValidTarget(target, character, out bool ignoredasMinorWounds)) { @@ -61,7 +61,7 @@ namespace Barotrauma protected override IEnumerable GetList() => Character.CharacterList; - protected override float TargetEvaluation() + protected override float GetTargetPriority() { if (Targets.None()) { return 100; } if (!objectiveManager.IsOrder(this)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs index ee7109ff5..c580a2d0c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs @@ -12,8 +12,8 @@ namespace Barotrauma private AIObjectiveGoTo moveInsideObjective, moveOutsideObjective; private bool usingEscapeBehavior, isSteeringThroughGap; - public override bool AllowOutsideSubmarine => true; - public override bool AllowInAnySub => true; + protected override bool AllowOutsideSubmarine => true; + protected override bool AllowInAnySub => true; public AIObjectiveReturn(Character character, Character orderGiver, AIObjectiveManager objectiveManager, float priorityModifier = 1.0f) : base(character, objectiveManager, priorityModifier) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs index d5f90c438..a859d0c50 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs @@ -178,7 +178,6 @@ namespace Barotrauma public PetBehavior(XElement element, EnemyAIController aiController) { AIController = aiController; - AIController.Character.CanBeDragged = true; MaxHappiness = element.GetAttributeFloat(nameof(MaxHappiness), 100.0f); UnhappyThreshold = element.GetAttributeFloat(nameof(UnhappyThreshold), MaxHappiness * 0.25f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs index 99d9e2fd1..6c74b71b7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs @@ -75,7 +75,7 @@ namespace Barotrauma public void Update(float deltaTime) { - if (!Active || character.IsArrested) { return; } + if (!Active || character.IsHandcuffed) { return; } decisionTimer -= deltaTime; if (decisionTimer <= 0.0f) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs index a2c281159..c2299f4cd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs @@ -1,4 +1,4 @@ -using Barotrauma.Extensions; +using Barotrauma.Extensions; using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System.Collections.Generic; @@ -109,7 +109,7 @@ namespace Barotrauma private static IEnumerable GetThalamusEntities(Submarine wreck, Identifier tag) => MapEntity.MapEntityList.Where(e => e.Submarine == wreck && e.Prefab != null && IsThalamus(e.Prefab, tag)); - private static bool IsThalamus(MapEntityPrefab entityPrefab, Identifier tag) => entityPrefab.HasSubCategory("thalamus") || entityPrefab.Tags.Contains(tag); + public static bool IsThalamus(MapEntityPrefab entityPrefab, Identifier tag) => entityPrefab.HasSubCategory("thalamus") || entityPrefab.Tags.Contains(tag); public static WreckAI Create(Submarine wreck) { @@ -213,7 +213,7 @@ namespace Barotrauma foreach (Item item in thalamusItems) { // Ensure that thalamus items are visible - item.HiddenInGame = false; + item.IsLayerHidden = false; if (item.HasTag(Config.Spawner)) { if (!spawnOrgans.Contains(item)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index c32e754fd..fa20dd94c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -154,7 +154,10 @@ namespace Barotrauma Collider.SetTransformIgnoreContacts(mainLimb.SimPosition, mainLimb.Rotation); //reset pull joints to prevent the character from "hanging" mid-air if pull joints had been active when the character was still moving //(except when dragging, then we need the pull joints) - if (!character.CanBeDragged || character.SelectedBy == null) { ResetPullJoints(); } + if (!Draggable || character.SelectedBy == null) + { + ResetPullJoints(); + } } if (character.IsDead && deathAnimTimer < deathAnimDuration) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index ba6f7eb5a..6024c582a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -309,7 +309,10 @@ namespace Barotrauma Collider.SetTransformIgnoreContacts(MainLimb.SimPosition, MainLimb.Rotation); //reset pull joints to prevent the character from "hanging" mid-air if pull joints had been active when the character was still moving //(except when dragging, then we need the pull joints) - if (!character.CanBeDragged || character.SelectedBy == null) { ResetPullJoints(); } + if (!Draggable || character.SelectedBy == null) + { + ResetPullJoints(); + } } return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index 8d75301d4..9ce0e88a9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -1096,8 +1096,8 @@ namespace Barotrauma if (newHull?.Submarine == null && currentHull?.Submarine != null) { //don't teleport out yet if the character is going through a gap - if (Gap.FindAdjacent(Gap.GapList.Where(g => g.Submarine == currentHull.Submarine), findPos, 150.0f) != null) { return; } - if (Limbs.Any(l => Gap.FindAdjacent(currentHull.ConnectedGaps, l.WorldPosition, ConvertUnits.ToDisplayUnits(l.body.GetSize().Combine())) != null)) { return; } + if (Gap.FindAdjacent(Gap.GapList.Where(g => g.Submarine == currentHull.Submarine), findPos, 150.0f, allowRoomToRoom: true) != null) { return; } + if (Limbs.Any(l => Gap.FindAdjacent(currentHull.ConnectedGaps, l.WorldPosition, ConvertUnits.ToDisplayUnits(l.body.GetSize().Combine()), allowRoomToRoom: true) != null)) { return; } character.MemLocalState?.Clear(); Teleport(ConvertUnits.ToSimUnits(currentHull.Submarine.Position), currentHull.Submarine.Velocity); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 374d89279..abe6bd9ae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -1,17 +1,17 @@ -using Barotrauma.Networking; +using Barotrauma.Abilities; +using Barotrauma.Extensions; +using Barotrauma.IO; +using Barotrauma.Items.Components; +using Barotrauma.Networking; using FarseerPhysics; +using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; -using Barotrauma.IO; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; using System.Linq; using System.Xml.Linq; -using Barotrauma.Items.Components; -using FarseerPhysics.Dynamics; -using Barotrauma.Extensions; -using System.Collections.Immutable; -using Barotrauma.Abilities; -using System.Diagnostics; #if SERVER using System.Text; #endif @@ -351,6 +351,15 @@ namespace Barotrauma public bool IsInstigator => CombatAction is { IsInstigator: true }; + /// + /// Do the outpost security officers treat the character as a criminal? + /// Triggers when the character has either committed a major crime or resisted being arrested (or fled). + /// Only affects the reactions of friendly NPCs in the outposts. + /// The NPCs still don't react immediately to "criminals", but take this into account when the character next time does something wrong. + /// The consequences are that the guards will not hold fire and will not give more warnings before attacking. + /// + public bool IsCriminal; + /// /// Set true only, if the character is turned hostile from an escort mission (See ). /// @@ -795,15 +804,27 @@ namespace Barotrauma } private float obstructVisionAmount; + public float ObstructVisionAmount + { + get { return obstructVisionAmount; } + set + { + obstructVisionAmount = MathHelper.Clamp(value, 0.0f, 1.0f); + } + } + + /// + /// Provided for backwards compatibility: use instead. + /// public bool ObstructVision { get { - return obstructVisionAmount > 0.5f; + return obstructVisionAmount > 0.01f; } set { - obstructVisionAmount = value ? 1.0f : 0.0f; + obstructVisionAmount = value ? 0.5f : 0.0f; } } @@ -857,7 +878,7 @@ namespace Barotrauma get { return CharacterHealth.IsUnconscious; } } - public bool IsArrested + public bool IsHandcuffed { get { return IsHuman && HasEquippedItem(Tags.HandLockerItem); } } @@ -955,6 +976,18 @@ namespace Barotrauma } } + private float textChatVolume; + + /// + /// How "loud" the player is when they use text chat. + /// When the user speaks in text chat this gets set to 1 and then slowly decreases back to 0 over 5 seconds. + /// + public float TextChatVolume + { + get => textChatVolume; + set => textChatVolume = MathHelper.Clamp(value, 0.0f, 1.0f); + } + public float PressureTimer { get; @@ -1105,37 +1138,8 @@ namespace Barotrauma return !Removed; } } - - private bool canBeDragged = true; - public bool CanBeDragged - { - get - { - if (!canBeDragged) { return false; } - if (Removed || !AnimController.Draggable) { return false; } - return IsKnockedDown || LockHands || IsPet || CanInventoryBeAccessed; - } - set { canBeDragged = value; } - } - - //can other characters access the inventory of this character - private bool canInventoryBeAccessed = true; - public bool CanInventoryBeAccessed - { - get - { - if (!canInventoryBeAccessed || Removed || Inventory == null) { return false; } - if (!Inventory.AccessibleWhenAlive) - { - return IsDead; - } - else - { - return IsKnockedDown || LockHands || IsBot && IsOnPlayerTeam; - } - } - set { canInventoryBeAccessed = value; } - } + + public bool IsDraggable => !Removed || AnimController.Draggable; public bool CanAim { @@ -1322,6 +1326,10 @@ namespace Barotrauma : base(null, id) { wallet = new Wallet(Option.Some(this)); + if (GameMain.GameSession?.Campaign?.Bank is { } bank) + { + wallet.SetRewardDistribution(bank.RewardDistribution); + } this.Seed = seed; this.Prefab = prefab; @@ -1357,6 +1365,8 @@ namespace Barotrauma if (Info != null) { teamID = Info.TeamID; + //no longer a new hire after spawning (only displayed as a new hire at the end of the outpost round, when the character hasn't spawned yet) + Info.IsNewHire = false; } keys = new Key[Enum.GetNames(typeof(InputType)).Length]; for (int i = 0; i < Enum.GetNames(typeof(InputType)).Length; i++) @@ -1873,7 +1883,7 @@ namespace Barotrauma public bool CanRunWhileDragging() { - if (selectedCharacter == null || !selectedCharacter.CanBeDragged) { return true; } + if (selectedCharacter is not { IsDraggable: true }) { return true; } //if the dragged character is conscious, don't allow running (the dragged character won't keep up, and the dragging gets interrupted) if (!selectedCharacter.IsIncapacitated && selectedCharacter.Stun <= 0.0f) { return false; } return HasAbilityFlag(AbilityFlags.MoveNormallyWhileDragging); @@ -2574,18 +2584,15 @@ namespace Barotrauma return null; } - public bool CanAccessInventory(Inventory inventory) + public bool CanAccessInventory(Inventory inventory, CharacterInventory.AccessLevel accessLevel = CharacterInventory.AccessLevel.Limited) { if (!CanInteract || inventory.Locked) { return false; } - - //the inventory belongs to some other character - if (inventory.Owner is Character character && inventory.Owner != this) + + if (inventory.Owner is Character inventoryOwner) { - var owner = character; - //can only be accessed if the character is incapacitated and has been selected - return SelectedCharacter == owner && owner.CanInventoryBeAccessed; + return inventoryOwner.IsInventoryAccessibleTo(this, accessLevel) && (inventoryOwner == this || CanInteractWith(inventoryOwner)); } - + if (inventory.Owner is Item item) { if (!CanInteractWith(item)) @@ -2606,7 +2613,47 @@ namespace Barotrauma } return true; } - + + public bool CanBeHealedBy(Character character, bool checkFriendlyTeam = true) => + !character.IsClimbing && !DisableHealthWindow && + UseHealthWindow && character.CanInteract && + (!checkFriendlyTeam || IsFriendly(character) || CanBeDraggedBy(character)) && + character.CanInteractWith(this, 160f, false); + + public bool CanBeDraggedBy(Character character) + { + if (!IsDraggable) { return false; } + return IsKnockedDown || LockHands || IsPet || (IsBot && character.TeamID == TeamID); + } + + /// + /// Is the inventory accessible to the character? Doesn't check if the character can actually interact with it (distance checks etc). + /// + public bool IsInventoryAccessibleTo(Character character, CharacterInventory.AccessLevel accessLevel = CharacterInventory.AccessLevel.Limited) + { + if (Removed || Inventory == null) { return false; } + if (!Inventory.AccessibleWhenAlive && !IsDead) + { + if (character == this) + { + return Inventory.AccessibleByOwner; + } + return false; + } + if (character == this) { return true; } + if (IsKnockedDown || LockHands) { return true; } + return accessLevel switch + { + CharacterInventory.AccessLevel.Restricted => false, + CharacterInventory.AccessLevel.Limited => (IsBot && IsOnSameTeam()) || IsFriendlyPet(), + CharacterInventory.AccessLevel.Allowed => IsOnSameTeam() || IsFriendlyPet(), + _ => throw new NotImplementedException() + }; + + bool IsOnSameTeam() => character.TeamID == teamID; + bool IsFriendlyPet() => IsPet && character.IsFriendly(this); + } + private Stopwatch sw; private Stopwatch StopWatch => sw ??= new Stopwatch(); private float _selectedItemPriority; @@ -2694,7 +2741,7 @@ namespace Barotrauma public bool CanInteractWith(Character c, float maxDist = 200.0f, bool checkVisibility = true, bool skipDistanceCheck = false) { if (c == this || Removed || !c.Enabled || !c.CanBeSelected || c.InvisibleTimer > 0.0f) { return false; } - if (!c.CharacterHealth.UseHealthWindow && !c.CanBeDragged && (c.onCustomInteract == null || !c.AllowCustomInteract)) { return false; } + if (!c.CharacterHealth.UseHealthWindow && !c.IsDraggable && (c.onCustomInteract == null || !c.AllowCustomInteract)) { return false; } if (!skipDistanceCheck) { @@ -2718,7 +2765,7 @@ namespace Barotrauma { distanceToItem = -1.0f; - bool hidden = item.HiddenInGame; + bool hidden = item.IsHidden; #if CLIENT if (Screen.Selected == GameMain.SubEditorScreen) { hidden = false; } #endif @@ -2827,23 +2874,7 @@ namespace Barotrauma } if (distanceToItem > interactDistance && item.InteractDistance > 0.0f) { return false; } - Vector2 itemPosition = item.SimPosition; - if (Submarine == null && item.Submarine != null) - { - //character is outside, item inside - itemPosition += item.Submarine.SimPosition; - } - else if (Submarine != null && item.Submarine == null) - { - //character is inside, item outside - itemPosition -= Submarine.SimPosition; - } - else if (Submarine != item.Submarine) - { - //character and the item are inside different subs - itemPosition += item.Submarine.SimPosition; - itemPosition -= Submarine.SimPosition; - } + Vector2 itemPosition = GetPosition(Submarine, item, item.SimPosition); if (SelectedSecondaryItem != null && !item.IsSecondaryItem) { @@ -2864,21 +2895,79 @@ namespace Barotrauma if (!item.Prefab.InteractThroughWalls && Screen.Selected != GameMain.SubEditorScreen && !insideTrigger) { var body = Submarine.CheckVisibility(SimPosition, itemPosition, ignoreLevel: true); - if (body != null) + bool itemCenterVisible = CheckBody(body, item); + + if (!itemCenterVisible && item.Prefab.RequireCursorInsideTrigger) { - var otherItem = body.UserData as Item ?? (body.UserData as ItemComponent)?.Item; - if (otherItem != item && - (body.UserData as ItemComponent)?.Item != item && - /*allow interacting through open doors (e.g. duct blocks' colliders stay active despite being open)*/ - otherItem?.GetComponent() is not { IsOpen: true } && - Submarine.LastPickedFixture?.UserData as Item != item) - { - return false; + foreach (Rectangle trigger in item.Prefab.Triggers) + { + Rectangle transformTrigger = item.TransformTrigger(trigger, world: false); + + RectangleF simRect = new RectangleF( + x: ConvertUnits.ToSimUnits(transformTrigger.X), + y: ConvertUnits.ToSimUnits(transformTrigger.Y - transformTrigger.Height), + width: ConvertUnits.ToSimUnits(transformTrigger.Width), + height: ConvertUnits.ToSimUnits(transformTrigger.Height)); + + simRect.Location = GetPosition(Submarine, item, simRect.Location); + + Vector2 closest = ToolBox.GetClosestPointOnRectangle(simRect, SimPosition); + var triggerBody = Submarine.CheckVisibility(SimPosition, closest, ignoreLevel: true); + + if (CheckBody(triggerBody, item)) { return true; } } } + else + { + return itemCenterVisible; + } + } return true; + + static bool CheckBody(Body body, Item item) + { + if (body is null) { return true; } + var otherItem = body.UserData as Item ?? (body.UserData as ItemComponent)?.Item; + if (otherItem != item && + (body.UserData as ItemComponent)?.Item != item && + /*allow interacting through open doors (e.g. duct blocks' colliders stay active despite being open)*/ + otherItem?.GetComponent() is not { IsOpen: true } && + Submarine.LastPickedFixture?.UserData as Item != item) + { + return false; + } + + return true; + } + + static Vector2 GetPosition(Submarine submarine, Item item, Vector2 simPosition) + { + Vector2 position = simPosition; + + Vector2 itemSubPos = item.Submarine?.SimPosition ?? Vector2.Zero; + Vector2 subPos = submarine?.SimPosition ?? Vector2.Zero; + + if (submarine == null && item.Submarine != null) + { + //character is outside, item inside + position += itemSubPos; + } + else if (submarine != null && item.Submarine == null) + { + //character is inside, item outside + position -= subPos; + } + else if (submarine != item.Submarine && submarine != null) + { + //character and the item are inside different subs + position += itemSubPos; + position -= subPos; + } + + return position; + } } /// @@ -3051,15 +3140,15 @@ namespace Barotrauma { DeselectCharacter(); } - else if (FocusedCharacter != null && IsKeyHit(InputType.Grab) && FocusedCharacter.CanBeDragged && (CanInteract || FocusedCharacter.IsDead && CanEat)) + else if (FocusedCharacter != null && IsKeyHit(InputType.Grab) && FocusedCharacter.CanBeDraggedBy(this) && (CanInteract || FocusedCharacter.IsDead && CanEat)) { SelectCharacter(FocusedCharacter); } - else if (FocusedCharacter != null && !FocusedCharacter.IsIncapacitated && IsKeyHit(InputType.Use) && FocusedCharacter.IsPet && CanInteract) + else if (FocusedCharacter is { IsIncapacitated: false } && IsKeyHit(InputType.Use) && FocusedCharacter.IsPet && CanInteract) { (FocusedCharacter.AIController as EnemyAIController).PetBehavior.Play(this); } - else if (FocusedCharacter != null && IsKeyHit(InputType.Health) && FocusedCharacter.CharacterHealth.UseHealthWindow && CanInteract && CanInteractWith(FocusedCharacter, 160f, false)) + else if (FocusedCharacter != null && IsKeyHit(InputType.Health) && FocusedCharacter.CanBeHealedBy(this)) { if (FocusedCharacter == SelectedCharacter) { @@ -3071,12 +3160,14 @@ namespace Barotrauma } #endif } - else if (!IsClimbing) + else { + SelectCharacter(FocusedCharacter); #if CLIENT if (Controlled == this) { HealingCooldown.PutOnCooldown(); + CharacterHealth.OpenHealthWindow = FocusedCharacter.CharacterHealth; } #elif SERVER if (GameMain.Server?.ConnectedClients is { } clients) @@ -3089,13 +3180,6 @@ namespace Barotrauma break; } } -#endif - SelectCharacter(FocusedCharacter); -#if CLIENT - if (Controlled == this) - { - CharacterHealth.OpenHealthWindow = FocusedCharacter.CharacterHealth; - } #endif } } @@ -3232,6 +3316,11 @@ namespace Barotrauma { UpdateProjSpecific(deltaTime, cam); + if (TextChatVolume > 0) + { + TextChatVolume -= 0.2f * deltaTime; + } + if (InvisibleTimer > 0.0f) { if (Controlled == null || Controlled == this || (Controlled.CharacterHealth.GetAffliction("psychosis")?.Strength ?? 0.0f) <= 0.0f) @@ -3461,16 +3550,33 @@ namespace Barotrauma DoInteractionUpdate(deltaTime, mouseSimPos); } - if (SelectedItem != null && !CanInteractWith(SelectedItem)) + if (MustDeselect(SelectedItem)) { SelectedItem = null; } - if (SelectedSecondaryItem != null && !CanInteractWith(SelectedSecondaryItem)) + if (MustDeselect(SelectedSecondaryItem)) { ReleaseSecondaryItem(); } if (!IsDead) { LockHands = false; } + + bool MustDeselect(Item item) + { + if (item == null) { return false; } + if (!CanInteractWith(item)) { return true; } + bool hasSelectableComponent = false; + foreach (var component in item.Components) + { + //the "selectability" of an item can change e.g. if the player unequips another item that's required to access it + if (component.CanBeSelected && component.HasRequiredItems(this, addMessage: false)) + { + hasSelectableComponent = true; + break; + } + } + return !hasSelectableComponent; + } } partial void UpdateControlled(float deltaTime, Camera cam); @@ -3786,6 +3892,9 @@ namespace Barotrauma private void UpdateSoundRange(float deltaTime) { + const float textChatVolumeMultiplier = 0.5f; + const float voiceChatVolumeMultiplier = 1.5f; + if (aiTarget == null) { return; } if (IsDead) { @@ -3795,8 +3904,37 @@ namespace Barotrauma { float massFactor = (float)Math.Sqrt(Mass / 10); float targetRange = Math.Min(massFactor * AnimController.Collider.LinearVelocity.Length() * 2 * Noise, maxAIRange); + float speechImpedimentMultiplier = 1.0f - SpeechImpediment / 100.0f; + if (TextChatVolume > 0) + { + targetRange = Math.Max(targetRange, TextChatVolume * textChatVolumeMultiplier * ChatMessage.SpeakRange * speechImpedimentMultiplier); + } + + if (IsPlayer) + { + float voipAmplitude = 0.0f; +#if SERVER + foreach (var c in GameMain.Server.ConnectedClients) + { + if (c.Character != this) { continue; } + voipAmplitude = c.VoipServerDecoder.Amplitude; + break; + } +#elif CLIENT && DEBUG + if (Controlled == this && GameMain.Client != null) + { + voipAmplitude = GameMain.Client.DebugServerVoipAmplitude; + } +#endif + targetRange = Math.Max(targetRange, voipAmplitude * voiceChatVolumeMultiplier * ChatMessage.SpeakRange * speechImpedimentMultiplier); + } + targetRange *= 1.0f + GetStatValue(StatTypes.SoundRangeMultiplier); + targetRange = Math.Min(targetRange, maxAIRange); + float newRange = MathHelper.SmoothStep(aiTarget.SoundRange, targetRange, deltaTime * aiTargetChangeSpeed); + + newRange *= 1.0f + GetStatValue(StatTypes.SoundRangeMultiplier); if (!float.IsNaN(newRange)) { aiTarget.SoundRange = newRange; @@ -4693,7 +4831,31 @@ namespace Barotrauma } #endif - isDead = true; + ApplyStatusEffects(ActionType.OnDeath, 1.0f); + + AnimController.Frozen = false; + + Character killer = causeOfDeathAffliction?.Source; + if (IsBot) + { + foreach (var item in Inventory.AllItems) + { + if (item.Equipper is { IsPlayer: true } && + item.GetComponents().Any(ic => ic.BlameEquipperForDeath())) + { + killer = item.Equipper; + if (AIController is HumanAIController humanAi) + { + humanAi.OnAttacked(killer, new AttackResult(damage: MaxVitality)); + } + break; + } + } + } + + CauseOfDeath = new CauseOfDeath( + causeOfDeath, causeOfDeathAffliction?.Prefab, + killer, LastDamageSource); // Save these resistances in the CharacterInfo object so that if they // are needed for respawning, they will be available (because there @@ -4704,13 +4866,25 @@ namespace Barotrauma info.LastResistanceMultiplierSkillLossRespawn = GetAbilityResistance(Tags.SkillLossRespawnResistance); } - ApplyStatusEffects(ActionType.OnDeath, 1.0f); + isDead = true; - AnimController.Frozen = false; +#if CLIENT + // Keep permadeath status in sync (to show it correctly in the UI, the server takes care of the actual logic) + // NOTE: The opposite is done in Revive + if (GameMain.NetworkMember is { ServerSettings.RespawnMode: RespawnMode.Permadeath } && + GameMain.Client.Character == this && + GameMain.Client.CharacterInfo is CharacterInfo characterInfo) + { + characterInfo.PermanentlyDead = true; + } +#endif - CauseOfDeath = new CauseOfDeath( - causeOfDeath, causeOfDeathAffliction?.Prefab, - causeOfDeathAffliction?.Source, LastDamageSource); +#if SERVER + if (Info is not null) + { + Info.LastRewardDistribution = Option.Some(Wallet.RewardDistribution); + } +#endif if (GameAnalyticsManager.SendUserStatistics && Prefab?.ContentPackage == ContentPackageManager.VanillaCorePackage) { @@ -4823,6 +4997,15 @@ namespace Barotrauma if (info != null) { info.CauseOfDeath = null; + + // Keep permadeath status in sync (to show it correctly in the UI, the server takes care of the actual logic) + // NOTE: The opposite is done in Kill + // FYI: In case you're wondering, it's alright to revive a "permanently" dead character here, because if + // this gets called, the character wasn't actually dead anyway (eg. returning to lobby without saving) + if (GameMain.NetworkMember is { ServerSettings.RespawnMode: RespawnMode.Permadeath }) + { + info.PermanentlyDead = false; + } } foreach (LimbJoint joint in AnimController.LimbJoints) @@ -5450,7 +5633,14 @@ namespace Barotrauma { statValue += wearableValue; } - + foreach (var heldItem in HeldItems) + { + if (heldItem.GetComponent() is Holdable holdable && + holdable.HoldableStatValues.TryGetValue(statType, out float holdableValue)) + { + statValue += holdableValue; + } + } return statValue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index 1b673b092..0d6aefde0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -1,4 +1,4 @@ -using Barotrauma.Extensions; +using Barotrauma.Extensions; using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; @@ -44,8 +44,13 @@ namespace Barotrauma public readonly Identifier MenuCategoryVar; public readonly Identifier Pronouns; - public CharacterInfoPrefab(ContentXElement headsElement, XElement varsElement, XElement menuCategoryElement, XElement pronounsElement) + public CharacterInfoPrefab(CharacterPrefab characterPrefab, ContentXElement headsElement, XElement varsElement, XElement menuCategoryElement, XElement pronounsElement) { + if (headsElement == null) + { + throw new Exception($"No heads configured for the character \"{characterPrefab.Identifier}\". Characters with CharacterInfo must have head sprites. Please add a element to the character's config."); + } + Heads = headsElement.Elements().Select(e => new CharacterInfo.HeadPreset(this, e)).ToImmutableArray(); if (varsElement != null) { @@ -82,6 +87,10 @@ namespace Barotrauma } } + /// + /// Stores information about the Character that is needed between rounds in the + /// menu etc., whereas Character itself is the object actually spawned in-game. + /// partial class CharacterInfo { public class HeadInfo @@ -289,7 +298,9 @@ namespace Barotrauma public XElement HealthData; public XElement OrderData; - private static ushort idCounter; + public bool PermanentlyDead; + + private static ushort idCounter = 1; private const string disguiseName = "???"; public bool HasNickname => Name != OriginalName; @@ -492,6 +503,9 @@ namespace Barotrauma public bool StartItemsGiven; + /// + /// Newly hired bot that hasn't spawned yet + /// public bool IsNewHire; public CauseOfDeath CauseOfDeath; @@ -642,6 +656,15 @@ namespace Barotrauma => element.GetAttributeBool("specifiertags", element.GetAttributeBool("genders", element.GetAttributeBool("races", false))); + + /// + /// Keeps track of the last reward distribution that was set on the character's wallet. + /// Is used to keep salary when the character respawns since CharacterInfo is preserved between deaths. + /// + /// + /// None means the salary has not been set yet, which is not always 0 if default salary is set. + /// + public Option LastRewardDistribution = Option.None; // Used for creating the data public CharacterInfo( @@ -662,6 +685,7 @@ namespace Barotrauma } ID = idCounter; idCounter++; + if (idCounter == 0) { idCounter++; } SpeciesName = speciesName; SpriteTags = new List(); CharacterConfigElement = CharacterPrefab.FindBySpeciesName(SpeciesName)?.ConfigElement; @@ -699,6 +723,12 @@ namespace Barotrauma Salary = CalculateSalary(); } OriginalName = !string.IsNullOrEmpty(originalName) ? originalName : Name; + + int loadedLastRewardDistribution = CharacterConfigElement.GetAttributeInt("lastrewarddistribution", -1); + if (loadedLastRewardDistribution >= 0) + { + LastRewardDistribution = Option.Some(loadedLastRewardDistribution); + } } private void SetPersonalityTrait() @@ -771,6 +801,7 @@ namespace Barotrauma HashSet tags = infoElement.GetAttributeIdentifierArray("tags", Array.Empty()).ToHashSet(); LoadTagsBackwardsCompatibility(infoElement, tags); SpeciesName = infoElement.GetAttributeIdentifier("speciesname", ""); + PermanentlyDead = infoElement.GetAttributeBool("permanentlydead", false); ContentXElement element; if (!SpeciesName.IsEmpty) { @@ -951,7 +982,7 @@ namespace Barotrauma } /// - /// Returns a presumably (not guaranteed) unique hash using the (current) Name, appearence, and job. + /// Returns a presumably (not guaranteed) unique and persistent hash using the (current) Name, appearence, and job. /// So unless there's another character with the exactly same name, job, and appearance, the hash should be unique. /// public int GetIdentifier() @@ -960,7 +991,7 @@ namespace Barotrauma } /// - /// Returns a presumably (not guaranteed) unique hash using the OriginalName, appearence, and job. + /// Returns a presumably (not guaranteed) unique hash and persistent using the OriginalName, appearence, and job. /// So unless there's another character with the exactly same name, job, and appearance, the hash should be unique. /// public int GetIdentifierUsingOriginalName() @@ -1466,7 +1497,10 @@ namespace Barotrauma new XAttribute("haircolor", XMLExtensions.ColorToString(Head.HairColor)), new XAttribute("facialhaircolor", XMLExtensions.ColorToString(Head.FacialHairColor)), new XAttribute("startitemsgiven", StartItemsGiven), - new XAttribute("personality", PersonalityTrait?.Identifier ?? Identifier.Empty)); + new XAttribute("personality", PersonalityTrait?.Identifier ?? Identifier.Empty), + new XAttribute("lastrewarddistribution", LastRewardDistribution.Match(some: value => value, none: () => -1).ToString()), + new XAttribute("permanentlydead", PermanentlyDead) + ); if (HumanPrefabIds != default) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs index c1cea91c8..22f22f6c6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs @@ -32,6 +32,8 @@ namespace Barotrauma return speciesName; } + public bool HasCharacterInfo { get; private set; } + public void InheritFrom(CharacterPrefab parent) { ConfigElement = CharacterParams.CreateVariantXml(originalElement, parent.ConfigElement).FromPackage(ConfigElement.ContentPackage); @@ -45,9 +47,10 @@ namespace Barotrauma var menuCategoryElement = ConfigElement.GetChildElement("MenuCategory"); var pronounsElement = ConfigElement.GetChildElement("Pronouns"); - if (headsElement != null) + HasCharacterInfo = headsElement != null || ConfigElement.GetAttributeBool(nameof(HasCharacterInfo), false); + if (HasCharacterInfo) { - CharacterInfoPrefab = new CharacterInfoPrefab(headsElement, varsElement, menuCategoryElement, pronounsElement); + CharacterInfoPrefab = new CharacterInfoPrefab(this, headsElement, varsElement, menuCategoryElement, pronounsElement); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index 10451960e..925d30006 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs @@ -100,6 +100,12 @@ namespace Barotrauma [Serialize(AIObjectiveIdle.BehaviorType.Passive, IsPropertySaveable.No)] public AIObjectiveIdle.BehaviorType Behavior { get; protected set; } + [Serialize(1.0f, IsPropertySaveable.No, description: + "Affects how far the character can hear sounds created by AI targets with the tag ProvocativeToHumanAI. "+ + "Used as a multiplier on the sound range of the target, e.g. a value of 0.5 would mean a target with a sound range of 1000 would need to be within 500 units for this character to hear it. "+ + "Only affects the \"fight intruders\" objective, which makes the character go and inspect noises.")] + public float Hearing { get; set; } = 1.0f; + [Serialize(float.PositiveInfinity, IsPropertySaveable.No)] public float ReportRange { get; protected set; } @@ -174,6 +180,7 @@ namespace Barotrauma idleObjective.PreferredOutpostModuleTypes.Add(moduleType); } } + humanAI.ReportRange = Hearing; humanAI.ReportRange = ReportRange; humanAI.FindWeaponsRange = FindWeaponsRange; humanAI.AimSpeed = AimSpeed; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs index e8e08f791..ff8c4ecaa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs @@ -10,15 +10,26 @@ namespace Barotrauma private float level; + /// + /// The highest skill level during the round (before any death penalties were applied) + /// + public float HighestLevelDuringRound { get; private set; } + public float Level { get { return level; } - set { level = value; } + set + { + HighestLevelDuringRound = MathHelper.Max(value, HighestLevelDuringRound); + level = value; + } } + public LocalizedString DisplayName { get; private set; } + public void IncreaseSkill(float value, bool increasePastMax) { - level = MathHelper.Clamp(level + value, 0.0f, increasePastMax ? SkillSettings.Current.MaximumSkillWithTalents : MaximumSkill); + Level = MathHelper.Clamp(level + value, 0.0f, increasePastMax ? SkillSettings.Current.MaximumSkillWithTalents : MaximumSkill); } private readonly Identifier iconJobId; @@ -32,16 +43,18 @@ namespace Barotrauma public Skill(SkillPrefab prefab, Rand.RandSync randSync) { Identifier = prefab.Identifier; - level = Rand.Range(prefab.LevelRange.Start, prefab.LevelRange.End, randSync); + Level = Rand.Range(prefab.LevelRange.Start, prefab.LevelRange.End, randSync); iconJobId = GetIconJobId(); PriceMultiplier = prefab.PriceMultiplier; + DisplayName = TextManager.Get("SkillName." + Identifier); } public Skill(Identifier identifier, float level) { Identifier = identifier; - this.level = level; + Level = level; iconJobId = GetIconJobId(); + DisplayName = TextManager.Get("SkillName." + Identifier); } private Identifier GetIconJobId() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs index 99e289b18..83f685fbd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs @@ -22,6 +22,7 @@ namespace Barotrauma abstract class GroundedMovementParams : AnimationParams { + [Header("Legs")] [Serialize("1.0, 1.0", IsPropertySaveable.Yes, description: "How big steps the character takes."), Editable(DecimalCount = 2, ValueStep = 0.01f)] public Vector2 StepSize { @@ -29,12 +30,14 @@ namespace Barotrauma set; } + [Header("Standing")] [Serialize(0f, IsPropertySaveable.Yes, description: "How high above the ground the character's head is positioned."), Editable(DecimalCount = 2, ValueStep = 0.1f)] public float HeadPosition { get; set; } [Serialize(0f, IsPropertySaveable.Yes, description: "How high above the ground the character's torso is positioned."), Editable(DecimalCount = 2, ValueStep = 0.1f)] public float TorsoPosition { get; set; } + [Header("Step lift")] [Serialize(1f, IsPropertySaveable.Yes, description: "Separate multiplier for the head lift"), Editable(MinValueFloat = 0, MaxValueFloat = 2, ValueStep = 0.1f)] public float StepLiftHeadMultiplier { get; set; } @@ -50,6 +53,7 @@ namespace Barotrauma [Serialize(2f, IsPropertySaveable.Yes, description: "How frequently the body raises when taking a step. The default is 2 (after every step)."), Editable(MinValueFloat = 0, MaxValueFloat = 10, ValueStep = 0.1f)] public float StepLiftFrequency { get; set; } + [Header("Movement")] [Serialize(0.75f, IsPropertySaveable.Yes, description: "The character's movement speed is multiplied with this value when moving backwards."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 0.99f, DecimalCount = 2)] public float BackwardsMovementMultiplier { get; set; } } @@ -69,11 +73,15 @@ namespace Barotrauma public bool IsGroundedAnimation => AnimationType is AnimationType.Walk or AnimationType.Run or AnimationType.Crouch; public bool IsSwimAnimation => AnimationType is AnimationType.SwimSlow or AnimationType.SwimFast; + [Header("General")] + [Serialize(AnimationType.NotDefined, IsPropertySaveable.Yes), Editable] + public virtual AnimationType AnimationType { get; protected set; } /// /// The cached animations of all the characters that have been loaded. /// private static readonly Dictionary> allAnimations = new Dictionary>(); + [Header("Movement")] [Serialize(1.0f, IsPropertySaveable.Yes), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = Ragdoll.MAX_SPEED, ValueStep = 0.1f)] public float MovementSpeed { get; set; } @@ -84,6 +92,7 @@ namespace Barotrauma /// /// In degrees. /// + [Header("Standing")] [Serialize(float.NaN, IsPropertySaveable.Yes), Editable(-360f, 360f)] public float HeadAngle { @@ -122,12 +131,11 @@ namespace Barotrauma [Serialize(50.0f, IsPropertySaveable.Yes, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] public float TorsoTorque { get; set; } + [Header("Legs")] [Serialize(25.0f, IsPropertySaveable.Yes, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] public float FootTorque { get; set; } - [Serialize(AnimationType.NotDefined, IsPropertySaveable.Yes), Editable] - public virtual AnimationType AnimationType { get; protected set; } - + [Header("Arms")] [Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to rotate the arms to the IK position."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] public float ArmIKStrength { get; set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs index 48981573b..3b9994127 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs @@ -69,21 +69,13 @@ namespace Barotrauma abstract class HumanSwimParams : SwimParams, IHumanAnimation { + [Header("Legs")] [Serialize(0.5f, IsPropertySaveable.Yes), Editable(DecimalCount = 2)] public float LegMoveAmount { get; set; } [Serialize(5.0f, IsPropertySaveable.Yes), Editable] public float LegCycleLength { get; set; } - [Serialize("0.5, 0.1", IsPropertySaveable.Yes), Editable(DecimalCount = 2)] - public Vector2 HandMoveAmount { get; set; } - - [Serialize(5.0f, IsPropertySaveable.Yes), Editable] - public float HandCycleSpeed { get; set; } - - [Serialize("0.0, 0.0", IsPropertySaveable.Yes), Editable(DecimalCount = 2)] - public Vector2 HandMoveOffset { get; set; } - /// /// In degrees. /// @@ -96,20 +88,33 @@ namespace Barotrauma FootAngleInRadians = MathHelper.ToRadians(value); } } + public float FootAngleInRadians { get; private set; } + [Header("Arms")] + [Serialize("0.5, 0.1", IsPropertySaveable.Yes), Editable(DecimalCount = 2)] + public Vector2 HandMoveAmount { get; set; } + + [Serialize(5.0f, IsPropertySaveable.Yes), Editable] + public float HandCycleSpeed { get; set; } + + [Serialize("0.0, 0.0", IsPropertySaveable.Yes), Editable(DecimalCount = 2)] + public Vector2 HandMoveOffset { get; set; } + [Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to move the arms."), Editable(MinValueFloat = 0, MaxValueFloat = 20, DecimalCount = 2)] public float ArmMoveStrength { get; set; } [Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] public float HandMoveStrength { get; set; } + [Header("Other")] [Serialize(true, IsPropertySaveable.Yes, description: "Is the head angle fixed or does the angle follow the mouse position?"), Editable] public bool FixedHeadAngle { get; set; } } abstract class HumanGroundedParams : GroundedMovementParams, IHumanAnimation { + [Header("Standing")] [Serialize(0.3f, IsPropertySaveable.Yes, description: "How much force is used to force the character upright."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)] public float GetUpForce { get; set; } @@ -119,6 +124,7 @@ namespace Barotrauma [Serialize(0.25f, IsPropertySaveable.Yes, description: "How much the character's torso leans forwards when moving."), Editable(DecimalCount = 2)] public float TorsoLeanAmount { get; set; } + [Header("Legs")] [Serialize(15.0f, IsPropertySaveable.Yes, description: "How much force is used to move the feet to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] public float FootMoveStrength { get; set; } @@ -152,6 +158,7 @@ namespace Barotrauma [Serialize(10.0f, IsPropertySaveable.Yes, description: "How much torque is used to bend the characters legs when taking a step."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] public float LegBendTorque { get; set; } + [Header("Arms")] [Serialize("0.4, 0.15", IsPropertySaveable.Yes, description: "How much the hands move along each axis."), Editable(DecimalCount = 2)] public Vector2 HandMoveAmount { get; set; } @@ -167,6 +174,7 @@ namespace Barotrauma [Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] public float HandMoveStrength { get; set; } + [Header("Other")] [Serialize(true, IsPropertySaveable.Yes, description: "Is the head angle fixed or does the angle follow the mouse position?"), Editable] public bool FixedHeadAngle { get; set; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs index f4bf452ce..e19fb68a5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs @@ -87,9 +87,20 @@ namespace Barotrauma set { jointScale = MathHelper.Clamp(value, MIN_SCALE, MAX_SCALE); } } - // Don't show in the editor, because shouldn't be edited in runtime. Requires that the limb scale and the collider sizes are adjusted. TODO: automatize? + /// + /// Can be used for scaling the textures without having to readjust the entire ragdoll. + /// Note that we'll still have to readjust the source rects and the colliders sizes, unless we also adjust . + /// E.g. for upscaling the textures 2x, set to 0.5 and to 2. + /// [Serialize(1f, IsPropertySaveable.No)] public float TextureScale { get; set; } + + /// + /// Multiplies both the position and the size of the source rects. + /// Used for scaling the textures when we cannot/don't want to touch the source rect definitions (e.g. on variants). + /// + [Serialize(1f, IsPropertySaveable.No)] + public float SourceRectScale { get; set; } [Serialize(45f, IsPropertySaveable.Yes, description: "How high from the ground the main collider levitates when the character is standing? Doesn't affect swimming."), Editable(0f, 1000f)] public float ColliderHeightFromFloor { get; set; } @@ -491,6 +502,18 @@ namespace Barotrauma float scaleMultiplier = ragdollElement.GetAttributeFloat("scalemultiplier", 1f); JointScale *= scaleMultiplier; LimbScale *= scaleMultiplier; + float textureScale = ragdollElement.GetAttributeFloat(nameof(TextureScale), 0f); + if (textureScale > 0) + { + // Override, if defined. + TextureScale = textureScale; + } + float sourceRectScale = ragdollElement.GetAttributeFloat(nameof(SourceRectScale), 0f); + if (sourceRectScale > 0) + { + // Override, if defined. + SourceRectScale = sourceRectScale; + } } } isVariantScaleApplied = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxConnection.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxConnection.cs index cd2b6bdb8..ce440505f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxConnection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxConnection.cs @@ -71,9 +71,9 @@ namespace Barotrauma private Vector2 position; - public List ExternallyConnectedFrom = new(); + public readonly List ExternallyConnectedFrom = new(); - public static float Size = CircuitBoxSizes.ConnectorSize; + public static readonly float Size = CircuitBoxSizes.ConnectorSize; public Vector2 Position { diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxInputOutputNode.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxInputOutputNode.cs index 4c935f53e..a212507dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxInputOutputNode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxInputOutputNode.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System.Collections.Generic; using System.Collections.Immutable; @@ -8,7 +8,7 @@ using Microsoft.Xna.Framework; namespace Barotrauma { - internal sealed class CircuitBoxInputOutputNode : CircuitBoxNode + internal sealed partial class CircuitBoxInputOutputNode : CircuitBoxNode { public enum Type { @@ -17,22 +17,104 @@ namespace Barotrauma Output } - public Type NodeType; + public readonly Type NodeType; + + private const int MaxConnectionLabelLength = 32; + private const string ConnectionLabelOverrideElementName = "ConnectionLabelOverride"; + + public Dictionary ConnectionLabelOverrides = new(); public CircuitBoxInputOutputNode(IReadOnlyList conns, Vector2 initialPosition, Type type, CircuitBox circuitBox): base(circuitBox) { - Size = CalculateSize(conns); + InitSize(conns); Connectors = conns.ToImmutableArray(); Position = initialPosition; NodeType = type; UpdatePositions(); } - public XElement Save() => new XElement($"{NodeType}Node", new XAttribute("pos", XMLExtensions.Vector2ToString(Position))); + public void ReplaceAllConnectionLabelOverrides(Dictionary replace) + { + foreach (var (_, value) in replace) + { + if (value.Length > MaxConnectionLabelLength) + { + DebugConsole.ThrowError($"Label override value \"{value}\" is too long (max {MaxConnectionLabelLength} characters)"); + return; + } + } + + foreach (var (name, value) in replace) + { + if (string.IsNullOrWhiteSpace(value)) + { + ConnectionLabelOverrides.Remove(name); + } + else + { + ConnectionLabelOverrides[name] = value; + } + } + + InitSize(Connectors); + UpdatePositions(); + } + + private void InitSize(IReadOnlyList conns) + { +#if CLIENT + foreach (CircuitBoxConnection conn in conns) + { + if (ConnectionLabelOverrides.TryGetValue(conn.Name, out string? value)) + { + LocalizedString newLabel = + string.IsNullOrWhiteSpace(value) + ? conn.Connection.DisplayName + : TextManager.Get(value).Fallback(value); + + conn.SetLabel(newLabel, this); + } + else + { + conn.SetLabel(conn.Connection.DisplayName, this); + } + } +#endif + Size = CalculateSize(conns); + } + + public XElement Save() + { + XElement element = new XElement($"{NodeType}Node", new XAttribute("pos", XMLExtensions.Vector2ToString(Position))); + + foreach (var (name, value) in ConnectionLabelOverrides) + { + element.Add(new XElement(ConnectionLabelOverrideElementName, + new XAttribute("name", name), + new XAttribute("value", value))); + } + + return element; + } public void Load(ContentXElement element) { Position = element.GetAttributeVector2("pos", Vector2.Zero); + + Dictionary loadedOverrides = new(); + foreach (var subElement in element.Elements()) + { + if (subElement.Name != ConnectionLabelOverrideElementName) { continue; } + + string name = subElement.GetAttributeString("name", string.Empty); + string value = subElement.GetAttributeString("value", string.Empty); + + loadedOverrides[name] = value; + } + + ConnectionLabelOverrides = loadedOverrides; + InitSize(Connectors); + UpdatePositions(); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxNetStructs.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxNetStructs.cs index 2d7ad4f36..e98c542c4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxNetStructs.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxNetStructs.cs @@ -37,6 +37,7 @@ namespace Barotrauma AddLabel, RemoveLabel, ResizeLabel, + RenameConnections, ServerInitialize } @@ -156,6 +157,10 @@ namespace Barotrauma [NetworkSerialize] internal readonly record struct CircuitBoxRenameLabelEvent(ushort LabelId, Color Color, NetLimitedString NewHeader, NetLimitedString NewBody) : INetSerializableStruct; + [NetworkSerialize] + internal readonly record struct CircuitBoxRenameConnectionLabelsEvent(CircuitBoxInputOutputNode.Type Type, NetDictionary Override) : INetSerializableStruct; + + [NetworkSerialize] internal readonly record struct CircuitBoxErrorEvent(string Message) : INetSerializableStruct; @@ -164,6 +169,7 @@ namespace Barotrauma ImmutableArray Components, ImmutableArray Wires, ImmutableArray Labels, + ImmutableArray LabelOverrides, Vector2 InputPos, Vector2 OutputPos) : INetSerializableStruct; @@ -198,6 +204,8 @@ namespace Barotrauma => CircuitBoxOpcode.RemoveLabel, CircuitBoxResizeLabelEvent => CircuitBoxOpcode.ResizeLabel, + CircuitBoxRenameConnectionLabelsEvent + => CircuitBoxOpcode.RenameConnections, _ => throw new ArgumentOutOfRangeException(nameof(Data)) }; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 316408b52..d3df8e41c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -14,6 +14,7 @@ using System.Threading.Tasks; using Barotrauma.MapCreatures.Behavior; using System.Text; + namespace Barotrauma { readonly struct ColoredText @@ -248,7 +249,7 @@ namespace Barotrauma GameMain.NetworkMember.ShowNetStats = !GameMain.NetworkMember.ShowNetStats; })); - commands.Add(new Command("spawn|spawncharacter", "spawn [creaturename/jobname] [near/inside/outside/cursor] [team (0-3)]: Spawn a creature at a random spawnpoint (use the second parameter to only select spawnpoints near/inside/outside the submarine). You can also enter the name of a job (e.g. \"Mechanic\") to spawn a character with a specific job and the appropriate equipment.", null, + commands.Add(new Command("spawn|spawncharacter", "spawn [creaturename/jobname] [near/inside/outside/cursor] [team (0-3)] [add to crew (true/false)]: Spawn a creature at a random spawnpoint (use the second parameter to only select spawnpoints near/inside/outside the submarine). You can also enter the name of a job (e.g. \"Mechanic\") to spawn a character with a specific job and the appropriate equipment.", null, () => { string[] creatureAndJobNames = @@ -260,7 +261,10 @@ namespace Barotrauma return new string[][] { creatureAndJobNames.ToArray(), - new string[] { "near", "inside", "outside", "cursor" } + new string[] { "near", "inside", "outside", "cursor" }, + new string[] { "0", "1", "2", "3" }, + new string[] { "true", "false" }, + }; }, isCheat: true)); @@ -566,11 +570,30 @@ namespace Barotrauma commands.Add(new Command("banaddress|banip", "banaddress [endpoint]: Ban the IP address/SteamID from the server.", null)); - commands.Add(new Command("teleportcharacter|teleport", "teleport [character name]: Teleport the specified character to the position of the cursor. If the name parameter is omitted, the controlled character will be teleported.", null, - () => + commands.Add(new Command("teleportcharacter|teleport", "teleport [character name] [location]: Teleport the specified character to a location , or the position of the cursor if location is omitted. If the name parameter is omitted, the controlled character will be teleported.", + onExecute: null, + getValidArgs:() => { - return new string[][] { ListCharacterNames() }; + var characterList = Character.Controlled != null ? new[] { "Me" } : Array.Empty(); + var subList = Submarine.MainSub != null ? new[] { "mainsub" } : Array.Empty(); + return new string[][] + { + characterList.Concat(ListCharacterNames()).ToArray(), + subList.Concat(ListAvailableLocations()).ToArray() + }; }, isCheat: true)); + + commands.Add(new Command("listlocations|locations", "listlocations: List all the locations in the level: subs, outposts, ruins, caves.", + onExecute:(string[] args) => + { + var availableLocations = ListAvailableLocations(); + NewMessage("***************", Color.Cyan); + foreach (var location in availableLocations) + { + NewMessage(location, Color.Cyan); + } + NewMessage("***************", Color.Cyan); + })); commands.Add(new Command("godmode", "godmode [character name]: Toggle character godmode. Makes the targeted character invulnerable to damage. If the name parameter is omitted, the controlled character will receive godmode.", (string[] args) => @@ -781,6 +804,17 @@ namespace Barotrauma { if (c.Character != revivedCharacter) { continue; } + // If killed in ironman mode, the character has been wiped from the save mid-round, so its + // original data needs to be restored to the save file (without making a backup of the dead character) + if (GameMain.Server.ServerSettings.IronmanMode && GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign) + { + if (mpCampaign.RestoreSingleCharacterFromBackup(c) is CharacterCampaignData characterToRestore) + { + characterToRestore.CharacterInfo.PermanentlyDead = false; + mpCampaign.SaveSingleCharacter(characterToRestore, skipBackup: true); + } + } + //clients stop controlling the character when it dies, force control back GameMain.Server.SetClientCharacter(c, revivedCharacter); break; @@ -1175,7 +1209,8 @@ namespace Barotrauma } },null)); - commands.Add(new Command("teleportsub", "teleportsub [start/end/endoutpost/cursor]: Teleport the submarine to the position of the cursor, or the start or end of the level. The 'endoutpost' argument also automatically docks the sub with the outpost at the end of the level. WARNING: does not take outposts into account, so often leads to physics glitches. Only use for debugging.", (string[] args) => + commands.Add(new Command("teleportsub", "teleportsub [start/end/endoutpost/cursor]: Teleport the submarine to the position of the cursor, or the start or end of the level. The 'endoutpost' argument also automatically docks the sub with the outpost at the end of the level. WARNING: does not take outposts into account, so often leads to physics glitches. Only use for debugging.", + onExecute:(string[] args) => { if (Submarine.MainSub == null) { return; } @@ -1232,7 +1267,7 @@ namespace Barotrauma } } }, - () => + getValidArgs:() => { return new string[][] { @@ -1923,6 +1958,7 @@ namespace Barotrauma })); #if DEBUG + commands.Add(new Command("debugvoip", "Toggle the server writing VOIP into audio files.", null, isCheat: false)); commands.Add(new Command("simulatedlongloadingtime", "simulatedlongloadingtime [minimum loading time]: forces loading a round to take at least the specified amount of seconds.", (string[] args) => { @@ -2046,6 +2082,11 @@ namespace Barotrauma string[] validArgs = allArgs[autoCompletedArgIndex].Where(arg => currentAutoCompletedCommand.Trim().Length <= arg.Length && arg.Substring(0, currentAutoCompletedCommand.Trim().Length).ToLower() == currentAutoCompletedCommand.Trim().ToLower()).ToArray(); + + // add all completions that contain the current argument, to the end of the list + validArgs = validArgs.Concat(allArgs[autoCompletedArgIndex].Where(arg => + arg.ToLower().Contains(currentAutoCompletedCommand.Trim().ToLower()) && + !validArgs.Contains(arg))).ToArray(); if (validArgs.Length == 0) { return command; } @@ -2094,95 +2135,219 @@ namespace Barotrauma currentAutoCompletedIndex = 0; } - public static void ExecuteCommand(string command) + /// + /// Executes the specific command or commands + /// + /// Command, or multiple commands separated by newlines. + public static void ExecuteCommand(string inputtedCommands) { - if (activeQuestionCallback != null) + if (string.IsNullOrWhiteSpace(inputtedCommands) || inputtedCommands == "\\" || inputtedCommands == "\n") { return; } + + string[] commandsToExecute = inputtedCommands.Split("\n"); + foreach (string command in commandsToExecute) { -#if CLIENT - activeQuestionText = null; -#endif - NewCommand(command); - //reset the variable before invoking the delegate because the method may need to activate another question - var temp = activeQuestionCallback; - activeQuestionCallback = null; - temp(command); - return; - } - - if (string.IsNullOrWhiteSpace(command) || command == "\\" || command == "\n") { return; } - - string[] splitCommand = ToolBox.SplitCommand(command); - if (splitCommand.Length == 0) - { - ThrowError("Failed to execute command \"" + command + "\"!"); - GameAnalyticsManager.AddErrorEventOnce( - "DebugConsole.ExecuteCommand:LengthZero", - GameAnalyticsManager.ErrorSeverity.Error, - "Failed to execute command \"" + command + "\"!"); - return; - } - - Identifier firstCommand = splitCommand[0].ToIdentifier(); - - if (firstCommand != "admin") - { - NewCommand(command); - } - -#if CLIENT - if (GameMain.Client != null) - { - Command matchingCommand = commands.Find(c => c.Names.Contains(firstCommand)); - if (matchingCommand == null) + if (activeQuestionCallback != null) { - //if the command is not defined client-side, we'll relay it anyway because it may be a custom command at the server's side - GameMain.Client.SendConsoleCommand(command); - NewMessage("Server command: " + command, Color.Cyan); +#if CLIENT + activeQuestionText = null; +#endif + NewCommand(command); + //reset the variable before invoking the delegate because the method may need to activate another question + var temp = activeQuestionCallback; + activeQuestionCallback = null; + temp(command); return; } - else if (GameMain.Client.HasConsoleCommandPermission(firstCommand)) + + if (string.IsNullOrWhiteSpace(command) || command == "\\") { return; } + + string[] splitCommand = ToolBox.SplitCommand(command); + if (splitCommand.Length == 0) { - if (matchingCommand.RelayToServer) + ThrowError("Failed to execute command \"" + command + "\"!"); + GameAnalyticsManager.AddErrorEventOnce( + "DebugConsole.ExecuteCommand:LengthZero", + GameAnalyticsManager.ErrorSeverity.Error, + "Failed to execute command \"" + command + "\"!"); + return; + } + + Identifier firstCommand = splitCommand[0].ToIdentifier(); + + if (firstCommand != "admin") + { + NewCommand(command); + } + +#if CLIENT + if (GameMain.Client != null) + { + Command matchingCommand = commands.Find(c => c.Names.Contains(firstCommand)); + if (matchingCommand == null) { + //if the command is not defined client-side, we'll relay it anyway because it may be a custom command at the server's side GameMain.Client.SendConsoleCommand(command); NewMessage("Server command: " + command, Color.Cyan); + return; } - else + else if (GameMain.Client.HasConsoleCommandPermission(firstCommand)) { - matchingCommand.ClientExecute(splitCommand.Skip(1).ToArray()); + if (matchingCommand.RelayToServer) + { + GameMain.Client.SendConsoleCommand(command); + NewMessage("Server command: " + command, Color.Cyan); + } + else + { + matchingCommand.ClientExecute(splitCommand.Skip(1).ToArray()); + } + return; } - return; - } - if (!IsCommandPermitted(firstCommand, GameMain.Client)) - { + if (!IsCommandPermitted(firstCommand, GameMain.Client)) + { #if DEBUG - AddWarning($"You're not permitted to use the command \"{firstCommand}\". Executing the command anyway because this is a debug build."); + AddWarning($"You're not permitted to use the command \"{firstCommand}\". Executing the command anyway because this is a debug build."); #else ThrowError($"You're not permitted to use the command \"{firstCommand}\"!"); return; #endif + } } - } #endif - bool commandFound = false; - foreach (Command c in commands) - { - if (!c.Names.Contains(firstCommand)) { continue; } - c.Execute(splitCommand.Skip(1).ToArray()); - commandFound = true; - break; - } + bool commandFound = false; + foreach (Command c in commands) + { + if (!c.Names.Contains(firstCommand)) { continue; } + c.Execute(splitCommand.Skip(1).ToArray()); + commandFound = true; + break; + } - if (!commandFound) - { - ThrowError("Command \"" + splitCommand[0] + "\" not found."); + if (!commandFound) + { + ThrowError("Command \"" + splitCommand[0] + "\" not found."); + } } } private static string[] ListCharacterNames() => Character.CharacterList.OrderBy(c => c.IsDead).ThenByDescending(c => c.IsHuman).ThenBy(c => c.Name).Select(c => c.Name).Distinct().ToArray(); + + private static string[] ListAvailableLocations() + { + List locationNames = new(); + foreach(var submarine in Submarine.Loaded) + { + locationNames.Add(submarine.Info.Name); + } - private static Character FindMatchingCharacter(string[] args, bool ignoreRemotePlayers = false, Client allowedRemotePlayer = null) + if (Level.Loaded != null) + { + foreach (var cave in Level.Loaded.Caves) + { + string caveName = cave.CaveGenerationParams.Name; + // add index in case there are duplicate names + int index = 1; + while (locationNames.Contains($"{caveName}_{index}")) + { + index++; + } + locationNames.Add($"{caveName}_{index}"); + } + } + + return locationNames.ToArray(); + } + + private static bool TryFindTeleportPosition(string locationName, out Vector2 teleportPosition) + { + if (Submarine.MainSub is Submarine mainSub && string.Equals(locationName, "mainsub", StringComparison.InvariantCultureIgnoreCase)) + { + var randomWaypoint = GetRandomWaypoint(mainSub.GetWaypoints(alsoFromConnectedSubs:false)); + if (randomWaypoint != null) + { + teleportPosition = randomWaypoint.WorldPosition; + return true; + } + LogError("No waypoints found in the main sub!"); + } + + foreach (var submarine in Submarine.Loaded) + { + if (string.Equals(submarine.Info.Name, locationName, StringComparison.InvariantCultureIgnoreCase)) + { + var randomWaypoint = GetRandomWaypoint(submarine.GetWaypoints(alsoFromConnectedSubs:false)); + if (randomWaypoint != null) + { + teleportPosition = randomWaypoint.WorldPosition; + return true; + } + LogError($"No waypoints found in sub {submarine.Info.Name}!"); + } + } + + if (Level.Loaded is Level loadedLevel) + { + (string locationNameNoIndex, int locationIndex) = SplitIndex(locationName); + int caveIndex = 1; + foreach (var cave in loadedLevel.Caves) + { + if (string.Equals(cave.CaveGenerationParams.Name, locationNameNoIndex, StringComparison.InvariantCultureIgnoreCase)) + { + if (caveIndex != locationIndex) + { + caveIndex++; + continue; + } + + var randomWaypoint = GetRandomWaypoint(cave.Tunnels.GetRandom(Rand.RandSync.Unsynced).WayPoints); + if (randomWaypoint != null) + { + teleportPosition = randomWaypoint.WorldPosition; + return true; + } + LogError($"No waypoints found in cave {cave.CaveGenerationParams.Name}!"); + } + } + } + teleportPosition = Vector2.Zero; + return false; + + WayPoint GetRandomWaypoint(IReadOnlyList waypoints) + { + if (waypoints.None()) + { + return null; + } + + if (waypoints.Any(point => point.SpawnType == SpawnType.Human)) + { + return waypoints.GetRandom(point => point.SpawnType == SpawnType.Human, Rand.RandSync.Unsynced); + } + + if (waypoints.Any(point => point.SpawnType == SpawnType.Path)) + { + return waypoints.GetRandom(point => point.SpawnType == SpawnType.Path, Rand.RandSync.Unsynced); + } + + return waypoints.GetRandom(Rand.RandSync.Unsynced); + } + + (string, int) SplitIndex(string caveName) + { + string[] splitName = caveName.Split('_'); + if (splitName.Length == 1) + { + return (splitName[0], -1); + } + else + { + return (splitName[0], int.Parse(splitName[1])); + } + } + } + + private static Character FindMatchingCharacter(string[] args, bool ignoreRemotePlayers = false, Client allowedRemotePlayer = null, bool botsOnly = false) { if (args.Length == 0) return null; @@ -2201,6 +2366,11 @@ namespace Barotrauma c.Name.Equals(characterName, StringComparison.OrdinalIgnoreCase) && (!c.IsRemotePlayer || !ignoreRemotePlayers || allowedRemotePlayer?.Character == c)); + if (botsOnly) + { + matchingCharacters = matchingCharacters.FindAll(c => c is AICharacter); + } + if (!matchingCharacters.Any()) { NewMessage("Character \""+ characterName + "\" not found", Color.Red); @@ -2231,6 +2401,68 @@ namespace Barotrauma return null; } + + private static void TeleportCharacter(Vector2 cursorWorldPos, Character controlledCharacter, string[] args) + { + if (Screen.Selected != GameMain.GameScreen) + { + NewMessage("Cannot teleport a character in the menu or the editor screens.", color: Color.Yellow); + return; + } + + Character targetCharacter = controlledCharacter; + Vector2 worldPosition = cursorWorldPos; + string locationNameArgument = ""; + + var availableLocations = ListAvailableLocations(); + if (args.Length > 0) + { + if (args.Length > 1) + { + // remove location name from args + if (availableLocations.Contains(args.Last()) + || string.Equals(args.Last(), "mainsub", StringComparison.InvariantCultureIgnoreCase)) + { + locationNameArgument = args.Last(); + args = args.Take(args.Length - 1).ToArray(); + } + else + { + NewMessage("Invalid arguments", color: Color.Yellow); + return; + } + } + + // the remaining args should be the character name and a possible index + if (args[0].ToLowerInvariant() != "me") + { + Character match = FindMatchingCharacter(args, ignoreRemotePlayers:false); + targetCharacter = match; + } + } + + if (!string.IsNullOrWhiteSpace(locationNameArgument)) + { + if (TryFindTeleportPosition(locationNameArgument, out Vector2 teleportPosition)) + { + worldPosition = teleportPosition; + } + else + { + ThrowError($"No teleport position for location \"{locationNameArgument}\" was found."); + return; + } + } + + if (targetCharacter != null) + { + targetCharacter.TeleportTo(worldPosition); + } + else + { + NewMessage("Invalid arguments", color: Color.Yellow); + } + } private static void SpawnCharacter(string[] args, Vector2 cursorWorldPos, out string errorMsg) { @@ -2252,8 +2484,8 @@ namespace Barotrauma { job = JobPrefab.Prefabs[characterLowerCase]; } - bool human = job != null || characterLowerCase == CharacterPrefab.HumanSpeciesName; - + bool isHuman = job != null || characterLowerCase == CharacterPrefab.HumanSpeciesName; + bool addToCrew = false; if (args.Length > 1) { switch (args[1].ToLowerInvariant()) @@ -2287,13 +2519,17 @@ namespace Barotrauma spawnPosition = cursorWorldPos; break; default: - spawnPoint = WayPoint.GetRandom(human ? SpawnType.Human : SpawnType.Enemy); + spawnPoint = WayPoint.GetRandom(isHuman ? SpawnType.Human : SpawnType.Enemy); break; } + addToCrew = + args.Length > 3 ? + args[3].Equals("true", StringComparison.OrdinalIgnoreCase) : + isHuman; } else { - spawnPoint = WayPoint.GetRandom(human ? SpawnType.Human : SpawnType.Enemy); + spawnPoint = WayPoint.GetRandom(isHuman ? SpawnType.Human : SpawnType.Enemy); } if (string.IsNullOrWhiteSpace(args[0])) { return; } @@ -2312,29 +2548,36 @@ namespace Barotrauma if (spawnPoint != null) { spawnPosition = spawnPoint.WorldPosition; } - if (human) + if (isHuman) { var variant = job != null ? Rand.Range(0, job.Variants, Rand.RandSync.ServerAndClient) : 0; CharacterInfo characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, variant: variant); spawnedCharacter = Character.Create(characterInfo, spawnPosition, ToolBox.RandomSeed(8)); - if (GameMain.GameSession != null) - { - spawnedCharacter.TeamID = teamType; -#if CLIENT - GameMain.GameSession.CrewManager.AddCharacter(spawnedCharacter); -#endif - } + spawnedCharacter.GiveJobItems(spawnPoint); spawnedCharacter.GiveIdCardTags(spawnPoint); spawnedCharacter.Info.StartItemsGiven = true; } else { - if (CharacterPrefab.FindBySpeciesName(args[0].ToIdentifier()) != null) + CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(args[0].ToIdentifier()); + if (prefab != null) { - Character.Create(args[0], spawnPosition, ToolBox.RandomSeed(8)); + CharacterInfo characterInfo = null; + if (prefab.HasCharacterInfo) + { + characterInfo = new CharacterInfo(prefab.Identifier); + } + spawnedCharacter = Character.Create(args[0], spawnPosition, ToolBox.RandomSeed(8), characterInfo); } } + if (addToCrew && GameMain.GameSession != null) + { + spawnedCharacter.TeamID = teamType; +#if CLIENT + GameMain.GameSession.CrewManager.AddCharacter(spawnedCharacter); +#endif + } } private static void SpawnItem(string[] args, Vector2 cursorPos, Character controlledCharacter, out string errorMsg) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index 2f71320e2..42a47e8de 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -253,6 +253,11 @@ namespace Barotrauma /// SwimmingSpeed, + /// + /// Increases the character's speed by a percentage when using an item that propels the character forwards (such as a diving scooter). + /// + PropulsionSpeed, + /// /// Decreases how long it takes for buffs applied to the character decay over time by a percentage. /// Buffs are afflictions that have isBuff set to true. diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs index e178bd9f6..cb10fd66a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs @@ -107,6 +107,11 @@ namespace Barotrauma { if (spawnPending) { + if (itemPrefab == null) + { + isFinished = true; + return; + } SpawnItem(); spawnPending = false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs index d3124db34..987932e55 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs @@ -1,4 +1,4 @@ -using Barotrauma.Extensions; +using Barotrauma.Extensions; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -30,7 +30,7 @@ namespace Barotrauma { if (TargetTag.IsEmpty) { - DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction with no target tag! This will cause the check to automatically succeed.", + DebugConsole.LogError($"CheckConditionalAction error: {GetEventDebugName()} uses a CheckConditionalAction with no target tag! This will cause the check to automatically succeed.", contentPackage: parentEvent.Prefab.ContentPackage); } var conditionalElements = element.GetChildElements("Conditional"); @@ -52,7 +52,7 @@ namespace Barotrauma if (Conditionals.None()) { - DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction with no valid PropertyConditional! This will cause the check to automatically succeed.", + DebugConsole.LogError($"CheckConditionalAction error: {GetEventDebugName()} uses a CheckConditionalAction with no valid PropertyConditional! This will cause the check to automatically succeed.", contentPackage: parentEvent.Prefab.ContentPackage); } @@ -67,11 +67,6 @@ namespace Barotrauma } } - private string GetEventName() - { - return ParentEvent?.Prefab?.Identifier is { IsEmpty: false } identifier ? $"the event \"{identifier}\"" : "an unknown event"; - } - protected override bool? DetermineSuccess() { IEnumerable targets = null; @@ -82,7 +77,7 @@ namespace Barotrauma if (targets.None()) { - DebugConsole.LogError($"{nameof(CheckConditionalAction)} error: {GetEventName()} uses a {nameof(CheckConditionalAction)} but no valid target was found for tag \"{TargetTag}\"! This will cause the check to automatically succeed.", + DebugConsole.LogError($"{nameof(CheckConditionalAction)} error: {GetEventDebugName()} uses a {nameof(CheckConditionalAction)} but no valid target was found for tag \"{TargetTag}\"! This will cause the check to automatically succeed.", contentPackage: ParentEvent.Prefab.ContentPackage); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDifficultyAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDifficultyAction.cs new file mode 100644 index 000000000..a120c66ca --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDifficultyAction.cs @@ -0,0 +1,35 @@ +#nullable enable +namespace Barotrauma; + +/// +/// Check whether the difficulty of the current level is within some specific range. +/// +class CheckDifficultyAction : BinaryOptionAction +{ + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Minimum difficulty of the current level for the check to succeed.")] + public float MinDifficulty { get; set; } + + [Serialize(100.0f, IsPropertySaveable.Yes, description: "Maximum difficulty of the current level for the check to succeed.")] + public float MaxDifficulty { get; set; } + + public CheckDifficultyAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + if (MaxDifficulty <= MinDifficulty) + { + DebugConsole.LogError($"Potential error in event {GetEventDebugName()}: maximum difficulty ({MaxDifficulty}) is not larger than minimum difficulty ({MinDifficulty}) in {nameof(CheckDifficultyAction)}.", + contentPackage: parentEvent.Prefab.ContentPackage); + } + } + + protected override bool? DetermineSuccess() + { + if (Level.Loaded == null) { return false; } + return Level.Loaded.Difficulty >= MinDifficulty && Level.Loaded.Difficulty <= MaxDifficulty; + } + + public override string ToDebugString() + { + return $"{ToolBox.GetDebugSymbol(DetermineFinished())} {nameof(CheckDifficultyAction)} -> (min: {MinDifficulty}, max: {MaxDifficulty}" + + $" Succeeded: {(succeeded.HasValue ? succeeded.Value.ToString() : "not determined").ColorizeObject()})"; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs index 779f9b4f2..b9a819843 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs @@ -1,4 +1,4 @@ -using Barotrauma.Extensions; +using Barotrauma.Extensions; using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System.Collections.Generic; @@ -36,6 +36,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes, description: "Does the item need to be equipped for the check to succeed?")] public bool RequireEquipped { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "Does the item need to be worn for the check to succeed?")] + public bool RequireWorn { get; set; } + [Serialize(true, IsPropertySaveable.Yes, description: "If enabled, the doesn't need to be directly inside the container/character we're checking, but can be nested inside multiple containers (e.g. in a toolbelt in a character's inventory).")] public bool Recursive { get; set; } @@ -256,6 +259,19 @@ namespace Barotrauma if (character == null) { return false; } return character.HasEquippedItem(item); } + if (RequireWorn) + { + if (character == null) { return false; } + foreach (var wearable in item.GetComponents()) + { + foreach (var allowedSlot in wearable.AllowedSlots) + { + if (allowedSlot == InvSlotType.Any) { continue; } + if (character.HasEquippedItem(item, allowedSlot)) { return true; } + } + } + return false; + } return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs index 93511edb4..dd9c5b7a0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs @@ -1,4 +1,4 @@ -using Barotrauma.Extensions; +using Barotrauma.Extensions; using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; @@ -134,6 +134,10 @@ namespace Barotrauma else { text = TextManager.Get(Text).Fallback(Text); + if (text.Value.IsNullOrEmpty()) + { + text = text.Fallback(Text); + } } return ParentEvent.ReplaceVariablesInEventText(text); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/DamageBeaconStationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/DamageBeaconStationAction.cs new file mode 100644 index 000000000..4a159fbc3 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/DamageBeaconStationAction.cs @@ -0,0 +1,55 @@ +#nullable enable +namespace Barotrauma; + + +/// +/// Can be used to disconnect wires and break devices and walls in beacon stations. Useful if you want the beacon to be in tact by default, and use events to determine whether it should be e.g. manned by bandits, or destroyed and infested by monsters. +/// +class DamageBeaconStationAction : EventAction +{ + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Probability of disconnecting wires (0.5 = 50% chance of disconnecting any given wire, 1 = all wires disconnected).")] + public float DisconnectWireProbability { get; set; } + + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Probability of a wall sections leaking (0.5 = 50% creating a leak on any given wall section, 1 = all walls leak).")] + public float DamageWallProbability { get; set; } + + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Probability of devices being damaged (0.5 = 50% chance of damaging any given devices, 1 = all devices are damaged).")] + public float DamageDeviceProbability { get; set; } + + private bool isFinished; + + public DamageBeaconStationAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + if (DisconnectWireProbability <= 0.0f && DamageWallProbability <= 0.0f && DamageDeviceProbability <= 0.0f) + { + DebugConsole.LogError($"Potential error in event {GetEventDebugName()}: {DisconnectWireProbability}, {DamageWallProbability} and {DamageDeviceProbability} are all set to 0 in {nameof(DamageBeaconStationAction)}, and the action will do nothing.", + contentPackage: parentEvent.Prefab.ContentPackage); + } + } + + public override bool IsFinished(ref string goToLabel) => isFinished; + + public override void Reset() + { + isFinished = false; + } + + public override void Update(float deltaTime) + { + if (isFinished) { return; } + + if (Level.Loaded != null) + { + Level.Loaded.DisconnectBeaconStationWires(DisconnectWireProbability); + Level.Loaded.DamageBeaconStationWalls(DamageWallProbability); + Level.Loaded.DamageBeaconStationDevices(DamageDeviceProbability); + } + + isFinished = true; + } + + public override string ToDebugString() + { + return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(DamageBeaconStationAction)}"; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs index f9be2652e..97e01c2b3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs @@ -198,6 +198,12 @@ namespace Barotrauma } } + protected string GetEventDebugName() + { + return ParentEvent?.Prefab?.Identifier is { IsEmpty: false } identifier ? $"the event \"{identifier}\"" : "an unknown event"; + } + + /// /// Rich test to display in debugdraw /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCOperateItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCOperateItemAction.cs index d19cd7c52..472b60461 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCOperateItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCOperateItemAction.cs @@ -1,4 +1,4 @@ -using Barotrauma.Extensions; +using Barotrauma.Extensions; using Barotrauma.Items.Components; using System.Collections.Generic; using System.Linq; @@ -31,6 +31,9 @@ namespace Barotrauma [Serialize(-1, IsPropertySaveable.Yes, description: "Maximum number of NPCs the action can target. For example, you could only make a specific number of security officers man a periscope.")] public int MaxTargets { get; set; } + [Serialize(100, IsPropertySaveable.Yes, description: "Priority of operating the item (0-100). Higher values will make the AI prefer operating the item over other orders (priority 60-70) or e.g. reacting to emergencies (priority 90).")] + public int Priority { get; set; } + [Serialize(true, IsPropertySaveable.Yes, description: "The event actions reset when a GoTo action makes the event jump to a different point. Should the NPC stop operating the item when the event resets?")] public bool AbandonOnReset { get; set; } @@ -72,7 +75,7 @@ namespace Barotrauma { var newObjective = new AIObjectiveOperateItem(itemComponent, npc, humanAiController.ObjectiveManager, OrderOption, RequireEquip) { - OverridePriority = 100.0f + OverridePriority = Priority }; humanAiController.ObjectiveManager.AddObjective(newObjective); humanAiController.ObjectiveManager.WaitTimer = 0.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs index a2046bd0e..e607b2b7c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs @@ -68,6 +68,7 @@ namespace Barotrauma ("bot", v => TagBots(playerCrewOnly: false)), ("crew", v => TagCrew()), ("humanprefabidentifier", TagHumansByIdentifier), + ("humanprefabtag", TagHumansByTag), ("jobidentifier", TagHumansByJobIdentifier), ("structureidentifier", TagStructuresByIdentifier), ("structurespecialtag", TagStructuresBySpecialTag), @@ -153,6 +154,11 @@ namespace Barotrauma AddTarget(Tag, Character.CharacterList.Where(c => c.HumanPrefab?.Identifier == identifier)); } + private void TagHumansByTag(Identifier tag) + { + AddTarget(Tag, Character.CharacterList.Where(c => c.HumanPrefab != null && c.HumanPrefab.GetTags().Contains(tag))); + } + private void TagHumansByJobIdentifier(Identifier jobIdentifier) { AddTarget(Tag, Character.CharacterList.Where(c => c.HasJob(jobIdentifier))); @@ -217,6 +223,7 @@ namespace Barotrauma private bool IsValidItem(Item it) { return + !it.IsLayerHidden && /*items in hidden layers are treated as if they didn't exist, regardless if hidden items should be allowed*/ (!it.HiddenInGame || AllowHiddenItems) && ModuleTagMatches(it) && //if the item has just spawned, it may be in a hull but not moved into the coordinate space of the hull yet diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index 556eb3474..83b2ff4d3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -424,7 +424,7 @@ namespace Barotrauma public void RegisterEventHistory(bool registerFinishedOnly = false) { if (level?.LevelData == null) { return; } - + level.LevelData.EventsExhausted = !registerFinishedOnly; if (level.LevelData.Type == LevelData.LevelType.Outpost) @@ -433,15 +433,15 @@ namespace Barotrauma { foreach (var finishedEvent in finishedEvents) { - var key = finishedEvent.ParentSet; - if (key == null) { continue; } - if (level.LevelData.FinishedEvents.ContainsKey(key)) + EventSet parentSet = finishedEvent.ParentSet; + if (parentSet == null) { continue; } + if (parentSet.Exhaustible) { - level.LevelData.FinishedEvents[key] += 1; + level.LevelData.EventsExhausted = true; } - else + if (!level.LevelData.FinishedEvents.TryAdd(parentSet, 1)) { - level.LevelData.FinishedEvents.Add(key, 1); + level.LevelData.FinishedEvents[parentSet] += 1; } } } @@ -691,6 +691,7 @@ namespace Barotrauma { return (e.BiomeIdentifier.IsEmpty || e.BiomeIdentifier == level.LevelData?.Biome?.Identifier) && + (e.RequiredLayer.IsEmpty || Submarine.LayerExistsInAnySub(e.RequiredLayer)) && !level.LevelData.NonRepeatableEvents.Contains(e.Identifier); } @@ -702,8 +703,9 @@ namespace Barotrauma private static bool IsValidForLevel(EventSet eventSet, Level level) { return - level.Difficulty >= eventSet.MinLevelDifficulty && level.Difficulty <= eventSet.MaxLevelDifficulty && + level.IsAllowedDifficulty(eventSet.MinLevelDifficulty, eventSet.MaxLevelDifficulty) && level.LevelData.Type == eventSet.LevelType && + (eventSet.RequiredLayer.IsEmpty || Submarine.LayerExistsInAnySub(eventSet.RequiredLayer)) && (eventSet.BiomeIdentifier.IsEmpty || eventSet.BiomeIdentifier == level.LevelData.Biome.Identifier); } @@ -953,7 +955,7 @@ namespace Barotrauma monsterStrength = 0; foreach (Character character in Character.CharacterList) { - if (character.IsIncapacitated || character.IsArrested || !character.Enabled || character.IsPet) { continue; } + if (character.IsIncapacitated || character.IsHandcuffed || !character.Enabled || character.IsPet) { continue; } if (character.AIController is EnemyAIController enemyAI) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs index f6a8847ae..afc0b5645 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs @@ -32,6 +32,11 @@ namespace Barotrauma /// public readonly Identifier BiomeIdentifier; + /// + /// If set, this layer must be present somewhere in the level. + /// + public readonly Identifier RequiredLayer; + /// /// If set, the event set can only be chosen in locations that belong to this faction. /// @@ -94,6 +99,8 @@ namespace Barotrauma Probability = Math.Clamp(element.GetAttributeFloat(1.0f, "probability", "spawnprobability"), 0, 1); TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", EventType != typeof(ScriptedEvent)); + RequiredLayer = element.GetAttributeIdentifier(nameof(RequiredLayer), Identifier.Empty); + UnlockPathEvent = element.GetAttributeBool("unlockpathevent", false); UnlockPathTooltip = element.GetAttributeString("unlockpathtooltip", "lockedpathtooltip"); UnlockPathReputation = element.GetAttributeInt("unlockpathreputation", 0); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs index 6aed0f365..b1354868c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs @@ -106,6 +106,11 @@ namespace Barotrauma ///
public readonly LevelData.LevelType LevelType; + /// + /// If set, this layer must be present somewhere in the level. + /// + public readonly Identifier RequiredLayer; + /// /// If set, the event set can only be chosen in locations of this type. /// @@ -368,7 +373,7 @@ namespace Barotrauma ChooseRandom = element.GetAttributeBool("chooserandom", false); eventCount = element.GetAttributeInt("eventcount", 1); SubSetCount = element.GetAttributeInt("setcount", 1); - Exhaustible = element.GetAttributeBool("exhaustible", false); + Exhaustible = element.GetAttributeBool("exhaustible", parentSet?.Exhaustible ?? false); MinDistanceTraveled = element.GetAttributeFloat("mindistancetraveled", 0.0f); MinMissionTime = element.GetAttributeFloat("minmissiontime", 0.0f); @@ -386,6 +391,8 @@ namespace Barotrauma ResetTime = element.GetAttributeFloat(nameof(ResetTime), parentSet?.ResetTime ?? 0); CampaignTutorialOnly = element.GetAttributeBool(nameof(CampaignTutorialOnly), parentSet?.CampaignTutorialOnly ?? false); + RequiredLayer = element.GetAttributeIdentifier(nameof(RequiredLayer), Identifier.Empty); + ForceAtDiscoveredNr = element.GetAttributeInt(nameof(ForceAtDiscoveredNr), -1); ForceAtVisitedNr = element.GetAttributeInt(nameof(ForceAtVisitedNr), -1); if (ForceAtDiscoveredNr >= 0 && ForceAtVisitedNr >= 0) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EliminateTargetsMission.cs similarity index 78% rename from Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs rename to Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EliminateTargetsMission.cs index d0c52b245..cc7855701 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EliminateTargetsMission.cs @@ -7,7 +7,8 @@ using System.Linq; namespace Barotrauma { - partial class AlienRuinMission : Mission + [TypePreviouslyKnownAs("AlienRuinMission")] + partial class EliminateTargetsMission : Mission { private readonly Identifier[] targetItemIdentifiers; private readonly Identifier[] targetEnemyIdentifiers; @@ -16,7 +17,10 @@ namespace Barotrauma private readonly HashSet spawnedTargets = new HashSet(); private readonly HashSet allTargets = new HashSet(); - private Ruin TargetRuin { get; set; } + public readonly SubmarineType TargetSubType; + public readonly bool PrioritizeThalamus; + + private Submarine TargetSub { get; set; } public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels { @@ -35,11 +39,13 @@ namespace Barotrauma } } - public AlienRuinMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) + public EliminateTargetsMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { targetItemIdentifiers = prefab.ConfigElement.GetAttributeIdentifierArray("targetitems", Array.Empty()); targetEnemyIdentifiers = prefab.ConfigElement.GetAttributeIdentifierArray("targetenemies", Array.Empty()); minEnemyCount = prefab.ConfigElement.GetAttributeInt("minenemycount", 0); + TargetSubType = prefab.ConfigElement.GetAttributeEnum("targetsub", SubmarineType.Ruin); + PrioritizeThalamus = prefab.RequireThalamusWreck; } protected override void StartMissionSpecific(Level level) @@ -48,23 +54,48 @@ namespace Barotrauma spawnedTargets.Clear(); allTargets.Clear(); if (IsClient) { return; } - TargetRuin = Level.Loaded?.Ruins?.GetRandom(randSync: Rand.RandSync.ServerAndClient); - if (TargetRuin == null) + + TargetSub = TargetSubType switch { - DebugConsole.ThrowError($"Failed to initialize an Alien Ruin mission (\"{Prefab.Identifier}\"): level contains no alien ruins", + SubmarineType.Wreck => FindWreck(), + SubmarineType.Ruin => Level.Loaded?.Ruins?.GetRandom(Rand.RandSync.ServerAndClient).Submarine, + SubmarineType.BeaconStation => Level.Loaded?.BeaconStation, + _ => null + }; + + Submarine FindWreck() + { + var wrecks = Level.Loaded?.Wrecks; + if (wrecks == null || wrecks.None()) { return null; } + + if (PrioritizeThalamus) + { + var thalamusWrecks = wrecks.Where(w => w.WreckAI != null).ToArray(); + if (thalamusWrecks.Any()) + { + return thalamusWrecks.GetRandom(Rand.RandSync.ServerAndClient); + } + } + + return wrecks.GetRandom(Rand.RandSync.ServerAndClient); + } + + if (TargetSub == null) + { + DebugConsole.ThrowError($"Failed to initialize an {nameof(EliminateTargetsMission)} mission (\"{Prefab.Identifier}\"): level contains no submarines", contentPackage: Prefab.ContentPackage); return; } if (targetItemIdentifiers.Length < 1 && targetEnemyIdentifiers.Length < 1) { - DebugConsole.ThrowError($"Failed to initialize an Alien Ruin mission (\"{Prefab.Identifier}\"): no target identifiers set in the mission definition", + DebugConsole.ThrowError($"Failed to initialize an {nameof(EliminateTargetsMission)} mission (\"{Prefab.Identifier}\"): no target identifiers set in the mission definition", contentPackage: Prefab.ContentPackage); return; } foreach (var item in Item.ItemList) { if (!targetItemIdentifiers.Contains(item.Prefab.Identifier)) { continue; } - if (item.Submarine != TargetRuin.Submarine) { continue; } + if (item.Submarine != TargetSub) { continue; } existingTargets.Add(item); allTargets.Add(item); } @@ -73,7 +104,7 @@ namespace Barotrauma { if (character.SpeciesName.IsEmpty) { continue; } if (!targetEnemyIdentifiers.Contains(character.SpeciesName)) { continue; } - if (character.Submarine != TargetRuin.Submarine) { continue; } + if (character.Submarine != TargetSub) { continue; } existingTargets.Add(character); allTargets.Add(character); existingEnemyCount++; @@ -103,7 +134,7 @@ namespace Barotrauma for (int i = 0; i < (minEnemyCount - existingEnemyCount); i++) { var prefab = enemyPrefabs.GetRandomUnsynced(); - var spawnPos = TargetRuin.Submarine.GetWaypoints(false).GetRandomUnsynced(w => w.CurrentHull != null)?.WorldPosition; + var spawnPos = TargetSub.GetWaypoints(false).GetRandomUnsynced(w => w.CurrentHull != null)?.WorldPosition; if (!spawnPos.HasValue) { DebugConsole.ThrowError($"Error in an Alien Ruin mission (\"{Prefab.Identifier}\"): no valid spawn positions could be found for the additional ({minEnemyCount - existingEnemyCount}) enemies to be spawned", diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index 060d47af6..2e324fa0b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -211,12 +211,12 @@ namespace Barotrauma public virtual void SetLevel(LevelData level) { } - public static Mission LoadRandom(Location[] locations, string seed, bool requireCorrectLocationType, MissionType missionType, bool isSinglePlayer = false) + public static Mission LoadRandom(Location[] locations, string seed, bool requireCorrectLocationType, MissionType missionType, bool isSinglePlayer = false, float? difficultyLevel = null) { - return LoadRandom(locations, new MTRandom(ToolBox.StringToInt(seed)), requireCorrectLocationType, missionType, isSinglePlayer); + return LoadRandom(locations, new MTRandom(ToolBox.StringToInt(seed)), requireCorrectLocationType, missionType, isSinglePlayer, difficultyLevel); } - public static Mission LoadRandom(Location[] locations, MTRandom rand, bool requireCorrectLocationType, MissionType missionType, bool isSinglePlayer = false) + public static Mission LoadRandom(Location[] locations, MTRandom rand, bool requireCorrectLocationType, MissionType missionType, bool isSinglePlayer = false, float? difficultyLevel = null) { List allowedMissions = new List(); if (missionType == MissionType.None) @@ -225,32 +225,20 @@ namespace Barotrauma } else { - allowedMissions.AddRange(MissionPrefab.Prefabs.Where(m => ((int)(missionType & m.Type)) != 0)); + allowedMissions.AddRange(MissionPrefab.Prefabs.Where(m => m.Type.HasAnyFlag(missionType))); } - - allowedMissions.RemoveAll(m => isSinglePlayer ? m.MultiplayerOnly : m.SingleplayerOnly); + allowedMissions.RemoveAll(m => isSinglePlayer ? m.MultiplayerOnly : m.SingleplayerOnly); if (requireCorrectLocationType) { allowedMissions.RemoveAll(m => !m.IsAllowed(locations[0], locations[1])); } - - if (allowedMissions.Count == 0) + if (difficultyLevel.HasValue) { - return null; + allowedMissions.RemoveAll(m => !m.IsAllowedDifficulty(difficultyLevel.Value)); } - - int probabilitySum = allowedMissions.Sum(m => m.Commonness); - int randomNumber = rand.NextInt32() % probabilitySum; - foreach (MissionPrefab missionPrefab in allowedMissions) - { - if (randomNumber <= missionPrefab.Commonness) - { - return missionPrefab.Instantiate(locations, Submarine.MainSub); - } - randomNumber -= missionPrefab.Commonness; - } - - return null; + if (allowedMissions.Count == 0) { return null; } + MissionPrefab missionPrefab = ToolBox.SelectWeightedRandom(allowedMissions, m => m.Commonness, rand); + return missionPrefab.Instantiate(locations, Submarine.MainSub); } /// @@ -288,7 +276,7 @@ namespace Barotrauma { foreach (MapEntity entityToShow in MapEntity.MapEntityList.Where(me => me.Prefab?.HasSubCategory(categoryToShow) ?? false)) { - entityToShow.HiddenInGame = false; + entityToShow.IsLayerHidden = false; } } this.level = level; @@ -381,9 +369,12 @@ namespace Barotrauma /// public void End() { - completed = - DetermineCompleted() && - (completeCheckDataAction == null ||completeCheckDataAction.GetSuccess()); + if (GameMain.NetworkMember is not { IsClient: true }) + { + completed = + DetermineCompleted() && + (completeCheckDataAction == null ||completeCheckDataAction.GetSuccess()); + } if (completed) { if (Prefab.LocationTypeChangeOnCompleted != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs index 96454bb6f..e0462ed76 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -23,9 +23,9 @@ namespace Barotrauma Pirate = 0x200, GoTo = 0x400, ScanAlienRuins = 0x800, - ClearAlienRuins = 0x1000, + EliminateTargets = 0x1000, End = 0x2000, - All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | AbandonedOutpost | Escort | Pirate | GoTo | ScanAlienRuins | ClearAlienRuins | End + All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | AbandonedOutpost | Escort | Pirate | GoTo | ScanAlienRuins | EliminateTargets | End } partial class MissionPrefab : PrefabWithUintIdentifier @@ -45,7 +45,7 @@ namespace Barotrauma { MissionType.Pirate, typeof(PirateMission) }, { MissionType.GoTo, typeof(GoToMission) }, { MissionType.ScanAlienRuins, typeof(ScanMission) }, - { MissionType.ClearAlienRuins, typeof(AlienRuinMission) }, + { MissionType.EliminateTargets, typeof(EliminateTargetsMission) }, { MissionType.End, typeof(EndMission) } }; public static readonly Dictionary PvPMissionClasses = new Dictionary() @@ -122,7 +122,7 @@ namespace Barotrauma public readonly bool AllowOtherMissionsInLevel; - public readonly bool RequireWreck, RequireRuin; + public readonly bool RequireWreck, RequireRuin, RequireThalamusWreck; /// /// If enabled, locations this mission takes place in cannot change their type @@ -216,6 +216,10 @@ namespace Barotrauma IsSideObjective = element.GetAttributeBool("sideobjective", false); RequireWreck = element.GetAttributeBool("requirewreck", false); RequireRuin = element.GetAttributeBool("requireruin", false); + RequireThalamusWreck = element.GetAttributeBool("requirethalamuswreck", false); + + if (RequireThalamusWreck) { RequireWreck = true; } + BlockLocationTypeChanges = element.GetAttributeBool(nameof(BlockLocationTypeChanges), false); RequiredLocationFaction = element.GetAttributeIdentifier(nameof(RequiredLocationFaction), Identifier.Empty); Commonness = element.GetAttributeInt("commonness", 1); @@ -360,11 +364,16 @@ namespace Barotrauma Identifier missionTypeName = element.GetAttributeIdentifier("type", Identifier.Empty); //backwards compatibility - if (missionTypeName == "outpostdestroy" || missionTypeName == "outpostrescue") - { - missionTypeName = "AbandonedOutpost".ToIdentifier(); - } + if (missionTypeName == "outpostdestroy" || missionTypeName == "outpostrescue") + { + missionTypeName = nameof(MissionType.AbandonedOutpost).ToIdentifier(); + } + else if (missionTypeName == "clearalienruins") + { + missionTypeName = nameof(MissionType.EliminateTargets).ToIdentifier(); + } + if (!Enum.TryParse(missionTypeName.Value, true, out Type)) { DebugConsole.ThrowErrorLocalized("Error in mission prefab \"" + Name + "\" - \"" + missionTypeName + "\" is not a valid mission type."); @@ -434,19 +443,13 @@ namespace Barotrauma } } - 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; } - } - else if (Type == MissionType.ScanAlienRuins || Type == MissionType.ClearAlienRuins) - { - var connection = from.Connections.Find(c => c.Locations.Contains(from) && c.Locations.Contains(to)); - if (connection?.LevelData == null || connection.LevelData.GenerationParams.GetMaxRuinCount() < 1) { return false; } - } - return false; } + + /// + /// Inclusive (matching the min an max values is accepted). + /// + public bool IsAllowedDifficulty(float difficulty) => difficulty >= MinLevelDifficulty && difficulty <= MaxLevelDifficulty; public Mission Instantiate(Location[] locations, Submarine sub) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index 8b3059af8..f4152f676 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -518,7 +518,9 @@ namespace Barotrauma contentPackage: Prefab.ContentPackage); continue; } - + + target.Item.DontCleanUp = true; + if (target.ParentTarget.Item.GetComponent() is ItemContainer container) { if (!container.Inventory.TryPutItem(target.Item, user: null)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index 139ae6c5f..13f857f12 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -533,7 +533,7 @@ namespace Barotrauma .Distinct(); public static IEnumerable FilterCargoCrates(IEnumerable items, Func conditional = null) - => items.Where(it => it.HasTag(Tags.Crate) && !it.NonInteractable && !it.NonPlayerTeamInteractable && !it.HiddenInGame && !it.Removed && (conditional == null || conditional(it))); + => items.Where(it => it.HasTag(Tags.Crate) && !it.NonInteractable && !it.NonPlayerTeamInteractable && !it.IsHidden && !it.Removed && (conditional == null || conditional(it))); public static IEnumerable FindReusableCargoContainers(IEnumerable subs, IEnumerable cargoRooms = null) => FilterCargoCrates(Item.ItemList, it => subs.Contains(it.Submarine) && !it.HasTag(Tags.CargoMissionItem) && (cargoRooms == null || cargoRooms.Contains(it.CurrentHull))) @@ -685,7 +685,14 @@ namespace Barotrauma var idCard = item.GetComponent(); if (cargoManager != null && idCard != null && purchased.BuyerCharacterInfoIdentifier != 0) { - cargoManager.purchasedIDCards.Add((purchased, idCard)); + if (purchased.DeliverImmediately) + { + InitPurchasedIDCard(purchased, idCard); + } + else + { + cargoManager.purchasedIDCards.Add((purchased, idCard)); + } } Submarine sub = item.Submarine ?? item.RootContainer?.Submarine; @@ -703,18 +710,23 @@ namespace Barotrauma { foreach ((PurchasedItem purchased, IdCard idCard) in purchasedIDCards) { - if (idCard != null && purchased.BuyerCharacterInfoIdentifier != 0) - { - var owner = Character.CharacterList.Find(c => c.Info?.GetIdentifier() == purchased.BuyerCharacterInfoIdentifier); - if (owner?.Info != null) - { - var mainSubSpawnPoints = WayPoint.SelectCrewSpawnPoints(new List() { owner.Info }, Submarine.MainSub); - idCard.Initialize(mainSubSpawnPoints.FirstOrDefault(), owner); - } - } + InitPurchasedIDCard(purchased, idCard); } } + private static void InitPurchasedIDCard(PurchasedItem purchased, IdCard idCard) + { + if (idCard != null && purchased.BuyerCharacterInfoIdentifier != 0) + { + var owner = Character.CharacterList.Find(c => c.Info?.GetIdentifier() == purchased.BuyerCharacterInfoIdentifier); + if (owner?.Info != null) + { + var mainSubSpawnPoints = WayPoint.SelectCrewSpawnPoints(new List() { owner.Info }, Submarine.MainSub); + idCard.Initialize(mainSubSpawnPoints.FirstOrDefault(), owner); + } + } + } + public static Vector2 GetCargoPos(Hull hull, ItemPrefab itemPrefab) { float floorPos = hull.Rect.Y - hull.Rect.Height; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index 2b5183312..f247879bd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -99,14 +99,20 @@ namespace Barotrauma { if (order.Identifier == Tags.DeconstructThis) { - Item.DeconstructItems.Add(item); + foreach (var stackedItem in item.GetStackedItems()) + { + Item.DeconstructItems.Add(stackedItem); + } #if CLIENT HintManager.OnItemMarkedForDeconstruction(order.OrderGiver); #endif } else { - Item.DeconstructItems.Remove(item); + foreach (var stackedItem in item.GetStackedItems()) + { + Item.DeconstructItems.Remove(stackedItem); + } } } } @@ -180,6 +186,17 @@ namespace Barotrauma DebugConsole.ThrowError("Tried to add a dead character to CrewManager!\n" + Environment.StackTrace.CleanupStackTrace()); return; } + if (character.Info == null) + { + if (character.Prefab.ContentPackage == GameMain.VanillaContent) + { + DebugConsole.ThrowError($"Added a character with no {nameof(CharacterInfo)} to the crew." + Environment.StackTrace.CleanupStackTrace()); + } + else + { + DebugConsole.ThrowError($"Added add a character with no {nameof(CharacterInfo)} to the crew. This may lead to issues: consider adding {nameof(CharacterPrefab.HasCharacterInfo)}=\"True\" to the character config."); + } + } if (!characters.Contains(character)) { @@ -275,11 +292,7 @@ namespace Barotrauma if (Level.Loaded != null && Level.Loaded.ShouldSpawnCrewInsideOutpost()) { - spawnWaypoints = WayPoint.WayPointList.FindAll(wp => - wp.SpawnType == SpawnType.Human && - wp.Submarine == Level.Loaded.StartOutpost && - wp.CurrentHull != null && - wp.CurrentHull.OutpostModuleTags.Contains("airlock".ToIdentifier())); + spawnWaypoints = GetOutpostSpawnpoints(); while (spawnWaypoints.Count > characterInfos.Count) { spawnWaypoints.RemoveAt(Rand.Int(spawnWaypoints.Count)); @@ -301,46 +314,8 @@ namespace Barotrauma var info = characterInfos[i]; info.TeamID = CharacterTeamType.Team1; Character character = Character.Create(info, spawnWaypoints[i].WorldPosition, info.Name); - if (character.Info != null) - { - if (!character.Info.StartItemsGiven && character.Info.InventoryData != null) - { - DebugConsole.AddWarning($"Error when initializing a round: character \"{character.Name}\" has not been given their initial items but has saved inventory data. Using the saved inventory data instead of giving the character new items."); - } - if (character.Info.InventoryData != null) - { - character.SpawnInventoryItems(character.Inventory, character.Info.InventoryData.FromPackage(null)); - } - else if (!character.Info.StartItemsGiven) - { - character.GiveJobItems(mainSubWaypoints[i]); - foreach (Item item in character.Inventory.AllItems) - { - //if the character is loaded from a human prefab with preconfigured items, its ID card gets assigned to the sub it spawns in - //we don't want that in this case, the crew's cards shouldn't be submarine-specific - var idCard = item.GetComponent(); - if (idCard != null) - { - idCard.SubmarineSpecificID = 0; - } - } - } - if (character.Info.HealthData != null) - { - CharacterInfo.ApplyHealthData(character, character.Info.HealthData); - } + InitializeCharacter(character, mainSubWaypoints[i], spawnWaypoints[i]); - character.LoadTalents(); - - character.GiveIdCardTags(mainSubWaypoints[i]); - character.GiveIdCardTags(spawnWaypoints[i]); - character.Info.StartItemsGiven = true; - if (character.Info.OrderData != null) - { - character.Info.ApplyOrderData(); - } - } - AddCharacter(character, sortCrewList: false); #if CLIENT if (IsSinglePlayer && (Character.Controlled == null || character.Info.LastControlled)) { Character.Controlled = character; } @@ -355,6 +330,61 @@ namespace Barotrauma conversationTimer = IsSinglePlayer ? Rand.Range(5.0f, 10.0f) : Rand.Range(45.0f, 60.0f); } + /// + /// Returns the potential crew spawnpositions for the crew in the loaded outpost + /// + public List GetOutpostSpawnpoints() + { + return WayPoint.WayPointList.FindAll(wp => + wp.SpawnType == SpawnType.Human && + wp.Submarine == Level.Loaded.StartOutpost && + wp.CurrentHull != null && + wp.CurrentHull.OutpostModuleTags.Contains("airlock".ToIdentifier())); + } + + public void InitializeCharacter(Character character, WayPoint mainSubWaypoint, WayPoint spawnWaypoint) + { + if (character.Info != null) + { + if (!character.Info.StartItemsGiven && character.Info.InventoryData != null) + { + DebugConsole.AddWarning($"Error when initializing a round: character \"{character.Name}\" has not been given their initial items but has saved inventory data. Using the saved inventory data instead of giving the character new items."); + } + if (character.Info.InventoryData != null) + { + character.SpawnInventoryItems(character.Inventory, character.Info.InventoryData.FromPackage(null)); + } + else if (!character.Info.StartItemsGiven) + { + character.GiveJobItems(mainSubWaypoint); + foreach (Item item in character.Inventory.AllItems) + { + //if the character is loaded from a human prefab with preconfigured items, its ID card gets assigned to the sub it spawns in + //we don't want that in this case, the crew's cards shouldn't be submarine-specific + var idCard = item.GetComponent(); + if (idCard != null) + { + idCard.SubmarineSpecificID = 0; + } + } + } + if (character.Info.HealthData != null) + { + CharacterInfo.ApplyHealthData(character, character.Info.HealthData); + } + + character.LoadTalents(); + + character.GiveIdCardTags(mainSubWaypoint); + character.GiveIdCardTags(spawnWaypoint); + character.Info.StartItemsGiven = true; + if (character.Info.OrderData != null) + { + character.Info.ApplyOrderData(); + } + } + } + public void RenameCharacter(CharacterInfo characterInfo, string newName) { int identifier = characterInfo.GetIdentifierUsingOriginalName(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs index 98a2ebc39..51d902c68 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs @@ -53,7 +53,7 @@ namespace Barotrauma internal struct NetWalletSetSalaryUpdate : INetSerializableStruct { [NetworkSerialize] - public ushort Target; + public Option Target; [NetworkSerialize(MinValueInt = 0, MaxValueInt = 100)] public int NewRewardDistribution; @@ -117,13 +117,13 @@ namespace Barotrauma public override int Balance { get => 0; - set => new InvalidOperationException("Tried to set the balance on an invalid wallet"); + set => throw new InvalidOperationException("Tried to set the balance on an invalid wallet"); } public override int RewardDistribution { get => 0; - set => new InvalidOperationException("Tried to set the reward distribution on an invalid wallet"); + set => throw new InvalidOperationException("Tried to set the reward distribution on an invalid wallet"); } } @@ -134,7 +134,7 @@ namespace Barotrauma public const string LowerCaseSaveElementName = "wallet"; private const string AttributeNameBalance = "balance", - AttrubuteNameRewardDistribution = "rewarddistribution", + AttributeNameRewardDistribution = "rewarddistribution", SaveElementName = "Wallet"; public readonly Option Owner; @@ -152,7 +152,15 @@ namespace Barotrauma public virtual int RewardDistribution { get => rewardDistribution; - set => rewardDistribution = ClampRewardDistribution(value); + set + { + rewardDistribution = ClampRewardDistribution(value); + + if (Owner.TryUnwrap(out var character) && character.Info is { } info) + { + info.LastRewardDistribution = Option.Some(rewardDistribution); + } + } } public Wallet(Option owner) @@ -163,12 +171,14 @@ namespace Barotrauma public Wallet(Option owner, XElement element): this(owner) { balance = ClampBalance(element.GetAttributeInt(AttributeNameBalance, 0)); - rewardDistribution = ClampBalance(element.GetAttributeInt(AttrubuteNameRewardDistribution, 0)); + rewardDistribution = ClampBalance(element.GetAttributeInt(AttributeNameRewardDistribution, 0)); } public XElement Save() { - XElement element = new XElement(SaveElementName, new XAttribute(AttributeNameBalance, Balance), new XAttribute(AttrubuteNameRewardDistribution, RewardDistribution)); + XElement element = new XElement(SaveElementName, + new XAttribute(AttributeNameBalance, Balance), + new XAttribute(AttributeNameRewardDistribution, RewardDistribution)); return element; } @@ -195,6 +205,11 @@ namespace Barotrauma SettingsChanged(balanceChanged: Option.Some(-price), rewardChanged: Option.None()); } + /// + /// Sets how much salary the wallet owner should receive from mission rewards. + /// Bank's salary determines the default salary for new characters. + /// + /// public void SetRewardDistribution(int value) { int oldValue = RewardDistribution; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index d66c8c079..eee7b7643 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -347,6 +347,10 @@ namespace Barotrauma /// public event Action BeforeLevelLoading; + /// + /// Triggers when saving and quitting mid-round (as in, not just transferring to a new level). Automatically cleared after triggering -> no need to unregister + /// + public event Action OnSaveAndQuit; public override void AddExtraMissions(LevelData levelData) { @@ -472,7 +476,7 @@ namespace Barotrauma var missionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Any(t => t == automaticMission.MissionTag)).OrderBy(m => m.UintIdentifier); if (missionPrefabs.Any()) { - var missionPrefab = ToolBox.SelectWeightedRandom(missionPrefabs, p => (float)p.Commonness, rand); + var missionPrefab = ToolBox.SelectWeightedRandom(missionPrefabs, p => p.Commonness, rand); if (missionPrefab.Type == MissionType.Pirate && Missions.Any(m => m.Prefab.Type == MissionType.Pirate)) { continue; @@ -515,8 +519,8 @@ namespace Barotrauma if (endLevelMissionPrefabs.Any()) { Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed)); - var endLevelMissionPrefab = ToolBox.SelectWeightedRandom(endLevelMissionPrefabs, p => (float)p.Commonness, rand); - if (!Missions.Any(m => m.Prefab.Type == endLevelMissionPrefab.Type)) + var endLevelMissionPrefab = ToolBox.SelectWeightedRandom(endLevelMissionPrefabs, p => p.Commonness, rand); + if (Missions.All(m => m.Prefab.Type != endLevelMissionPrefab.Type)) { if (levelData.Type == LevelData.LevelType.LocationConnection) { @@ -913,6 +917,20 @@ namespace Barotrauma } } + /// + /// Handles updating store stock, registering event history and relocating items (i.e. things that need to be done when saving and quitting mid-round) + /// + public void HandleSaveAndQuit() + { + OnSaveAndQuit?.Invoke(); + OnSaveAndQuit = null; + if (Level.IsLoadedFriendlyOutpost) + { + UpdateStoreStock(); + } + GameMain.GameSession.EventManager?.RegisterEventHistory(registerFinishedOnly: true); + } + /// /// Updates store stock before saving the game /// @@ -1368,13 +1386,13 @@ namespace Barotrauma { if (item.Removed) { continue; } if (item.NonInteractable || item.NonPlayerTeamInteractable) { continue; } - if (item.HiddenInGame) { continue; } + if (item.IsHidden) { continue; } if (!connectedSubs.Contains(item.Submarine)) { continue; } if (item.Prefab.DontTransferBetweenSubs) { continue; } if (AnyParentInventoryDisableTransfer(item)) { continue; } var rootOwner = item.GetRootInventoryOwner(); if (rootOwner is Character) { continue; } - if (rootOwner is Item ownerItem && (ownerItem.NonInteractable || item.NonPlayerTeamInteractable || ownerItem.HiddenInGame)) { continue; } + if (rootOwner is Item ownerItem && (ownerItem.NonInteractable || item.NonPlayerTeamInteractable || ownerItem.IsHidden)) { continue; } if (item.GetComponent() != null) { continue; } if (item.Components.None(c => c is Pickable)) { continue; } if (item.Components.Any(c => c is Pickable p && p.IsAttached)) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs index 789a31c8f..1d9760c2f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs @@ -29,7 +29,7 @@ namespace Barotrauma : base(preset) { Location[] locations = { GameMain.GameSession.StartLocation, GameMain.GameSession.EndLocation }; - var mission = Mission.LoadRandom(locations, seed, false, missionType); + var mission = Mission.LoadRandom(locations, seed, requireCorrectLocationType: false, missionType, difficultyLevel: GameMain.NetworkMember.ServerSettings.SelectedLevelDifficulty); if (mission != null) { missions.Add(mission); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 1037d9607..a79c24188 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -231,6 +231,12 @@ namespace Barotrauma { campaign.Bank.Deduct(selectedSub.Price); campaign.Bank.Balance = Math.Max(campaign.Bank.Balance, 0); +#if SERVER + if (GameMain.Server?.ServerSettings?.NewCampaignDefaultSalary is { } salary) + { + campaign.Bank.SetRewardDistribution((int)Math.Round(salary, digits: 0)); + } +#endif } return campaign; } @@ -960,6 +966,8 @@ namespace Barotrauma { ToggleTabMenu(); } + DeathPrompt?.Close(); + DeathPrompt.CloseBotPanel(); GUI.PreventPauseMenuToggle = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/HireManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/HireManager.cs index 5e2ecdd71..33ff5e43a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/HireManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/HireManager.cs @@ -42,16 +42,25 @@ namespace Barotrauma { AvailableCharacters.ForEach(c => c.Remove()); AvailableCharacters.Clear(); + + foreach (var missingJob in location.Type.GetHireablesMissingFromCrew()) + { + AddCharacter(missingJob); + amount--; + } for (int i = 0; i < amount; i++) { - JobPrefab job = location.Type.GetRandomHireable(); - if (job == null) { return; } - - var variant = Rand.Range(0, job.Variants, Rand.RandSync.ServerAndClient); - AvailableCharacters.Add(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, variant: variant)); + AddCharacter(location.Type.GetRandomHireable()); } if (location.Faction != null) { GenerateFactionCharacters(location.Faction.Prefab); } if (location.SecondaryFaction != null) { GenerateFactionCharacters(location.SecondaryFaction.Prefab); } + + void AddCharacter(JobPrefab job) + { + if (job == null) { return; } + int variant = Rand.Range(0, job.Variants, Rand.RandSync.ServerAndClient); + AvailableCharacters.Add(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, variant: variant)); + } } private void GenerateFactionCharacters(FactionPrefab faction) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs index a2a9ea63b..c8f511b79 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System; using System.Collections.Generic; @@ -125,7 +125,7 @@ namespace Barotrauma public NetCrewMember(CharacterInfo info) { - CharacterInfoID = info.GetIdentifierUsingOriginalName(); + CharacterInfoID = info.ID; Afflictions = ImmutableArray.Empty; } @@ -138,7 +138,7 @@ namespace Barotrauma { foreach (CharacterInfo info in crew) { - if (info.GetIdentifierUsingOriginalName() == CharacterInfoID) + if (info.ID == CharacterInfoID) { return info; } @@ -159,6 +159,14 @@ namespace Barotrauma private readonly CampaignMode? campaign; + /// + /// Characters whose afflictions have changed (processed periodically, refreshing the UI client-side, sending updates to clients server-side + /// + private readonly HashSet charactersWithAfflictionChanges = new HashSet(); + + private float processAfflictionChangesTimer; + private const float ProcessAfflictionChangesInterval = 1.0f; + public MedicalClinic(CampaignMode campaign) { this.campaign = campaign; @@ -305,35 +313,8 @@ namespace Barotrauma private void OnAfflictionCountChangedPrivate(Character character) { - if (character is not { CharacterHealth: { } health, Info: { } info }) { return; } - - ImmutableArray afflictions = GetAllAfflictions(health); - -#if CLIENT - if (GameMain.NetworkMember is null) - { - ui?.UpdateAfflictions(new NetCrewMember(info, afflictions)); - } - - ui?.UpdateCrewPanel(); -#elif SERVER - foreach (AfflictionSubscriber sub in afflictionSubscribers.ToList()) - { - if (sub.Expiry < DateTimeOffset.Now) - { - afflictionSubscribers.Remove(sub); - continue; - } - - if (sub.Target == info) - { - ServerSend(new NetCrewMember(info, afflictions), - header: NetworkHeader.AFFLICTION_UPDATE, - deliveryMethod: DeliveryMethod.Unreliable, - targetClient: sub.Subscriber); - } - } -#endif + if (character?.Info == null) { return; } + charactersWithAfflictionChanges.Add(character); } public int GetTotalCost() => PendingHeals.SelectMany(static h => h.Afflictions).Aggregate(0, static (current, affliction) => current + affliction.Price); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs index 887028517..223ff5c0e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs @@ -295,7 +295,7 @@ namespace Barotrauma /// /// Purchases an item swap and handles logic for deducting the credit. /// - public void PurchaseItemSwap(Item itemToRemove, ItemPrefab itemToInstall, bool force = false, Client? client = null) + public void PurchaseItemSwap(Item itemToRemove, ItemPrefab itemToInstall, bool isNetworkMessage = false, Client? client = null) { if (!CanUpgradeSub()) { @@ -343,12 +343,14 @@ namespace Barotrauma price = itemToInstall.SwappableItem.GetPrice(Campaign.Map?.CurrentLocation) * linkedItems.Count; } - if (force) + if (isNetworkMessage) { price = 0; } - if (Campaign.TryPurchase(client, price)) + //do not try to purchase if this is a network message (if the server is telling us that an item swap was purchased) + //we want to do the purchase no matter what, and the server handles deducting the money + if (isNetworkMessage || Campaign.TryPurchase(client, price)) { PurchasedItemSwaps.RemoveAll(p => linkedItems.Contains(p.ItemToRemove)); if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) @@ -433,8 +435,7 @@ namespace Barotrauma { if (itemToCancel.PendingItemSwap == null) { - var replacement = MapEntityPrefab.Find("", swappableItem.ReplacementOnUninstall) as ItemPrefab; - if (replacement == null) + if (MapEntityPrefab.FindByIdentifier(swappableItem.ReplacementOnUninstall) is not ItemPrefab replacement) { DebugConsole.ThrowError($"Failed to uninstall item \"{itemToCancel.Name}\". Could not find the replacement item \"{swappableItem.ReplacementOnUninstall}\"."); return; @@ -786,11 +787,10 @@ namespace Barotrauma private void LoadPendingUpgrades(XElement? element, bool isSingleplayer = true) { - if (!(element is { HasElements: true })) { return; } + if (element is not { HasElements: true }) { return; } List pendingUpgrades = new List(); - - // ReSharper disable once LoopCanBeConvertedToQuery + foreach (XElement upgrade in element.Elements()) { Identifier categoryIdentifier = upgrade.GetAttributeIdentifier("category", Identifier.Empty); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index a8d47043e..52cb5d15f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -15,6 +15,19 @@ namespace Barotrauma partial class CharacterInventory : Inventory { + /// + /// How much access other characters have to the inventory? + /// = Only accessible when character is knocked down or handcuffed. + /// = Can also access inventories of bots on the same team and friendly pets. + /// = Can also access other players in the same team (used for drag and drop give). + /// + public enum AccessLevel + { + Restricted, + Limited, + Allowed + } + private readonly Character character; public InvSlotType[] SlotTypes @@ -22,8 +35,7 @@ namespace Barotrauma get; private set; } - - + public static readonly List AnySlot = new List() { InvSlotType.Any }; public static bool IsHandSlotType(InvSlotType s) => s.HasFlag(InvSlotType.LeftHand) || s.HasFlag(InvSlotType.RightHand); @@ -546,6 +558,7 @@ namespace Barotrauma { item.AssignCampaignInteractionType(CampaignMode.InteractionType.None); } + item.Equipper = user; } protected override void CreateNetworkEvent(Range slotRange) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index bfef76f61..620d6772b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -658,7 +658,8 @@ namespace Barotrauma.Items.Components hulls[i] = new Hull(hullRects[i], subs[i]) { RoomName = IsHorizontal ? "entityname.dockingport" : "entityname.dockinghatch", - AvoidStaying = true + AvoidStaying = true, + IsWetRoom = true }; hulls[i].AddToGrid(subs[i]); hulls[i].FreeID(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index 08e2ba6b4..c396ebcd8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -180,6 +180,22 @@ namespace Barotrauma.Items.Components OpenState = isOpen ? 1.0f : 0.0f; } } + + /// + /// Can be used by status effects to tell the door to open (setting IsOpen directly would make it immediately fully open) + /// + public bool ShouldBeOpen + { + get { return isOpen; } + set + { + if (isOpen != value) + { + ToggleState(ActionType.OnUse, user: null); + } + } + } + public bool IsClosed => !IsOpen; public bool IsFullyOpen => IsOpen && OpenState >= 1.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs index 8395c7c58..a163881d7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs @@ -896,9 +896,9 @@ namespace Barotrauma.Items.Components return element; } - public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap) { - base.Load(componentElement, usePrefabValues, idRemap); + base.Load(componentElement, usePrefabValues, idRemap, isItemSwap); flowerTiles = componentElement.GetAttributeIntArray("flowertiles", Array.Empty())!; Decayed = componentElement.GetAttributeBool("decayed", false); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index a7671a642..2d1d5228d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -1,10 +1,12 @@ -using Barotrauma.Networking; +using Barotrauma.Abilities; +using Barotrauma.Networking; using FarseerPhysics; using FarseerPhysics.Dynamics; using FarseerPhysics.Dynamics.Contacts; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; @@ -41,6 +43,9 @@ namespace Barotrauma.Items.Components private bool attachable, attached, attachedByDefault; private Voronoi2.VoronoiCell attachTargetCell; private PhysicsBody body; + + public readonly ImmutableDictionary HoldableStatValues; + public PhysicsBody Pusher { get; @@ -287,6 +292,22 @@ namespace Barotrauma.Items.Components } } characterUsable = element.GetAttributeBool("characterusable", true); + + Dictionary statValues = new Dictionary(); + foreach (var subElement in element.GetChildElements("statvalue")) + { + StatTypes statType = CharacterAbilityGroup.ParseStatType(subElement.GetAttributeString("stattype", ""), Name); + float statValue = subElement.GetAttributeFloat("value", 0f); + if (statValues.ContainsKey(statType)) + { + statValues[statType] += statValue; + } + else + { + statValues.TryAdd(statType, statValue); + } + } + HoldableStatValues = statValues.ToImmutableDictionary(); } private bool OnPusherCollision(Fixture sender, Fixture other, Contact contact) @@ -304,9 +325,9 @@ namespace Barotrauma.Items.Components } private bool loadedFromInstance; - public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap) { - base.Load(componentElement, usePrefabValues, idRemap); + base.Load(componentElement, usePrefabValues, idRemap, isItemSwap); loadedFromInstance = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index 12c906e15..dd6002926 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -296,7 +296,7 @@ namespace Barotrauma.Items.Components impactQueue.Clear(); item.body.FarseerBody.OnCollision -= OnCollision; item.body.CollisionCategories = Physics.CollisionItem; - item.body.CollidesWith = Physics.CollisionWall; + item.body.CollidesWith = Physics.DefaultItemCollidesWith; item.body.FarseerBody.IsBullet = false; item.body.PhysEnabled = false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs index 5db4a58ff..07299eb0e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs @@ -251,6 +251,7 @@ namespace Barotrauma.Items.Components foreach (ConnectionPanel connectionPanel in item.GetComponents()) { + connectionPanel.DisconnectedWires.Clear(); foreach (Connection c in connectionPanel.Connections) { foreach (Wire w in c.Wires.ToArray()) @@ -260,7 +261,7 @@ namespace Barotrauma.Items.Components w.Item.SetTransform(pos, 0.0f); } } - } + } } public override void Drop(Character dropper, bool setTransform = true) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs index 0c45b1548..03184cb48 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs @@ -62,7 +62,7 @@ namespace Barotrauma.Items.Components if (!MathUtils.IsValid(dir)) { return true; } float length = 200; dir = dir.ClampLength(length) / length; - Vector2 propulsion = dir * Force * character.PropulsionSpeedMultiplier; + Vector2 propulsion = dir * Force * character.PropulsionSpeedMultiplier * (1.0f + character.GetStatValue(StatTypes.PropulsionSpeed)); if (character.AnimController.InWater && Force > 0.0f) { character.AnimController.TargetMovement = dir; } foreach (Limb limb in character.AnimController.Limbs) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index 52a49838e..75065b031 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -272,22 +272,6 @@ namespace Barotrauma.Items.Components item.AiTarget.SightRange = item.AiTarget.MaxSightRange; } - ignoredBodies.Clear(); - foreach (Limb l in character.AnimController.Limbs) - { - if (l.IsSevered) { continue; } - ignoredBodies.Add(l.body.FarseerBody); - } - - foreach (Item heldItem in character.HeldItems) - { - var holdable = heldItem.GetComponent(); - if (holdable?.Pusher != null) - { - ignoredBodies.Add(holdable.Pusher.FarseerBody); - } - } - float degreeOfFailure = 1.0f - DegreeOfSuccess(character); degreeOfFailure *= degreeOfFailure; if (degreeOfFailure > Rand.Range(0.0f, 1.0f)) @@ -311,6 +295,25 @@ namespace Barotrauma.Items.Components } float damageMultiplier = (1f + item.GetQualityModifier(Quality.StatType.FirepowerMultiplier)) * WeaponDamageModifier; projectile.Launcher = item; + + ignoredBodies.Clear(); + if (!projectile.DamageUser) + { + foreach (Limb l in character.AnimController.Limbs) + { + if (l.IsSevered) { continue; } + ignoredBodies.Add(l.body.FarseerBody); + } + + foreach (Item heldItem in character.HeldItems) + { + var holdable = heldItem.GetComponent(); + if (holdable?.Pusher != null) + { + ignoredBodies.Add(holdable.Pusher.FarseerBody); + } + } + } projectile.Shoot(character, character.AnimController.AimSourceSimPos, barrelPos, rotation + spread, ignoredBodies: ignoredBodies.ToList(), createNetworkEvent: false, damageMultiplier, LaunchImpulse); projectile.Item.GetComponent()?.Attach(Item, projectile.Item); if (projectile.Item.body != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs index 33c3f6f37..3c22efde5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs @@ -158,7 +158,7 @@ namespace Barotrauma.Items.Components else { throwAngle = ThrowAngleStart; - ac.HoldItem(deltaTime, item, handlePos, itemPos: aimPos, aim: false, holdAngle); + ac.HoldItem(deltaTime, item, handlePos, itemPos: holdPos, aim: false, holdAngle); } } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index b24a94f6c..d74db768a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -939,14 +939,16 @@ namespace Barotrauma.Items.Components #endif } - public virtual void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) + public virtual void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap) { if (componentElement != null) { foreach (XAttribute attribute in componentElement.Attributes()) { if (!SerializableProperties.TryGetValue(attribute.NameAsIdentifier(), out SerializableProperty property)) { continue; } - if (property.OverridePrefabValues || !usePrefabValues) + if (property.OverridePrefabValues || + !usePrefabValues || + (isItemSwap && property.GetAttribute() is { TransferToSwappedItem: true })) { property.TrySetValue(this, attribute.Value); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 66fef2fbf..a8a73f35a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -5,6 +5,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Globalization; using System.Linq; using System.Xml.Linq; @@ -12,7 +13,7 @@ namespace Barotrauma.Items.Components { partial class ItemContainer : ItemComponent, IDrawableComponent { - readonly record struct ActiveContainedItem(Item Item, StatusEffect StatusEffect, bool ExcludeBroken, bool ExcludeFullCondition); + readonly record struct ActiveContainedItem(Item Item, StatusEffect StatusEffect, bool ExcludeBroken, bool ExcludeFullCondition, bool BlameEquipperForDeath); readonly record struct ContainedItem(Item Item, bool Hide, Vector2? ItemPos, float Rotation); @@ -252,6 +253,8 @@ namespace Barotrauma.Items.Components private float autoInjectCooldown = 1.0f; const float AutoInjectInterval = 1.0f; + private bool subContainersCanAutoInject; + public bool ShouldBeContained(string[] identifiersOrTags, out bool isRestrictionsDefined) { @@ -277,6 +280,11 @@ namespace Barotrauma.Items.Components public readonly bool HasSubContainers; + public bool hasSignalConnections; + + private string totalConditionValueString = "", totalConditionPercentageString = "", totalItemsString = ""; + private float prevTotalConditionValue = 0, prevTotalConditionPercentage = 0; int prevTotalItems = 0; + public ItemContainer(Item item, ContentXElement element) : base(item, element) { @@ -323,6 +331,8 @@ namespace Barotrauma.Items.Components int subMaxStackSize = subElement.GetAttributeInt("maxstacksize", maxStackSize); bool autoInject = subElement.GetAttributeBool("autoinject", false); + subContainersCanAutoInject |= autoInject; + var subContainableItems = new List(); foreach (var subSubElement in subElement.Elements()) { @@ -411,7 +421,12 @@ namespace Barotrauma.Items.Components relatedItem ??= containableItem; foreach (StatusEffect effect in containableItem.StatusEffects) { - activeContainedItems.Add(new ActiveContainedItem(containedItem, effect, containableItem.ExcludeBroken, containableItem.ExcludeFullCondition)); + activeContainedItems.Add(new ActiveContainedItem( + containedItem, + effect, + containableItem.ExcludeBroken, + containableItem.ExcludeFullCondition, + containableItem.BlameEquipperForDeath)); } } } @@ -448,8 +463,8 @@ namespace Barotrauma.Items.Components GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":GardeningPlanted:" + containedItem.Prefab.Identifier); } - //no need to Update() if this item has no statuseffects and no physics body - IsActive = activeContainedItems.Count > 0 || Inventory.AllItems.Any(it => it.body != null); + //no need to Update() if this item has no statuseffects and no physics body, and if there are no signal connections. + IsActive = hasSignalConnections || activeContainedItems.Count > 0 || Inventory.AllItems.Any(static it => it.body != null); if (IsActive && item.GetRootInventoryOwner() is Character owner && owner.HasEquippedItem(item, predicate: slot => slot.HasFlag(InvSlotType.LeftHand) || slot.HasFlag(InvSlotType.RightHand))) @@ -481,11 +496,16 @@ namespace Barotrauma.Items.Components containedItems.RemoveAll(i => i.Item == containedItem); item.SetContainedItemPositions(); //deactivate if the inventory is empty - IsActive = activeContainedItems.Count > 0 || Inventory.AllItems.Any(it => it.body != null); + IsActive = hasSignalConnections || activeContainedItems.Count > 0 || Inventory.AllItems.Any(static it => it.body != null); CharacterHUD.RecreateHudTextsIfFocused(item, containedItem); OnContainedItemsChanged.Invoke(this); } + public bool BlameEquipperForDeath() + { + return activeContainedItems.Any(c => c.BlameEquipperForDeath); + } + public bool CanBeContained(Item item) { if (!AllowAccessWhenDropped && this.item.body is { Enabled: true }) { return false; } @@ -545,11 +565,47 @@ namespace Barotrauma.Items.Components alwaysContainedItemsSpawned = true; } + if (hasSignalConnections) + { + float totalConditionValue = 0, totalConditionPercentage = 0; int totalItems = 0; + foreach (var item in Inventory.AllItems) + { + if (!MathUtils.NearlyEqual(item.Condition, 0)) + { + totalConditionValue += item.Condition; + totalConditionPercentage += item.ConditionPercentage; + totalItems++; + } + } + + if (!MathUtils.NearlyEqual(totalConditionValue, prevTotalConditionValue)) + { + totalConditionValueString = ((int)totalConditionValue).ToString(CultureInfo.InvariantCulture); + prevTotalConditionValue = totalConditionValue; + } + + if (!MathUtils.NearlyEqual(totalConditionPercentage, prevTotalConditionPercentage)) + { + totalConditionPercentageString = ((int)totalConditionPercentage).ToString(CultureInfo.InvariantCulture); + prevTotalConditionPercentage = totalConditionPercentage; + } + + if (totalItems != prevTotalItems) + { + totalItemsString = totalItems.ToString(CultureInfo.InvariantCulture); + prevTotalItems = totalItems; + } + + item.SendSignal(totalConditionValueString, "contained_conditions"); + item.SendSignal(totalConditionPercentageString, "contained_conditions_percentage"); + item.SendSignal(totalItemsString, "contained_items"); + } + if (item.ParentInventory is CharacterInventory ownerInventory) { SetContainedItemPositionsIfNeeded(); - if (AutoInject || slotRestrictions.Any(s => s.AutoInject)) + if (AutoInject || subContainersCanAutoInject) { //normally autoinjection should delete the (medical) item, so it only gets applied once //but in multiplayer clients aren't allowed to remove items themselves, so they may be able to trigger this dozens of times @@ -595,7 +651,7 @@ namespace Barotrauma.Items.Components SetContainedItemPositionsIfNeeded(); } } - else if (activeContainedItems.Count == 0) + else if (!hasSignalConnections && activeContainedItems.Count == 0) { IsActive = false; return; @@ -987,6 +1043,7 @@ namespace Barotrauma.Items.Components { Inventory.AllowSwappingContainedItems = AllowSwappingContainedItems; containableItemIdentifiers = slotRestrictions.SelectMany(s => s.ContainableItems?.SelectMany(ri => ri.Identifiers) ?? Enumerable.Empty()).ToImmutableHashSet(); + hasSignalConnections = item.Connections?.Any(c => c.Name is "contained_conditions" or "contained_conditions_percentage" or "contained_items") ?? false; if (item.Submarine == null || !item.Submarine.Loading) { SpawnAlwaysContainedItems(); @@ -1087,9 +1144,9 @@ namespace Barotrauma.Items.Components } } - public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap) { - base.Load(componentElement, usePrefabValues, idRemap); + base.Load(componentElement, usePrefabValues, idRemap, isItemSwap); string containedString = componentElement.GetAttributeString("contained", ""); string[] itemIdStrings = containedString.Split(','); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index 7b04931f6..22af56cf6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -589,9 +589,9 @@ namespace Barotrauma.Items.Components return SaveLimbPositions(base.Save(parentElement)); } - public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap) { - base.Load(componentElement, usePrefabValues, idRemap); + base.Load(componentElement, usePrefabValues, idRemap, isItemSwap); if (GameMain.GameSession?.GameMode?.Preset == GameModePreset.TestMode) { LoadLimbPositions(componentElement); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs index 311da9b7b..7083a1fd4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs @@ -6,7 +6,7 @@ using Barotrauma.Networking; namespace Barotrauma.Items.Components { - partial class Engine : Powered, IServerSerializable, IClientSerializable + partial class Engine : Powered, IServerSerializable, IClientSerializable, IDeteriorateUnderStress { private float force; @@ -76,10 +76,7 @@ namespace Barotrauma.Items.Components set { force = MathHelper.Clamp(value, -100.0f, 100.0f); } } - public float CurrentVolume - { - get { return Math.Abs((force / 100.0f) * (MinVoltage <= 0.0f ? 1.0f : Math.Min(prevVoltage, 1.0f))); } - } + public float CurrentVolume => CurrentStress; public float CurrentBrokenVolume { @@ -90,6 +87,8 @@ namespace Barotrauma.Items.Components } } + public float CurrentStress => Math.Abs((force / 100.0f) * (MinVoltage <= 0.0f ? 1.0f : Math.Min(prevVoltage, 1.0f))); + private const float TinkeringForceIncrease = 1.5f; public Engine(Item item, ContentXElement element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index 790838fe0..b6e76e442 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -707,9 +707,15 @@ namespace Barotrauma.Items.Components private static bool AnyOneHasRecipeForItem(Character user, ItemPrefab item) { + CharacterType mustHaveRecipe = GameMain.GameSession?.GameMode is { IsSinglePlayer: true } ? + //in single player it doesn't matter if it's a bot or a player who has the recipe + //(the bots can turn into a "player" when switching characters, and that could interrupt the fabrication) + CharacterType.Both : + //in MP the recipes other players have don't cound + CharacterType.Bot; return (user != null && user.HasRecipeForItem(item.Identifier)) || - GameSession.GetSessionCrewCharacters(CharacterType.Bot).Any(c => c.HasRecipeForItem(item.Identifier)); + GameSession.GetSessionCrewCharacters(mustHaveRecipe).Any(c => c.HasRecipeForItem(item.Identifier)); } private readonly HashSet usedIngredients = new HashSet(); @@ -986,9 +992,9 @@ namespace Barotrauma.Items.Components return componentElement; } - public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap) { - base.Load(componentElement, usePrefabValues, idRemap); + base.Load(componentElement, usePrefabValues, idRemap, isItemSwap); savedFabricatedItem = componentElement.GetAttributeString("fabricateditemidentifier", ""); savedTimeUntilReady = componentElement.GetAttributeFloat("savedtimeuntilready", 0.0f); savedRequiredTime = componentElement.GetAttributeFloat("savedrequiredtime", 0.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index 6d51be758..d64e96d43 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -7,7 +7,7 @@ using System.Linq; namespace Barotrauma.Items.Components { - partial class Pump : Powered, IServerSerializable, IClientSerializable + partial class Pump : Powered, IServerSerializable, IClientSerializable, IDeteriorateUnderStress { private float flowPercentage; private float maxFlow; @@ -85,6 +85,8 @@ namespace Barotrauma.Items.Components public override bool UpdateWhenInactive => true; + public float CurrentStress => Math.Abs(flowPercentage / 100.0f); + public Pump(Item item, ContentXElement element) : base(item, element) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs index 8eda11f8e..f12df7acb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs @@ -102,6 +102,7 @@ namespace Barotrauma.Items.Components protected virtual PowerPriority Priority { get { return PowerPriority.Default; } } + [Header(localizedTextTag: "sp.powered.propertyheader")] [Editable, Serialize(0.5f, IsPropertySaveable.Yes, description: "The minimum voltage required for the device to function. " + "The voltage is calculated as power / powerconsumption, meaning that a device " + "with a power consumption of 1000 kW would need at least 500 kW of power to work if the minimum voltage is set to 0.5.")] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index dde0538e6..a2a0a573f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -288,6 +288,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, IsPropertySaveable.No, description: "Can the projectile hit the user? Should generally be disabled, unless the projectile is for example something like shrapnel launched by a projectile impact.")] + public bool DamageUser + { + get; + set; + } + public bool IsStuckToTarget => StickTarget != null; private Category originalCollisionCategories; @@ -413,6 +420,12 @@ namespace Barotrauma.Items.Components if (StickTarget != null || IsActive) { return false; } float initialRotation = item.body.Rotation; + //if the item is being launched from an inventory, assume it's being fired by a gun that handles setting the rotation correctly + //but if the item is e.g. being thrown by a character, we need to take the direction into account + if (item.body.Dir < 0 && item.ParentInventory is not ItemInventory) + { + initialRotation -= MathHelper.Pi; + } for (int i = 0; i < HitScanCount; i++) { float launchAngle; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/RemoteController.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/RemoteController.cs index 5e330bd94..b4268bf95 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/RemoteController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/RemoteController.cs @@ -74,7 +74,7 @@ namespace Barotrauma.Items.Components float closestDist = float.PositiveInfinity; foreach (Item targetItem in Item.ItemList) { - if (targetItem.NonInteractable || targetItem.NonPlayerTeamInteractable || targetItem.HiddenInGame) { continue; } + if (targetItem.NonInteractable || targetItem.NonPlayerTeamInteractable || targetItem.IsHidden) { continue; } if (OnlyInOwnSub) { if (targetItem.Submarine != item.Submarine) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index 65bc2c00c..35ec9cd11 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -1,5 +1,4 @@ using Barotrauma.Abilities; -using Barotrauma.Extensions; using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; @@ -9,6 +8,14 @@ using System.Linq; namespace Barotrauma.Items.Components { + /// + /// + /// + interface IDeteriorateUnderStress + { + public float CurrentStress { get; } + } + partial class Repairable : ItemComponent, IServerSerializable, IClientSerializable { private readonly LocalizedString header; @@ -80,6 +87,34 @@ namespace Barotrauma.Items.Components set; } + [Serialize(1.0f, IsPropertySaveable.Yes, description: "How much faster the device can deteriorate when under stress (e.g. when operating at full speed/power)."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f, DecimalCount = 2)] + public float MaxStressDeteriorationMultiplier + { + get; + set; + } + + [Serialize(0.5f, IsPropertySaveable.Yes, description: "At what speed/power must the device be operating at to be considered \"under stress\"."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f, DecimalCount = 2)] + public float StressDeteriorationThreshold + { + get; + set; + } + + [Serialize(0.1f, IsPropertySaveable.Yes, description: "How fast the deterioration speed increases when under stress."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f, DecimalCount = 2)] + public float StressDeteriorationIncreaseSpeed + { + get; + set; + } + + [Serialize(0.1f, IsPropertySaveable.Yes, description: "How fast the deterioration speed decreases when not under stress."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f, DecimalCount = 2)] + public float StressDeteriorationDecreaseSpeed + { + get; + set; + } + [Serialize(100.0f, IsPropertySaveable.Yes, description: "The amount of time it takes to fix the item with insufficient skill levels."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] public float FixDurationLowSkill { @@ -139,6 +174,8 @@ namespace Barotrauma.Items.Components private float tinkeringDuration; private float tinkeringStrength; + public float StressDeteriorationMultiplier { get; private set; } = 1.0f; + public float TinkeringStrength => tinkeringStrength; private bool tinkeringPowersDevices; @@ -147,7 +184,6 @@ namespace Barotrauma.Items.Components public bool IsBelowRepairThreshold => item.ConditionPercentageRelativeToDefaultMaxCondition < RepairThreshold; public bool IsBelowRepairIconThreshold => item.ConditionPercentageRelativeToDefaultMaxCondition < RepairThreshold / 2; - public enum FixActions : int { @@ -420,6 +456,21 @@ namespace Barotrauma.Items.Components item.SendSignal(conditionSignal, "condition_out"); + foreach (var component in item.Components) + { + if (component is IDeteriorateUnderStress deteriorateUnderStress) + { + if (deteriorateUnderStress.CurrentStress >= StressDeteriorationThreshold) + { + StressDeteriorationMultiplier = Math.Min(StressDeteriorationMultiplier + deltaTime * StressDeteriorationIncreaseSpeed, MaxStressDeteriorationMultiplier); + } + else + { + StressDeteriorationMultiplier = Math.Max(StressDeteriorationMultiplier - deltaTime * StressDeteriorationDecreaseSpeed, 1.0f); + } + } + } + if (ForceDeteriorationTimer > 0.0f) { ForceDeteriorationTimer -= deltaTime; @@ -599,7 +650,7 @@ namespace Barotrauma.Items.Components { float deteriorationSpeed = item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.DetoriationSpeed, DeteriorationSpeed); if (ForceDeteriorationTimer > 0.0f) { deteriorationSpeed = Math.Max(deteriorationSpeed, 1.0f); } - item.Condition -= deteriorationSpeed * deltaTime; + item.Condition -= deteriorationSpeed * StressDeteriorationMultiplier * deltaTime; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs index 64fa0d3bc..f96cf2d5c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs @@ -125,9 +125,9 @@ namespace Barotrauma.Items.Components /// private Option delayedElementToLoad; - public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap) { - base.Load(componentElement, usePrefabValues, idRemap); + base.Load(componentElement, usePrefabValues, idRemap, isItemSwap); if (delayedElementToLoad.IsSome()) { return; } delayedElementToLoad = Option.Some(componentElement); } @@ -380,6 +380,18 @@ namespace Barotrauma.Items.Components OnViewUpdateProjSpecific(); } + private void RenameConnectionLabelsInternal(CircuitBoxInputOutputNode.Type type, Dictionary overrides) + { + foreach (var node in InputOutputNodes) + { + if (node.NodeType != type) { continue; } + + node.ReplaceAllConnectionLabelOverrides(overrides); + break; + } + OnViewUpdateProjSpecific(); + } + private static bool IsExternalConnection(CircuitBoxConnection conn) => conn is (CircuitBoxInputConnection or CircuitBoxOutputConnection); private void CreateWireWithoutItem(CircuitBoxConnection one, CircuitBoxConnection two, ushort id, ItemPrefab prefab) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs index b1424074a..238f9f47c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs @@ -282,9 +282,9 @@ namespace Barotrauma.Items.Components return false; } - public override void Load(ContentXElement element, bool usePrefabValues, IdRemap idRemap) + public override void Load(ContentXElement element, bool usePrefabValues, IdRemap idRemap, bool isItemSwap) { - base.Load(element, usePrefabValues, idRemap); + base.Load(element, usePrefabValues, idRemap, isItemSwap); List loadedConnections = new List(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index ec26fafb9..e32e3f471 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -284,7 +284,7 @@ namespace Barotrauma.Items.Components public override void OnMapLoaded() { #if CLIENT - if (item.HiddenInGame) + if (item.IsHidden) { Light.Enabled = false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs index b1d16e4d6..be6176fc1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs @@ -165,9 +165,9 @@ namespace Barotrauma.Items.Components updateTimer = Rand.Range(0.0f, UpdateInterval); } - public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap) { - base.Load(componentElement, usePrefabValues, idRemap); + base.Load(componentElement, usePrefabValues, idRemap, isItemSwap); //backwards compatibility if (componentElement.GetAttributeBool("onlyhumans", false)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs index 6b73ebd93..4c9d0347d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs @@ -188,9 +188,9 @@ namespace Barotrauma.Items.Components return componentElement; } - public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap) { - base.Load(componentElement, usePrefabValues, idRemap); + base.Load(componentElement, usePrefabValues, idRemap, isItemSwap); for (int i = 0; i < MaxMessages; i++) { string msg = componentElement.GetAttributeString("msg" + i, null); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs index a7117b73b..bd88cc3bd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs @@ -116,9 +116,9 @@ namespace Barotrauma.Items.Components IsActive = true; } - public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap) { - base.Load(componentElement, usePrefabValues, idRemap); + base.Load(componentElement, usePrefabValues, idRemap, isItemSwap); channelMemory = componentElement.GetAttributeIntArray("channelmemory", new int[ChannelMemorySize]); if (channelMemory.Length != ChannelMemorySize) { @@ -296,7 +296,7 @@ namespace Barotrauma.Items.Components { if (GameMain.Client == null) { - GameMain.GameSession?.CrewManager?.AddSinglePlayerChatMessage(signal.source?.Name ?? "", signal.value, ChatMessageType.Radio, sender: null); + GameMain.GameSession?.CrewManager?.AddSinglePlayerChatMessage(signal.source?.Name ?? "", signal.value, ChatMessageType.Radio, sender: item); } } #elif SERVER @@ -306,7 +306,7 @@ namespace Barotrauma.Items.Components if (recipientClient != null) { GameMain.Server.SendDirectChatMessage( - ChatMessage.Create(signal.source?.Name ?? "", chatMsg, ChatMessageType.Radio, null), recipientClient); + ChatMessage.Create(signal.source?.Name ?? "", chatMsg, ChatMessageType.Radio, item), recipientClient); } } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs index 3923350ca..8e0cd1ada 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs @@ -268,8 +268,10 @@ namespace Barotrauma.Items.Components CreateNetworkEvent(); } #endif - //the wire is active if only one end has been connected - IsActive = connections[0] == null ^ connections[1] == null; + //the wire is active if it's currently being wired to something (in character inventory and connected from one end) + IsActive = + item.ParentInventory is CharacterInventory && + connections[0] == null ^ connections[1] == null; } Drawable = IsActive || nodes.Any(); @@ -839,9 +841,9 @@ namespace Barotrauma.Items.Components } } - public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap) { - base.Load(componentElement, usePrefabValues, idRemap); + base.Load(componentElement, usePrefabValues, idRemap, isItemSwap); nodes.AddRange(ExtractNodes(componentElement)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index c89a073aa..04548a5d6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -17,20 +17,21 @@ namespace Barotrauma.Items.Components private readonly List<(Sprite sprite, Vector2 position)> chargeSprites = new List<(Sprite sprite, Vector2 position)>(); private readonly List spinningBarrelSprites = new List(); + /// + /// Sentinel value that represents the turret being launched without a projectile in network events + /// + const ushort LaunchWithoutProjectileId = ushort.MaxValue; + private Vector2 barrelPos; private Vector2 transformedBarrelPos; - private float rotation, targetRotation; + private float targetRotation; - private float reload, reloadTime, delayBetweenBurst; - private int shotsPerBurst, shotCounter; + private float reload; + private int shotCounter; private float minRotation, maxRotation; - private float launchImpulse; - - private float damageMultiplier; - private Camera cam; private float angularVelocity; @@ -90,11 +91,8 @@ namespace Barotrauma.Items.Components private readonly bool isSlowTurret; - public float Rotation - { - get { return rotation; } - } - + public float Rotation { get; private set; } + [Serialize("0,0", IsPropertySaveable.No, description: "The position of the barrel relative to the upper left corner of the base sprite (in pixels).")] public Vector2 BarrelPos { @@ -110,195 +108,35 @@ namespace Barotrauma.Items.Components } [Serialize("0,0", IsPropertySaveable.No, description: "The projectile launching location relative to transformed barrel position (in pixels).")] - public Vector2 FiringOffset - { - get; - set; - } + public Vector2 FiringOffset { get; set; } - public bool flipFiringOffset; + private bool flipFiringOffset; [Serialize(false, IsPropertySaveable.No, description: "If enabled, the firing offset will alternate from left to right (i.e. flipping the x-component of the offset each shot.)")] - public bool AlternatingFiringOffset - { - get; - set; - } + public bool AlternatingFiringOffset { get; set; } - public Vector2 TransformedBarrelPos - { - get - { - return transformedBarrelPos; - } - } + public Vector2 TransformedBarrelPos => transformedBarrelPos; [Serialize(0.0f, IsPropertySaveable.No, description: "The impulse applied to the physics body of the projectile (the higher the impulse, the faster the projectiles are launched).")] - public float LaunchImpulse - { - get { return launchImpulse; } - set { launchImpulse = value; } - } - - [Editable(0.0f, 1000.0f, decimals: 3), Serialize(5.0f, IsPropertySaveable.No, description: "The period of time the user has to wait between shots.")] - public float Reload - { - get { return reloadTime; } - set { reloadTime = value; } - } - - [Editable(1, 100), Serialize(1, IsPropertySaveable.No, description: "How many projectiles needs to be shot before we add an extra break? Think of the double coilgun.")] - public int ShotsPerBurst - { - get { return shotsPerBurst; } - set { shotsPerBurst = value; } - } - - [Editable(0.0f, 1000.0f, decimals: 3), Serialize(0.0f, IsPropertySaveable.No, description: "An extra delay between the bursts. Added to the reload.")] - public float DelayBetweenBursts - { - get { return delayBetweenBurst; } - set { delayBetweenBurst = value; } - } - - [Editable(0.1f, 10f), Serialize(1.0f, IsPropertySaveable.No, description: "Modifies the duration of retraction of the barrell after recoil to get back to the original position after shooting. Reload time affects this too.")] - public float RetractionDurationMultiplier - { - get; - set; - } - - [Editable(0.1f, 10f), Serialize(0.1f, IsPropertySaveable.No, description: "How quickly the recoil moves the barrel after launching.")] - public float RecoilTime - { - get; - set; - } - - [Editable(0f, 1000f), Serialize(0f, IsPropertySaveable.No, description: "How long the barrell stays in place after the recoil and before retracting back to the original position.")] - public float RetractionDelay - { - get; - set; - } + public float LaunchImpulse { get; set; } [Serialize(1.0f, IsPropertySaveable.No, description: "Multiplies the damage the turret deals by this amount.")] - public float DamageMultiplier - { - get { return damageMultiplier; } - set { damageMultiplier = value; } - } + public float DamageMultiplier { get; set; } [Serialize(1, IsPropertySaveable.No, description: "How many projectiles the weapon launches when fired once.")] - public int ProjectileCount - { - get; - set; - } + public int ProjectileCount { get; set; } [Serialize(false, IsPropertySaveable.No, description: "Can the turret be fired without projectiles (causing it just to execute the OnUse effects and the firing animation without actually firing anything).")] - public bool LaunchWithoutProjectile - { - get; - set; - } - - [Editable(VectorComponentLabels = new string[] { "editable.minvalue", "editable.maxvalue" }), - Serialize("0.0,0.0", IsPropertySaveable.Yes, description: "The range at which the barrel can rotate.", alwaysUseInstanceValues: true)] - public Vector2 RotationLimits - { - get - { - return new Vector2(MathHelper.ToDegrees(minRotation), MathHelper.ToDegrees(maxRotation)); - } - set - { - minRotation = MathHelper.ToRadians(Math.Min(value.X, value.Y)); - maxRotation = MathHelper.ToRadians(Math.Max(value.X, value.Y)); - - rotation = (minRotation + maxRotation) / 2; -#if CLIENT - if (lightComponents != null) - { - foreach (var light in lightComponents) - { - light.Rotation = rotation; - light.Light.Rotation = -rotation; - } - } -#endif - } - } + public bool LaunchWithoutProjectile { get; set; } [Serialize(0.0f, IsPropertySaveable.No, description: "Random spread applied to the firing angle of the projectiles (in degrees).")] - public float Spread - { - get; - set; - } - - [Editable(0.0f, 1000.0f, DecimalCount = 2), - Serialize(5.0f, IsPropertySaveable.No, description: "How much torque is applied to rotate the barrel when the item is used by a character" - + " with insufficient skills to operate it. Higher values make the barrel rotate faster.")] - public float SpringStiffnessLowSkill - { - get; - private set; - } - [Editable(0.0f, 1000.0f, DecimalCount = 2), - Serialize(2.0f, IsPropertySaveable.No, description: "How much torque is applied to rotate the barrel when the item is used by a character" - + " with sufficient skills to operate it. Higher values make the barrel rotate faster.")] - public float SpringStiffnessHighSkill - { - get; - private set; - } - - [Editable(0.0f, 1000.0f, DecimalCount = 2), - Serialize(50.0f, IsPropertySaveable.No, description: "How much torque is applied to resist the movement of the barrel when the item is used by a character" - + " with insufficient skills to operate it. Higher values make the aiming more \"snappy\", stopping the barrel from swinging around the direction it's being aimed at.")] - public float SpringDampingLowSkill - { - get; - private set; - } - [Editable(0.0f, 1000.0f, DecimalCount = 2), - Serialize(10.0f, IsPropertySaveable.No, description: "How much torque is applied to resist the movement of the barrel when the item is used by a character" - + " with sufficient skills to operate it. Higher values make the aiming more \"snappy\", stopping the barrel from swinging around the direction it's being aimed at.")] - public float SpringDampingHighSkill - { - get; - private set; - } - - [Editable(0.0f, 100.0f, DecimalCount = 2), - Serialize(1.0f, IsPropertySaveable.No, description: "Maximum angular velocity of the barrel when used by a character with insufficient skills to operate it.")] - public float RotationSpeedLowSkill - { - get; - private set; - } - [Editable(0.0f, 100.0f, DecimalCount = 2), - Serialize(5.0f, IsPropertySaveable.No, description: "Maximum angular velocity of the barrel when used by a character with sufficient skills to operate it."),] - public float RotationSpeedHighSkill - { - get; - private set; - } - + public float Spread { get; set; } + [Serialize(1.0f, IsPropertySaveable.No, description: "How fast the turret can rotate while firing (for charged weapons).")] - public float FiringRotationSpeedModifier - { - get; - private set; - } + public float FiringRotationSpeedModifier { get; set; } [Serialize(false, IsPropertySaveable.Yes, description: "Whether the turret should always charge-up fully to shoot.")] - public bool SingleChargedShot - { - get; - private set; - } + public bool SingleChargedShot { get; set; } private float prevScale; float prevBaseRotation; @@ -314,11 +152,7 @@ namespace Barotrauma.Items.Components } [Serialize(3500.0f, IsPropertySaveable.Yes, description: "How close to a target the turret has to be for an AI character to fire it.")] - public float AIRange - { - get; - set; - } + public float AIRange { get; set; } private float _maxAngleOffset; [Serialize(10.0f, IsPropertySaveable.No, description: "How much off the turret can be from the target for the AI to shoot. In degrees.")] @@ -329,68 +163,164 @@ namespace Barotrauma.Items.Components } [Serialize(1.1f, IsPropertySaveable.No, description: "How much does the AI prefer currently selected targets over new targets closer to the turret.")] - public float AICurrentTargetPriorityMultiplier - { - get; - private set; - } + public float AICurrentTargetPriorityMultiplier { get; private set; } [Serialize(-1, IsPropertySaveable.Yes, description: "The turret won't fire additional projectiles if the number of previously fired, still active projectiles reaches this limit. If set to -1, there is no limit to the number of projectiles.")] - public int MaxActiveProjectiles - { - get; - set; - } + public int MaxActiveProjectiles { get; set; } [Serialize(0f, IsPropertySaveable.Yes, description: "The time required for a charge-type turret to charge up before able to fire.")] - public float MaxChargeTime - { - get; - private set; - } + public float MaxChargeTime { get; private set; } - [Serialize(false, IsPropertySaveable.Yes, description:"Should the turret operate automatically using AI targeting? Comes with some optional random movement that can be adjusted below."), Editable] + #region Editable properties + + [Serialize(5.0f, IsPropertySaveable.No, description: "The period of time the user has to wait between shots."), + Editable(0.0f, 1000.0f, decimals: 3)] + public float Reload { get; set; } + + [Serialize(1, IsPropertySaveable.No, description: "How many projectiles needs to be shot before we add an extra break? Think of the double coilgun."), + Editable(1, 100)] + public int ShotsPerBurst { get; set; } + + [Serialize(0.0f, IsPropertySaveable.No, description: "An extra delay between the bursts. Added to the reload."), + Editable(0.0f, 1000.0f, decimals: 3)] + public float DelayBetweenBursts { get; set; } + + [Serialize(1.0f, IsPropertySaveable.No, description: "Modifies the duration of retraction of the barrell after recoil to get back to the original position after shooting. Reload time affects this too."), + Editable(0.1f, 10f)] + public float RetractionDurationMultiplier { get; set; } + + [Serialize(0.1f, IsPropertySaveable.No, description: "How quickly the recoil moves the barrel after launching."), + Editable(0.1f, 10f)] + public float RecoilTime { get; set; } + + [Serialize(0f, IsPropertySaveable.No, description: "How long the barrell stays in place after the recoil and before retracting back to the original position."), + Editable(0f, 1000f)] + public float RetractionDelay { get; set; } + + [Editable(VectorComponentLabels = new string[] { "editable.minvalue", "editable.maxvalue" }), + Serialize("0.0,0.0", IsPropertySaveable.Yes, description: "The range at which the barrel can rotate.", alwaysUseInstanceValues: true)] + public Vector2 RotationLimits + { + get + { + return new Vector2(MathHelper.ToDegrees(minRotation), MathHelper.ToDegrees(maxRotation)); + } + set + { + float newMinRotation = MathHelper.ToRadians(value.X); + float newMaxRotation = MathHelper.ToRadians(value.Y); + + bool minRotationModified = MathHelper.Distance(newMinRotation, minRotation) > 0.02f; + bool maxRotationModified = MathHelper.Distance(newMaxRotation, maxRotation) > 0.02f; + + // if only one rotation changes (when editing via text field), use the other one to clamp to max range + if (minRotationModified && !maxRotationModified) + { + newMinRotation = MathHelper.Clamp(newMinRotation, maxRotation - MathHelper.TwoPi, maxRotation); + } + else if (!minRotationModified && maxRotationModified) + { + newMaxRotation = MathHelper.Clamp(newMaxRotation, minRotation, minRotation + MathHelper.TwoPi); + } + + maxRotation = newMaxRotation; + minRotation = newMinRotation; + + Rotation = (minRotation + maxRotation) / 2; +#if CLIENT + if (lightComponents != null) + { + foreach (var light in lightComponents) + { + light.Rotation = Rotation; + light.Light.Rotation = -Rotation; + } + } +#endif + } + } + + [Serialize(5.0f, IsPropertySaveable.No, description: "How much torque is applied to rotate the barrel when the item is used by a character with insufficient skills to operate it. Higher values make the barrel rotate faster."), + Editable(0.0f, 1000.0f, DecimalCount = 2)] + public float SpringStiffnessLowSkill { get; private set; } + + [Serialize(2.0f, IsPropertySaveable.No, description: "How much torque is applied to rotate the barrel when the item is used by a character with sufficient skills to operate it. Higher values make the barrel rotate faster."), + Editable(0.0f, 1000.0f, DecimalCount = 2)] + public float SpringStiffnessHighSkill { get; private set; } + + [Serialize(50.0f, IsPropertySaveable.No, description: "How much torque is applied to resist the movement of the barrel when the item is used by a character with insufficient skills to operate it. Higher values make the aiming more \"snappy\", stopping the barrel from swinging around the direction it's being aimed at."), + Editable(0.0f, 1000.0f, DecimalCount = 2)] + public float SpringDampingLowSkill { get; private set; } + + [Serialize(10.0f, IsPropertySaveable.No, description: "How much torque is applied to resist the movement of the barrel when the item is used by a character with sufficient skills to operate it. Higher values make the aiming more \"snappy\", stopping the barrel from swinging around the direction it's being aimed at."), + Editable(0.0f, 1000.0f, DecimalCount = 2)] + public float SpringDampingHighSkill { get; private set; } + + [Serialize(1.0f, IsPropertySaveable.No, description: "Maximum angular velocity of the barrel when used by a character with insufficient skills to operate it."), + Editable(0.0f, 100.0f, DecimalCount = 2)] + public float RotationSpeedLowSkill { get; private set; } + + [Serialize(5.0f, IsPropertySaveable.No, description: "Maximum angular velocity of the barrel when used by a character with sufficient skills to operate it."), + Editable(0.0f, 100.0f, DecimalCount = 2)] + public float RotationSpeedHighSkill { get; private set; } + + [Serialize("0,0,0,0", IsPropertySaveable.Yes, description: "Optional screen tint color when the item is being operated (R,G,B,A)."), + Editable] + public Color HudTint { get; set; } + + [Header(localizedTextTag: "sp.turret.AutoOperate.propertyheader")] + [Serialize(false, IsPropertySaveable.Yes, description:"Should the turret operate automatically using AI targeting? Comes with some optional random movement that can be adjusted below."), + Editable(TransferToSwappedItem = true)] public bool AutoOperate { get; set; } - [Serialize(0f, IsPropertySaveable.Yes, description: "[Auto Operate] How much the turret should adjust the aim off the target randomly instead of tracking the target perfectly? In Degrees."), Editable] + [Serialize(0f, IsPropertySaveable.Yes, description: "[Auto Operate] How much the turret should adjust the aim off the target randomly instead of tracking the target perfectly? In Degrees."), + Editable(TransferToSwappedItem = true)] public float RandomAimAmount { get; set; } - [Serialize(0f, IsPropertySaveable.Yes, description: "[Auto Operate] How often the turret should adjust the aim randomly instead of tracking the target perfectly? Minimum wait time, in seconds."), Editable] + [Serialize(0f, IsPropertySaveable.Yes, description: "[Auto Operate] How often the turret should adjust the aim randomly instead of tracking the target perfectly? Minimum wait time, in seconds."), + Editable(TransferToSwappedItem = true)] public float RandomAimMinTime { get; set; } - [Serialize(0f, IsPropertySaveable.Yes, description: "[Auto Operate] How often the turret should adjust the aim randomly instead of tracking the target perfectly? Maximum wait time, in seconds."), Editable] + [Serialize(0f, IsPropertySaveable.Yes, description: "[Auto Operate] How often the turret should adjust the aim randomly instead of tracking the target perfectly? Maximum wait time, in seconds."), + Editable(TransferToSwappedItem = true)] public float RandomAimMaxTime { get; set; } - [Serialize(false, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret move randomly while idle?"), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret move randomly while idle?"), + Editable(TransferToSwappedItem = true)] public bool RandomMovement { get; set; } - [Serialize(false, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret have a delay while targeting targets or always aim prefectly?"), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret have a delay while targeting targets or always aim prefectly?"), + Editable(TransferToSwappedItem = true)] public bool AimDelay { get; set; } - [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target characters in general?"), Editable] + [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target characters in general?"), + Editable(TransferToSwappedItem = true)] public bool TargetCharacters { get; set; } - [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target all monsters?"), Editable] + [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target all monsters?"), + Editable(TransferToSwappedItem = true)] public bool TargetMonsters { get; set; } - [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target all humans (or creatures in the same group, like pets)?"), Editable] + [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target all humans (or creatures in the same group, like pets)?"), + Editable(TransferToSwappedItem = true)] public bool TargetHumans { get; set; } - [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target other submarines?"), Editable] + [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target other submarines?"), + Editable(TransferToSwappedItem = true)] public bool TargetSubmarines { get; set; } - [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target items?"), Editable] + [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target items?"), + Editable(TransferToSwappedItem = true)] public bool TargetItems { get; set; } - [Serialize("", IsPropertySaveable.Yes, description: "[Auto Operate] Group or SpeciesName that the AI ignores when the turret is operated automatically."), Editable] + [Serialize("", IsPropertySaveable.Yes, description: "[Auto Operate] Group or SpeciesName that the AI ignores when the turret is operated automatically."), + Editable(TransferToSwappedItem = true)] public Identifier FriendlyTag { get; private set; } - - [Editable, Serialize("0,0,0,0", IsPropertySaveable.Yes, description: "Optional screen tint color when the item is being operated (R,G,B,A).")] - public Color HudTint - { - get; - private set; - } + + #endregion + + private const string SetAutoOperatePin = "set_auto_operate"; + private const string ToggleAutoOperatePin = "toggle_auto_operate"; public Turret(Item item, ContentXElement element) : base(item, element) @@ -442,8 +372,16 @@ namespace Barotrauma.Items.Components base.OnMapLoaded(); if (loadedRotationLimits.HasValue) { RotationLimits = loadedRotationLimits.Value; } if (loadedBaseRotation.HasValue) { BaseRotation = loadedBaseRotation.Value; } - targetRotation = rotation; + targetRotation = Rotation; UpdateTransformedBarrelPos(); + if (!AutoOperate) + { + // If the turret is not set to auto operate, don't allow changing the state with wirings. + foreach (ConnectionPanel connectionPanel in Item.GetComponents()) + { + connectionPanel.Connections.RemoveAll(c => c.Name is ToggleAutoOperatePin or SetAutoOperatePin); + } + } } private void FindLightComponents() @@ -471,7 +409,7 @@ namespace Barotrauma.Items.Components // We want the turret to control the state of the LightComponent, not tie it's state to the state of the Turret (the light can be inactive even if the turret is active) light.Parent = null; light.Rotation = Rotation - item.RotationRad; - light.Light.Rotation = -rotation; + light.Light.Rotation = -Rotation; //turret lights are high-prio (don't want the lights to disappear when you're fighting something) light.Light.PriorityMultiplier *= 10.0f; } @@ -520,8 +458,8 @@ namespace Barotrauma.Items.Components { // single charged shot guns will decharge after firing // for cosmetic reasons, this is done by lerping in half the reload time - currentChargeTime = reloadTime > 0.0f ? - Math.Max(0f, MaxChargeTime * (reload / reloadTime - 0.5f)) : + currentChargeTime = Reload > 0.0f ? + Math.Max(0f, MaxChargeTime * (reload / Reload - 0.5f)) : 0.0f; } else @@ -584,9 +522,9 @@ namespace Barotrauma.Items.Components SkillSettings.Current.SkillIncreasePerSecondWhenOperatingTurret * deltaTime); } - float rotMidDiff = MathHelper.WrapAngle(rotation - (minRotation + maxRotation) / 2.0f); + float rotMidDiff = MathHelper.WrapAngle(Rotation - (minRotation + maxRotation) / 2.0f); - float targetRotationDiff = MathHelper.WrapAngle(targetRotation - rotation); + float targetRotationDiff = MathHelper.WrapAngle(targetRotation - Rotation); if ((maxRotation - minRotation) < MathHelper.TwoPi) { @@ -611,18 +549,18 @@ namespace Barotrauma.Items.Components (targetRotationDiff * springStiffness - angularVelocity * springDamping) * deltaTime; angularVelocity = MathHelper.Clamp(angularVelocity, -rotationSpeed, rotationSpeed); - rotation += angularVelocity * deltaTime; + Rotation += angularVelocity * deltaTime; - rotMidDiff = MathHelper.WrapAngle(rotation - (minRotation + maxRotation) / 2.0f); + rotMidDiff = MathHelper.WrapAngle(Rotation - (minRotation + maxRotation) / 2.0f); if (rotMidDiff < -maxDist) { - rotation = minRotation; + Rotation = minRotation; angularVelocity *= -0.5f; } else if (rotMidDiff > maxDist) { - rotation = maxRotation; + Rotation = maxRotation; angularVelocity *= -0.5f; } @@ -683,7 +621,7 @@ namespace Barotrauma.Items.Components private Vector2 GetBarrelDir() { - return new Vector2((float)Math.Cos(rotation), -(float)Math.Sin(rotation)); + return new Vector2((float)Math.Cos(Rotation), -(float)Math.Sin(Rotation)); } private bool TryLaunch(float deltaTime, Character character = null, bool ignorePower = false) @@ -888,18 +826,13 @@ namespace Barotrauma.Items.Components public EventData(Item projectile, Turret turret) { - System.Diagnostics.Debug.Assert(projectile != null, $"Tried to create Turret {nameof(EventData)} with no projectile."); - GameAnalyticsManager.AddErrorEventOnce( - "Turret.EventData:entitynull"+ turret.Item.Prefab.Identifier, - GameAnalyticsManager.ErrorSeverity.Error, - $"Turret \"{turret.Item.Prefab.Identifier}\" tried to create {nameof(EventData)} with no projectile."); Projectile = projectile; } } private void Launch(Item projectile, Character user = null, float? launchRotation = null, float tinkeringStrength = 0f) { - reload = reloadTime; + reload = Reload; if (ShotsPerBurst > 1) { shotCounter++; @@ -946,7 +879,7 @@ namespace Barotrauma.Items.Components { launchPos = Submarine.LastPickedPosition; } - projectile.SetTransform(launchPos, -(launchRotation ?? rotation) + spread); + projectile.SetTransform(launchPos, -(launchRotation ?? Rotation) + spread); projectile.UpdateTransform(); projectile.Submarine = projectile.body?.Submarine; @@ -1540,7 +1473,7 @@ namespace Barotrauma.Items.Components if (!IsWithinAimingRadius(closestPoint)) { // The closest point can't be targeted -> get a point directly in front of the turret - Vector2 barrelDir = new Vector2((float)Math.Cos(rotation), -(float)Math.Sin(rotation)); + Vector2 barrelDir = new Vector2((float)Math.Cos(Rotation), -(float)Math.Sin(Rotation)); if (MathUtils.GetLineSegmentIntersection(p1, p2, item.WorldPosition, item.WorldPosition + barrelDir * shootDistance, out Vector2 intersection)) { closestPoint = intersection; @@ -1687,7 +1620,7 @@ namespace Barotrauma.Items.Components private bool IsPointingTowards(Vector2 targetPos) { float enemyAngle = MathUtils.VectorToAngle(targetPos - item.WorldPosition); - float turretAngle = -rotation; + float turretAngle = -Rotation; float maxAngleError = MathHelper.ToRadians(MaxAngleOffset); if (MaxChargeTime > 0.0f && currentChargingState == ChargingState.WindingUp && FiringRotationSpeedModifier > 0.0f) { @@ -1720,7 +1653,7 @@ namespace Barotrauma.Items.Components } else if (target is Item targetItem) { - if (targetItem.Removed || targetItem.Condition <= 0 || !targetItem.Prefab.IsAITurretTarget || targetItem.Prefab.AITurretPriority <= 0 || targetItem.HiddenInGame) + if (targetItem.Removed || targetItem.Condition <= 0 || !targetItem.Prefab.IsAITurretTarget || targetItem.Prefab.AITurretPriority <= 0 || targetItem.IsHidden) { return false; } @@ -1832,7 +1765,7 @@ namespace Barotrauma.Items.Components { Vector2 currOffSet = FiringOffset; if (flipFiringOffset) { currOffSet.X = -currOffSet.X; } - transformedFiringOffset = MathUtils.RotatePoint(new Vector2(-currOffSet.Y, -currOffSet.X) * item.Scale, -rotation); + transformedFiringOffset = MathUtils.RotatePoint(new Vector2(-currOffSet.Y, -currOffSet.X) * item.Scale, -Rotation); } return new Vector2(item.WorldRect.X + transformedBarrelPos.X + transformedFiringOffset.X, item.WorldRect.Y - transformedBarrelPos.Y + transformedFiringOffset.Y); } @@ -1939,7 +1872,7 @@ namespace Barotrauma.Items.Components minRotation += MathHelper.TwoPi; maxRotation += MathHelper.TwoPi; } - targetRotation = rotation = (minRotation + maxRotation) / 2; + targetRotation = Rotation = (minRotation + maxRotation) / 2; UpdateTransformedBarrelPos(); UpdateLightComponents(); @@ -1961,7 +1894,7 @@ namespace Barotrauma.Items.Components minRotation += MathHelper.TwoPi; maxRotation += MathHelper.TwoPi; } - targetRotation = rotation = (minRotation + maxRotation) / 2; + targetRotation = Rotation = (minRotation + maxRotation) / 2; UpdateTransformedBarrelPos(); UpdateLightComponents(); @@ -2019,14 +1952,23 @@ namespace Barotrauma.Items.Components UpdateLightComponents(); } break; + case SetAutoOperatePin: + AutoOperate = signal.value != "0"; + break; + case ToggleAutoOperatePin: + if (signal.value != "0") + { + AutoOperate = !AutoOperate; + } + break; } } private Vector2? loadedRotationLimits; private float? loadedBaseRotation; - public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap) { - base.Load(componentElement, usePrefabValues, idRemap); + base.Load(componentElement, usePrefabValues, idRemap, isItemSwap); loadedRotationLimits = componentElement.GetAttributeVector2("rotationlimits", RotationLimits); loadedBaseRotation = componentElement.GetAttributeFloat("baserotation", componentElement.Parent.GetAttributeFloat("rotation", BaseRotation)); } @@ -2035,7 +1977,7 @@ namespace Barotrauma.Items.Components { base.OnItemLoaded(); FindLightComponents(); - targetRotation = rotation; + targetRotation = Rotation; if (!loadedBaseRotation.HasValue) { if (item.FlippedX) { FlipX(relativeToSub: false); } @@ -2047,8 +1989,8 @@ namespace Barotrauma.Items.Components { if (TryExtractEventData(extraData, out EventData eventData)) { - msg.WriteUInt16(eventData.Projectile?.ID ?? Entity.NullEntityID); - msg.WriteRangedSingle(MathHelper.Clamp(wrapAngle(rotation), minRotation, maxRotation), minRotation, maxRotation, 16); + msg.WriteUInt16(eventData.Projectile?.ID ?? LaunchWithoutProjectileId); + msg.WriteRangedSingle(MathHelper.Clamp(wrapAngle(Rotation), minRotation, maxRotation), minRotation, maxRotation, 16); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index 001fd3eef..805d46e52 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -582,9 +582,9 @@ namespace Barotrauma.Items.Components } private int loadedVariant = -1; - public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap) { - base.Load(componentElement, usePrefabValues, idRemap); + base.Load(componentElement, usePrefabValues, idRemap, isItemSwap); loadedVariant = componentElement.GetAttributeInt("variant", -1); } public override void OnItemLoaded() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ContainerTagPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ContainerTagPrefab.cs index ad61ca8e2..a629fba3f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ContainerTagPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ContainerTagPrefab.cs @@ -129,7 +129,8 @@ namespace Barotrauma { if (!allContainerTagsInTheGame.Contains(prefab.Identifier)) { - DebugConsole.ThrowError($"Container tag \"{prefab.Identifier}\" defined in ContainerTagPrefab is not used in any item prefabs, did you misspell it?", contentPackage: prefab.ContentPackage); + DebugConsole.AddWarning($"Container tag \"{prefab.Identifier}\" defined in ContainerTagPrefab is not used in any item prefabs, did you misspell it? It's also possible mods override container tags in a way that causes some of the pre-defined tags to become unused.", + contentPackage: prefab.ContentPackage); } } @@ -139,7 +140,7 @@ namespace Barotrauma { if (Prefabs.All(p => p.Identifier != vanillaTag)) { - DebugConsole.ThrowError($"Container tag \"{vanillaTag}\" is used in vanilla item prefabs but not defined in a ContainerTagPrefab."); + DebugConsole.AddWarning($"Container tag \"{vanillaTag}\" is used in vanilla item prefabs but not defined in a ContainerTagPrefab."); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index 078765cf1..0d626dd60 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -228,6 +228,9 @@ namespace Barotrauma public readonly Entity Owner; + /// + /// Capacity, or the number of slots in the inventory. + /// protected readonly int capacity; protected readonly ItemSlot[] slots; @@ -272,6 +275,21 @@ namespace Barotrauma { get { return capacity; } } + + public static bool IsDragAndDropGiveAllowed + { + get + { + // allowed for single player + if (GameMain.NetworkMember == null) + { + return true; + } + + // controlled by server setting in multiplayer + return GameMain.NetworkMember.ServerSettings.AllowDragAndDropGive; + } + } public int EmptySlotCount => slots.Count(i => !i.Empty()); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index b8fa59f5b..35b3a877e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -231,6 +231,12 @@ namespace Barotrauma public bool EditableWhenEquipped { get; set; } = false; + /// + /// Which character equipped this item? + /// May not be the same character as the one who it's equipped on (you can e.g. equip diving masks on another character). + /// + public Character Equipper; + //the inventory in which the item is contained in public Inventory ParentInventory { @@ -387,7 +393,7 @@ namespace Barotrauma return true; } #endif - if (HiddenInGame) { return false; } + if (IsHidden) { return false; } if (character != null && character.IsOnPlayerTeam) { return IsPlayerTeamInteractable; @@ -759,7 +765,16 @@ namespace Barotrauma [Editable, Serialize(false, isSaveable: IsPropertySaveable.Yes, "When enabled will prevent the item from taking damage from all sources")] public bool InvulnerableToDamage { get; set; } + /// + /// Was the item stolen during the current round. Note that it's possible for the items to be found in the player's inventory even though they weren't actually stolen. + /// For example, a guard can place handcuffs there. So use for checking if the item is illegitimately held. + /// public bool StolenDuringRound; + + /// + /// Item shouldn't be in the player's inventory. If the guards find it, they will consider it as a theft. + /// + public bool Illegitimate => !AllowStealing && SpawnedInCurrentOutpost; private bool spawnedInCurrentOutpost; public bool SpawnedInCurrentOutpost @@ -1116,8 +1131,8 @@ namespace Barotrauma string collisionCategoryStr = subElement.GetAttributeString("collisioncategory", null); Category collisionCategory = Physics.CollisionItem; - Category collidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform | Physics.CollisionRepairableWall; - if ((Prefab.DamagedByProjectiles || Prefab.DamagedByMeleeWeapons) && Condition > 0) + Category collidesWith = Physics.DefaultItemCollidesWith; + if ((Prefab.DamagedByProjectiles || Prefab.DamagedByMeleeWeapons || Prefab.DamagedByRepairTools) && Condition > 0) { //force collision category to Character to allow projectiles and weapons to hit //(we could also do this by making the projectiles and weapons hit CollisionItem @@ -2241,7 +2256,7 @@ namespace Barotrauma public override void Update(float deltaTime, Camera cam) { - if (!isActive) { return; } + if (!isActive || IsLayerHidden) { return; } if (impactQueue != null) { @@ -3068,6 +3083,13 @@ namespace Barotrauma if (ic.CanBeSelected && ic is not Door) { selected = true; } } } + if (ParentInventory?.Owner == user && + GetComponent() != null) + { + //can't select ItemContainers in the character's inventory + //(the inventory is drawn by hovering the cursor over the inventory slot, not as a hovering interface on the screen) + selected = false; + } if (!picked) { return false; } @@ -3927,7 +3949,8 @@ namespace Barotrauma //if we're overriding a non-overridden item in a sub/assembly xml or vice versa, //use the values from the prefab instead of loading them from the sub/assembly xml - bool usePrefabValues = thisIsOverride != ItemPrefab.Prefabs.IsOverride(prefab) || appliedSwap != null; + bool isItemSwap = appliedSwap != null; + bool usePrefabValues = thisIsOverride != ItemPrefab.Prefabs.IsOverride(prefab) || isItemSwap; List unloadedComponents = new List(item.components); foreach (var subElement in element.Elements()) { @@ -3940,7 +3963,7 @@ namespace Barotrauma int level = subElement.GetAttributeInt("level", 1); if (upgradePrefab != null) { - item.AddUpgrade(new Upgrade(item, upgradePrefab, level, appliedSwap != null ? null : subElement)); + item.AddUpgrade(new Upgrade(item, upgradePrefab, level, isItemSwap ? null : subElement)); } else { @@ -3958,13 +3981,13 @@ namespace Barotrauma { ItemComponent component = unloadedComponents.Find(x => x.Name == subElement.Name.ToString()); if (component == null) { continue; } - component.Load(subElement, usePrefabValues, idRemap); + component.Load(subElement, usePrefabValues, idRemap, isItemSwap); unloadedComponents.Remove(component); break; } } } - if (usePrefabValues && appliedSwap == null) + if (usePrefabValues && !isItemSwap) { //use prefab scale when overriding a non-overridden item or vice versa item.Scale = prefab.ConfigElement.GetAttributeFloat(item.scale, "scale", "Scale"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index d9a727c97..7bea0b8b8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -340,8 +340,12 @@ namespace Barotrauma } } - this.RequiredSkills = requiredSkills.ToImmutableArray(); - this.RequiredItems = requiredItems.ToImmutableArray(); + RequiredSkills = requiredSkills.ToImmutableArray(); + RequiredItems = requiredItems + /*Put the items required by identifier first - since we must use specific items for those, we should check them before the ones that accept multiple items. + Otherwise we might end up choosing the "specific item" as the multi-option ingredient, and not have enough left for the "specific item" requirement */ + .OrderBy(requiredItem => requiredItem is RequiredItemByIdentifier ? 0 : 1) + .ToImmutableArray(); RecipeHash = GenerateHash(); } @@ -438,8 +442,12 @@ namespace Barotrauma public int GetPrice(Location location = null) { - int price = BasePrice; - return location?.GetAdjustedMechanicalCost(price) ?? price; + int price = location?.GetAdjustedMechanicalCost(BasePrice) ?? BasePrice; + if (GameMain.GameSession?.Campaign is CampaignMode campaign) + { + price = (int)(price * campaign.Settings.ShipyardPriceMultiplier); + } + return price; } public SwappableItem(ContentXElement element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs index f868f7d3b..b681da829 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs @@ -134,6 +134,12 @@ namespace Barotrauma /// public bool SetActive; + /// + /// Only valid when used in the Containable definitions of an ItemContainer. + /// Should the character who equipped the item be blamed if the wearer / character who's inventory the item is in dies? + /// + public bool BlameEquipperForDeath; + /// /// Only valid for the RequiredItems of an ItemComponent. Can be used to make the requirement optional, /// meaning that you don't need to have the item to interact with something, but having it may still affect what the interaction does (such as using a crowbar on a door). @@ -272,6 +278,7 @@ namespace Barotrauma AllowVariants = element.GetAttributeBool("allowvariants", true); Rotation = element.GetAttributeFloat("rotation", 0f); SetActive = element.GetAttributeBool("setactive", false); + BlameEquipperForDeath = element.GetAttributeBool(nameof(BlameEquipperForDeath), false); CharacterInventorySlotType = element.GetAttributeEnum(nameof(CharacterInventorySlotType), InvSlotType.None); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/GrowIdleState.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/GrowIdleState.cs index 9246cb7e3..cd89bfe5c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/GrowIdleState.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/GrowIdleState.cs @@ -92,7 +92,7 @@ namespace Barotrauma.MapCreatures.Behavior if (availableBranches.Count == 0) { return availableBranches; } //prefer growing from the branches furthest from the root (ones with the largest branch depth) - var branch = ToolBox.SelectWeightedRandom(availableBranches, b => (float)b.BranchDepth, Rand.RandSync.Unsynced); + var branch = ToolBox.SelectWeightedRandom(availableBranches, b => b.BranchDepth, Rand.RandSync.Unsynced); TileSide side = branch.GetRandomFreeSide(); if (side == TileSide.None) { return availableBranches; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index 4d3e9b8d9..643a228ed 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -756,11 +756,12 @@ namespace Barotrauma hull2.Oxygen -= deltaOxygen; } - public static Gap FindAdjacent(IEnumerable gaps, Vector2 worldPos, float allowedOrthogonalDist) + public static Gap FindAdjacent(IEnumerable gaps, Vector2 worldPos, float allowedOrthogonalDist, bool allowRoomToRoom = false) { foreach (Gap gap in gaps) { - if (gap.Open == 0.0f || gap.IsRoomToRoom) { continue; } + if (gap.Open == 0.0f) { continue; } + if (gap.IsRoomToRoom && !allowRoomToRoom) { continue; } if (gap.ConnectedWall != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index cea585ed9..063606549 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; +using System.Xml.Linq; using Voronoi2; namespace Barotrauma @@ -430,6 +431,11 @@ namespace Barotrauma { get { return ForcedDifficulty ?? LevelData.Difficulty; } } + + /// + /// Inclusive (matching the min an max values is accepted). + /// + public bool IsAllowedDifficulty(float minDifficulty, float maxDifficulty) => LevelData.IsAllowedDifficulty(minDifficulty, maxDifficulty); public LevelData.LevelType Type { @@ -3718,7 +3724,7 @@ namespace Barotrauma return MathUtils.LineSegmentToPointDistanceSquared(endPosition, endExitPosition, position) < minDist * minDist; } - private Submarine SpawnSubOnPath(string subName, ContentFile contentFile, SubmarineType type) + private Submarine SpawnSubOnPath(string subName, ContentFile contentFile, SubmarineType type, bool forceThalamus = false) { var tempSW = new Stopwatch(); @@ -3801,7 +3807,7 @@ namespace Barotrauma } } // Only spawn thalamus when the wreck has some thalamus items defined. - if (Rand.Value(Rand.RandSync.ServerAndClient) <= Loaded.GenerationParams.ThalamusProbability && sub.GetItems(false).Any(i => i.Prefab.HasSubCategory("thalamus"))) + if ((forceThalamus || Rand.Value(Rand.RandSync.ServerAndClient) <= Loaded.GenerationParams.ThalamusProbability) && sub.GetItems(false).Any(i => i.Prefab.HasSubCategory("thalamus"))) { if (!sub.CreateWreckAI()) { @@ -4039,72 +4045,114 @@ namespace Barotrauma private readonly Dictionary> wreckPositions = new Dictionary>(); private readonly Dictionary> blockedRects = new Dictionary>(); + private readonly record struct PlaceableWreck(WreckFile WreckFile, WreckInfo WreckInfo) + { + public static Option TryCreate(WreckFile wreckFile) + { + var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(i => i.FilePath == wreckFile.Path.Value); + if (matchingSub?.WreckInfo is null) + { + DebugConsole.ThrowError($"No matching submarine info found for the wreck file {wreckFile.Path.Value}"); + return Option.None; + } + + return Option.Some(new PlaceableWreck(wreckFile, matchingSub.WreckInfo)); + } + } + private void CreateWrecks() { var totalSW = new Stopwatch(); totalSW.Start(); - var wreckFiles = ContentPackageManager.EnabledPackages.All + var placeableWrecks = ContentPackageManager.EnabledPackages.All .SelectMany(p => p.GetFiles()) - .OrderBy(f => f.UintIdentifier).ToList(); + .OrderBy(f => f.UintIdentifier) + .Select(PlaceableWreck.TryCreate) + .Where(w => w.IsSome()) + .Select(o => o.TryUnwrap(out var w) ? w : throw new InvalidOperationException()) + .ToList(); - for (int i = wreckFiles.Count - 1; i >= 0; i--) + for (int i = placeableWrecks.Count - 1; i >= 0; i--) { - var wreckFile = wreckFiles[i]; - var wreckInfos = SubmarineInfo.SavedSubmarines.Where(i => i.IsWreck); - var matchingInfo = wreckInfos.SingleOrDefault(info => info.FilePath == wreckFile.Path.Value); - Debug.Assert(matchingInfo != null); - if (matchingInfo?.WreckInfo is WreckInfo wreckInfo) + var wreckInfo = placeableWrecks[i].WreckInfo; + if (!IsAllowedDifficulty(wreckInfo.MinLevelDifficulty, wreckInfo.MaxLevelDifficulty)) { - if (Difficulty < wreckInfo.MinLevelDifficulty || Difficulty > wreckInfo.MaxLevelDifficulty) - { - wreckFiles.RemoveAt(i); - } + placeableWrecks.RemoveAt(i); } } - if (wreckFiles.None()) + if (placeableWrecks.None()) { DebugConsole.ThrowError($"No wreck files found for the level difficulty {LevelData.Difficulty}!"); Wrecks = new List(); return; } - wreckFiles.Shuffle(Rand.RandSync.ServerAndClient); + placeableWrecks.Shuffle(Rand.RandSync.ServerAndClient); - int minWreckCount = Math.Min(Loaded.GenerationParams.MinWreckCount, wreckFiles.Count); - int maxWreckCount = Math.Min(Loaded.GenerationParams.MaxWreckCount, wreckFiles.Count); + int minWreckCount = Math.Min(Loaded.GenerationParams.MinWreckCount, placeableWrecks.Count); + int maxWreckCount = Math.Min(Loaded.GenerationParams.MaxWreckCount, placeableWrecks.Count); int wreckCount = Rand.Range(minWreckCount, maxWreckCount + 1, Rand.RandSync.ServerAndClient); + bool requireThalamus = false; if (GameMain.GameSession?.GameMode?.Missions.Any(m => m.Prefab.RequireWreck) ?? false) { wreckCount = Math.Max(wreckCount, 1); } + + if (GameMain.GameSession?.GameMode?.Missions.Any(static m => m.Prefab.RequireThalamusWreck) ?? false) + { + requireThalamus = true; + } if (LevelData.ForceWreck != null) { //force the desired wreck to be chosen first - var matchingFile = wreckFiles.FirstOrDefault(w => w.Path == LevelData.ForceWreck.FilePath); - if (matchingFile != null) + var matchingFile = placeableWrecks.FirstOrDefault(w => w.WreckFile.Path == LevelData.ForceWreck.FilePath); + if (matchingFile.WreckFile != null) { - wreckFiles.Remove(matchingFile); - wreckFiles.Insert(0, matchingFile); + placeableWrecks.Remove(matchingFile); + placeableWrecks.Insert(0, matchingFile); } wreckCount = Math.Max(wreckCount, 1); } + if (requireThalamus) + { + var thalamusWrecks = placeableWrecks + .Where(static w => w.WreckInfo.WreckContainsThalamus == WreckInfo.HasThalamus.Yes) + .ToList(); + + if (thalamusWrecks.Any()) + { + thalamusWrecks.Shuffle(Rand.RandSync.ServerAndClient); + + foreach (var wreck in thalamusWrecks) + { + placeableWrecks.Remove(wreck); + placeableWrecks.Insert(0, wreck); + } + } + } + Wrecks = new List(wreckCount); for (int i = 0; i < wreckCount; i++) { //how many times we'll try placing another sub before giving up const int MaxSubsToTry = 2; int attempts = 0; - while (wreckFiles.Any() && attempts < MaxSubsToTry) + while (placeableWrecks.Any() && attempts < MaxSubsToTry) { - ContentFile contentFile = wreckFiles.First(); - wreckFiles.RemoveAt(0); - if (contentFile == null) { continue; } - string wreckName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path.Value); - if (SpawnSubOnPath(wreckName, contentFile, SubmarineType.Wreck) != null) + var placeableWreck = placeableWrecks.First(); + var wreckFile = placeableWreck.WreckFile; + placeableWrecks.RemoveAt(0); + if (wreckFile == null) { continue; } + string wreckName = System.IO.Path.GetFileNameWithoutExtension(wreckFile.Path.Value); + if (SpawnSubOnPath(wreckName, wreckFile, SubmarineType.Wreck, forceThalamus: requireThalamus) is { } wreck) { + if (wreck.WreckAI is not null) + { + requireThalamus = false; + } //placed successfully break; } @@ -4223,7 +4271,7 @@ namespace Barotrauma { foreach (MapEntity entityToHide in MapEntity.MapEntityList.Where(me => me.Submarine == outpost && (me.Prefab?.HasSubCategory(categoryToHide) ?? false))) { - entityToHide.HiddenInGame = true; + entityToHide.IsLayerHidden = true; } } } @@ -4489,72 +4537,97 @@ namespace Barotrauma else if (GameMain.NetworkMember is not { IsClient: true }) { bool allowDisconnectedWires = true; + bool allowDamagedDevices = true; bool allowDamagedWalls = true; if (BeaconStation?.Info?.BeaconStationInfo is BeaconStationInfo info) { allowDisconnectedWires = info.AllowDisconnectedWires; allowDamagedWalls = info.AllowDamagedWalls; + allowDamagedDevices = info.AllowDamagedDevices; } //remove wires - float removeWireMinDifficulty = 20.0f; - float removeWireProbability = MathUtils.InverseLerp(removeWireMinDifficulty, 100.0f, LevelData.Difficulty) * 0.5f; - if (removeWireProbability > 0.0f && allowDisconnectedWires) + float disconnectWireMinDifficulty = 20.0f; + float disconnectWireProbability = MathUtils.InverseLerp(disconnectWireMinDifficulty, 100.0f, LevelData.Difficulty) * 0.5f; + if (disconnectWireProbability > 0.0f && allowDisconnectedWires) { - foreach (Item item in beaconItems.Where(it => it.GetComponent() != null).ToList()) - { - if (item.NonInteractable || item.InvulnerableToDamage) { 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 - } - } - } - } + DisconnectBeaconStationWires(disconnectWireProbability); } + if (allowDamagedDevices) + { + DamageBeaconStationDevices(breakDeviceProbability: 0.5f); + } if (allowDamagedWalls) { - //break powered items - foreach (Item item in beaconItems.Where(it => it.Components.Any(c => c is Powered) && it.Components.Any(c => c is Repairable))) + DamageBeaconStationWalls(damageWallProbability: 0.25f); + } + } + SetLinkedSubCrushDepth(BeaconStation); + } + + public void DisconnectBeaconStationWires(float disconnectWireProbability) + { + if (disconnectWireProbability <= 0.0f) { return; } + List beaconItems = Item.ItemList.FindAll(it => it.Submarine == BeaconStation); + foreach (Item item in beaconItems.Where(it => it.GetComponent() != null).ToList()) + { + if (item.NonInteractable || item.InvulnerableToDamage) { 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) < disconnectWireProbability) + { + foreach (Connection connection in wire.Connections) { - if (item.NonInteractable || item.InvulnerableToDamage) { continue; } - if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.5f) + if (connection != null) { - item.Condition *= Rand.Range(0.6f, 0.8f, Rand.RandSync.Unsynced); - } - } - //poke holes in the walls - foreach (Structure structure in Structure.WallList.Where(s => s.Submarine == BeaconStation)) - { - if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.25f) - { - int sectionIndex = Rand.Range(0, structure.SectionCount - 1, Rand.RandSync.Unsynced); - structure.AddDamage(sectionIndex, Rand.Range(structure.MaxHealth * 0.2f, structure.MaxHealth, Rand.RandSync.Unsynced)); + connection.ConnectionPanel.DisconnectedWires.Add(wire); + wire.RemoveConnection(connection.Item); +#if SERVER + connection.ConnectionPanel.Item.CreateServerEvent(connection.ConnectionPanel); + wire.CreateNetworkEvent(); +#endif } } } } - SetLinkedSubCrushDepth(BeaconStation); + } + + public void DamageBeaconStationDevices(float breakDeviceProbability) + { + if (breakDeviceProbability <= 0.0f) { return; } + //break powered items + List beaconItems = Item.ItemList.FindAll(it => it.Submarine == BeaconStation); + foreach (Item item in beaconItems.Where(it => it.Components.Any(c => c is Powered) && it.Components.Any(c => c is Repairable))) + { + if (item.NonInteractable || item.InvulnerableToDamage) { continue; } + if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < breakDeviceProbability) + { + item.Condition *= Rand.Range(0.6f, 0.8f, Rand.RandSync.Unsynced); + } + } + } + + public void DamageBeaconStationWalls(float damageWallProbability) + { + if (damageWallProbability <= 0.0f) { return; } + //poke holes in the walls + foreach (Structure structure in Structure.WallList.Where(s => s.Submarine == BeaconStation)) + { + if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < damageWallProbability) + { + int sectionIndex = Rand.Range(0, structure.SectionCount - 1, Rand.RandSync.Unsynced); + structure.AddDamage(sectionIndex, Rand.Range(structure.MaxHealth * 0.2f, structure.MaxHealth, Rand.RandSync.Unsynced)); + } + } } public bool CheckBeaconActive() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index fb6e9d1bf..7b1797956 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -99,6 +99,11 @@ namespace Barotrauma return Math.Max(Size.Y * Physics.DisplayToRealWorldRatio, Level.DefaultRealWorldCrushDepth); } } + + /// + /// Inclusive (matching the min an max values is accepted). + /// + public bool IsAllowedDifficulty(float minDifficulty, float maxDifficulty) => Difficulty >= minDifficulty && Difficulty <= maxDifficulty; public LevelData(string seed, float difficulty, float sizeFactor, LevelGenerationParams generationParams, Biome biome) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs index 66e83c546..9e578573b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs @@ -60,6 +60,7 @@ namespace Barotrauma set; } + [Header("General")] [Serialize(LevelData.LevelType.LocationConnection, IsPropertySaveable.Yes), Editable] public LevelData.LevelType Type { @@ -88,42 +89,9 @@ namespace Barotrauma set; } - [Serialize("27,30,36", IsPropertySaveable.Yes), Editable] - public Color AmbientLightColor - { - get; - set; - } - - [Serialize("20,40,50", IsPropertySaveable.Yes), Editable] - public Color BackgroundTextureColor - { - get; - set; - } - - [Serialize("20,40,50", IsPropertySaveable.Yes), Editable] - public Color BackgroundColor - { - get; - set; - } - - [Serialize("255,255,255", IsPropertySaveable.Yes), Editable] - public Color WallColor - { - get; - set; - } - - [Serialize("255,255,255", IsPropertySaveable.Yes), Editable] - public Color WaterParticleColor - { - get; - set; - } - private Vector2 startPosition; + + [Header("Layout")] [Serialize("0,0", IsPropertySaveable.Yes, "Start position of the level (relative to the size of the level. 0,0 = top left corner, 1,1 = bottom right corner)"), Editable(DecimalCount = 2)] public Vector2 StartPosition { @@ -169,32 +137,11 @@ namespace Barotrauma set; } - [Serialize(true, IsPropertySaveable.Yes, "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 + [Serialize(0.4f, IsPropertySaveable.Yes, description: "The probability for wall cells to be removed from the bottom of the map. A value of 0 will produce a completely enclosed tunnel and 1 will make the entire bottom of the level completely open."), Editable()] + public float BottomHoleProbability { - get; - set; - } - - [Serialize(false, IsPropertySaveable.Yes, description: "If enabled, no walls generate in the level. Can be useful for e.g. levels that are just supposed to consist of a pre-built outpost."), Editable] - public bool NoLevelGeometry - { - get; - set; - } - - [Serialize(1000, IsPropertySaveable.Yes, description: "The total number of level objects (vegetation, vents, etc) in the level."), Editable(MinValueInt = 0, MaxValueInt = 100000)] - public int LevelObjectAmount - { - get; - set; - } - - [Serialize(80, IsPropertySaveable.Yes, description: "The total number of decorative background creatures."), Editable(MinValueInt = 0, MaxValueInt = 1000)] - public int BackgroundCreatureAmount - { - get; - set; + get { return bottomHoleProbability; } + set { bottomHoleProbability = MathHelper.Clamp(value, 0.0f, 1.0f); } } [Serialize(100000, IsPropertySaveable.Yes), Editable] @@ -232,31 +179,10 @@ namespace Barotrauma set { initialDepthMax = Math.Max(value, initialDepthMin); } } - [Serialize(6500, IsPropertySaveable.Yes, description: "Minimum width of the main tunnel going through the level, in pixels. Can be automatically increased by the level editor if the submarine is larger than this."), Editable(MinValueInt = 5000, MaxValueInt = 1000000)] - public int MinTunnelRadius - { - get; - set; - } + [Header("Level geometry")] - - [Serialize("0,1", IsPropertySaveable.Yes, description: "Amount of side tunnels in the level (min,max)."), Editable] - public Point SideTunnelCount - { - get; - set; - } - - - [Serialize(0.5f, IsPropertySaveable.Yes, description: "How much the side tunnels can \"zigzag\". 0 = completely straight tunnel, 1 = can go all the way from the top of the level to the bottom."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] - public float SideTunnelVariance - { - get; - set; - } - - [Serialize("2000,6000", IsPropertySaveable.Yes, description: "Minimum width of the side tunnels, in pixels. Unlike the main tunnel, does not get adjusted based on the size of the submarine."), Editable] - public Point MinSideTunnelRadius + [Serialize(false, IsPropertySaveable.Yes, description: "If enabled, no walls generate in the level. Can be useful for e.g. levels that are just supposed to consist of a pre-built outpost."), Editable] + public bool NoLevelGeometry { get; set; @@ -324,6 +250,37 @@ namespace Barotrauma } + [Header("Tunnels")] + [Serialize(6500, IsPropertySaveable.Yes, description: "Minimum width of the main tunnel going through the level, in pixels. Can be automatically increased by the level editor if the submarine is larger than this."), Editable(MinValueInt = 5000, MaxValueInt = 1000000)] + public int MinTunnelRadius + { + get; + set; + } + + + [Serialize("0,1", IsPropertySaveable.Yes, description: "Amount of side tunnels in the level (min,max)."), Editable] + public Point SideTunnelCount + { + get; + set; + } + + + [Serialize(0.5f, IsPropertySaveable.Yes, description: "How much the side tunnels can \"zigzag\". 0 = completely straight tunnel, 1 = can go all the way from the top of the level to the bottom."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] + public float SideTunnelVariance + { + get; + set; + } + + [Serialize("2000,6000", IsPropertySaveable.Yes, description: "Minimum width of the side tunnels, in pixels. Unlike the main tunnel, does not get adjusted based on the size of the submarine."), Editable] + public Point MinSideTunnelRadius + { + get; + set; + } + [Editable(VectorComponentLabels = new string[] { "editable.minvalue", "editable.maxvalue" }), Serialize("5000, 10000", IsPropertySaveable.Yes, description: "The distance between the nodes that are used to generate the main path through the level (min, max). Larger values produce a straighter path.")] public Point MainPathNodeIntervalRange @@ -343,6 +300,22 @@ namespace Barotrauma set; } + [Header("Contents")] + [Serialize(1000, IsPropertySaveable.Yes, description: "The total number of level objects (vegetation, vents, etc) in the level."), Editable(MinValueInt = 0, MaxValueInt = 100000)] + public int LevelObjectAmount + { + get; + set; + } + + [Serialize(80, IsPropertySaveable.Yes, description: "The total number of decorative background creatures."), Editable(MinValueInt = 0, MaxValueInt = 1000)] + public int BackgroundCreatureAmount + { + get; + set; + } + + [Editable, Serialize(5, IsPropertySaveable.Yes, description: "The number of caves placed along the main path.")] public int CaveCount { @@ -406,6 +379,14 @@ namespace Barotrauma set; } + [Header("Abyss")] + [Serialize(true, IsPropertySaveable.Yes, "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(5, IsPropertySaveable.Yes, description: "Number of abyss islands in the level."), Editable(MinValueInt = 0, MaxValueInt = 20)] public int AbyssIslandCount { @@ -448,6 +429,7 @@ namespace Barotrauma set; } + [Header("Sea floor")] [Serialize(-300000, IsPropertySaveable.Yes, description: "How far below the level the sea floor is placed."), Editable(MinValueFloat = Level.MaxEntityDepth, MaxValueFloat = 0.0f)] public int SeaFloorDepth { @@ -506,6 +488,7 @@ namespace Barotrauma public int GetMaxRuinCount() => UseRandomRuinCount() ? MaxRuinCount : RuinCount; + [Header("Ruins")] [Serialize(1, IsPropertySaveable.Yes, description: "The number of alien ruins in the level. Ignored, if both MinRuinCount and MaxRuinCount are defined."), Editable(MinValueInt = 0, MaxValueInt = 10)] public int RuinCount { get; set; } @@ -517,6 +500,7 @@ namespace Barotrauma // TODO: Move the wreck parameters under a separate class? #region Wreck parameters + [Header("Wrecks")] [Serialize(1, IsPropertySaveable.Yes, description: "The minimum number of wrecks in the level. Note that this value cannot be higher than the amount of wreck prefabs (subs)."), Editable(MinValueInt = 0, MaxValueInt = 10)] public int MinWreckCount { get; set; } @@ -545,13 +529,7 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes, description: "Should a beacon station always spawn in this type of level?")] public string ForceBeaconStation { get; set; } - [Serialize(0.4f, IsPropertySaveable.Yes, description: "The probability for wall cells to be removed from the bottom of the map. A value of 0 will produce a completely enclosed tunnel and 1 will make the entire bottom of the level completely open."), Editable()] - public float BottomHoleProbability - { - get { return bottomHoleProbability; } - set { bottomHoleProbability = MathHelper.Clamp(value, 0.0f, 1.0f); } - } - + [Header("Visuals")] [Serialize(1.0f, IsPropertySaveable.Yes, description: "Scale of the water particle texture."), Editable] public float WaterParticleScale { @@ -595,6 +573,58 @@ namespace Barotrauma set; } + [Serialize(120.0f, IsPropertySaveable.Yes, description: "How far the level walls' edge texture portrudes outside the actual, \"physical\" edge of the cell."), Editable(minValue: 0.0f, maxValue: 1000.0f)] + public float WallEdgeExpandOutwardsAmount + { + get; + private set; + } + + [Serialize(1000.0f, IsPropertySaveable.Yes, description: "How far inside the level walls the edge texture continues."), Editable(minValue: 0.0f, maxValue: 10000.0f)] + public float WallEdgeExpandInwardsAmount + { + get; + private set; + } + + [Header("Colors")] + [Serialize("27,30,36", IsPropertySaveable.Yes), Editable] + public Color AmbientLightColor + { + get; + set; + } + + [Serialize("20,40,50", IsPropertySaveable.Yes), Editable] + public Color BackgroundTextureColor + { + get; + set; + } + + [Serialize("20,40,50", IsPropertySaveable.Yes), Editable] + public Color BackgroundColor + { + get; + set; + } + + [Serialize("255,255,255", IsPropertySaveable.Yes), Editable] + public Color WallColor + { + get; + set; + } + + [Serialize("255,255,255", IsPropertySaveable.Yes), Editable] + public Color WaterParticleColor + { + get; + set; + } + + + [Header("Sounds")] [Serialize(false, IsPropertySaveable.Yes, description: "Should the \"ambient noise\" of the biome play in this level if it's an outpost level."), Editable] public bool PlayNoiseLoopInOutpostLevel { @@ -609,19 +639,6 @@ namespace Barotrauma set; } - [Serialize(120.0f, IsPropertySaveable.Yes, description: "How far the level walls' edge texture portrudes outside the actual, \"physical\" edge of the cell."), Editable(minValue: 0.0f, maxValue: 1000.0f)] - public float WallEdgeExpandOutwardsAmount - { - get; - private set; - } - - [Serialize(1000.0f, IsPropertySaveable.Yes, description: "How far inside the level walls the edge texture continues."), Editable(minValue: 0.0f, maxValue: 10000.0f)] - public float WallEdgeExpandInwardsAmount - { - get; - private set; - } public Sprite BackgroundSprite { get; private set; } public Sprite BackgroundTopSprite { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs index 98b36532c..e6412b79c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -103,7 +103,7 @@ namespace Barotrauma foreach (Structure structure in Structure.WallList) { - if (!structure.HasBody || structure.HiddenInGame) { continue; } + if (!structure.HasBody || structure.IsHidden) { continue; } LevelObjectPrefab.SpawnPosType spawnPosType = LevelObjectPrefab.SpawnPosType.None; if (level.Ruins.Any(r => r.Submarine == structure.Submarine)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index dd3df3d5d..25d860303 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -188,18 +188,7 @@ namespace Barotrauma public static PurchasedItem CreateInitialStockItem(ItemPrefab itemPrefab, PriceInfo priceInfo) { - int quantity = PriceInfo.DefaultAmount; - if (priceInfo.MaxAvailableAmount > 0) - { - quantity = - priceInfo.MaxAvailableAmount > priceInfo.MinAvailableAmount ? - Rand.Range(priceInfo.MinAvailableAmount, priceInfo.MaxAvailableAmount + 1) : - priceInfo.MaxAvailableAmount; - } - else if (priceInfo.MinAvailableAmount > 0) - { - quantity = priceInfo.MinAvailableAmount; - } + int quantity = Rand.Range(priceInfo.MinAvailableAmount, priceInfo.MaxAvailableAmount + 1); return new PurchasedItem(itemPrefab, quantity, buyer: null); } @@ -256,7 +245,7 @@ namespace Barotrauma if (stockItem.ItemPrefab.GetPriceInfo(this) is PriceInfo priceInfo) { if (!priceInfo.CanBeSpecial) { continue; } - var baseQuantity = priceInfo.MinAvailableAmount > 0 ? priceInfo.MinAvailableAmount : PriceInfo.DefaultAmount; + var baseQuantity = priceInfo.MinAvailableAmount; weight += (float)(stockItem.Quantity - baseQuantity) / baseQuantity; if (weight < 0.0f) { continue; } } @@ -906,8 +895,8 @@ namespace Barotrauma } MissionPrefab missionPrefab = random != null ? - ToolBox.SelectWeightedRandom(suitableMissions.OrderBy(m => m.Identifier), m => m.Commonness, random) : - ToolBox.SelectWeightedRandom(suitableMissions.OrderBy(m => m.Identifier), m => m.Commonness, Rand.RandSync.Unsynced); + ToolBox.SelectWeightedRandom(suitableMissions, m => m.Commonness, random) : + ToolBox.SelectWeightedRandom(suitableMissions, m => m.Commonness, Rand.RandSync.Unsynced); var mission = InstantiateMission(missionPrefab, out LocationConnection connection); //don't allow duplicate missions in the same connection @@ -1141,6 +1130,12 @@ namespace Barotrauma return HireManager.AvailableCharacters; } + public void ForceHireableCharacters(IEnumerable hireableCharacters) + { + HireManager ??= new HireManager(); + HireManager.AvailableCharacters = hireableCharacters.ToList(); + } + private void CreateRandomName(LocationType type, Random rand, IEnumerable existingLocations) { if (!type.ForceLocationName.IsEmpty) @@ -1402,12 +1397,12 @@ namespace Barotrauma existingStock.Quantity = Math.Min( existingStock.Quantity + 1, - priceInfo.MaxAvailableAmount > 0 ? priceInfo.MaxAvailableAmount : CargoManager.MaxQuantity); + priceInfo.MaxAvailableAmount); } } else if (existingStock != null) { - stockToRemove.Add(existingStock); + stockToRemove.Add(existingStock); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs index d67c0c29d..acedd1cf0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs @@ -18,7 +18,7 @@ namespace Barotrauma private readonly ImmutableArray portraits; // - private readonly ImmutableArray<(Identifier Name, float Commonness)> hireableJobs; + private readonly ImmutableArray<(Identifier Identifier, float Commonness, bool AlwaysAvailableIfMissingFromCrew)> hireableJobs; private readonly float totalHireableWeight; public readonly Dictionary CommonnessPerZone = new Dictionary(); @@ -226,7 +226,7 @@ namespace Barotrauma MinCountPerZone[zoneIndex] = minCount; } var portraits = new List(); - var hireableJobs = new List<(Identifier, float)>(); + var hireableJobs = new List<(Identifier, float, bool)>(); foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -234,8 +234,9 @@ namespace Barotrauma case "hireable": Identifier jobIdentifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty); float jobCommonness = subElement.GetAttributeFloat("commonness", 1.0f); + bool availableIfMissing = subElement.GetAttributeBool("AlwaysAvailableIfMissingFromCrew", false); totalHireableWeight += jobCommonness; - hireableJobs.Add((jobIdentifier, jobCommonness)); + hireableJobs.Add((jobIdentifier, jobCommonness, availableIfMissing)); break; case "symbol": Sprite = new Sprite(subElement, lazyLoad: true); @@ -270,16 +271,33 @@ namespace Barotrauma this.hireableJobs = hireableJobs.ToImmutableArray(); } + public IEnumerable GetHireablesMissingFromCrew() + { + if (GameMain.GameSession?.CrewManager != null) + { + var missingJobs = hireableJobs + .Where(j => j.AlwaysAvailableIfMissingFromCrew) + .Where(j => GameMain.GameSession.CrewManager.GetCharacterInfos().None(c => c.Job?.Prefab.Identifier == j.Identifier)); + if (missingJobs.Any()) + { + foreach (var missingJob in missingJobs) + { + if (JobPrefab.Prefabs.TryGet(missingJob.Identifier, out JobPrefab job)) + { + yield return job; + } + } + } + } + } + public JobPrefab GetRandomHireable() { - float randFloat = Rand.Range(0.0f, totalHireableWeight, Rand.RandSync.ServerAndClient); - - foreach ((Identifier jobIdentifier, float commonness) in hireableJobs) + Identifier selectedJobId = hireableJobs.GetRandomByWeight(j => j.Commonness, Rand.RandSync.ServerAndClient).Identifier; + if (JobPrefab.Prefabs.TryGet(selectedJobId, out JobPrefab job)) { - if (randFloat < commonness) { return JobPrefab.Prefabs[jobIdentifier]; } - randFloat -= commonness; + return job; } - return null; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index 5e2b376b2..56acaad1c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -261,8 +261,7 @@ namespace Barotrauma foreach (var endLocation in EndLocations) { - if (endLocation.Type?.ForceLocationName != null && - !endLocation.Type.ForceLocationName.IsEmpty) + if (endLocation.Type?.ForceLocationName is { IsEmpty: false }) { endLocation.ForceName(endLocation.Type.ForceLocationName); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index 47f5bd9ca..eab9aecb9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -196,6 +196,16 @@ namespace Barotrauma set; } + /// + /// Is the layer this entity is in currently hidden? If it is, the entity is not updated and should do nothing. + /// + public bool IsLayerHidden { get; set; } + + /// + /// Is the entity hidden due to being enabled or the layer the entity is in being hidden? + /// + public bool IsHidden => HiddenInGame || IsLayerHidden; + public override Vector2 Position { get diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/ExtraSubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/ExtraSubmarineInfo.cs index c7b6155c7..c744c6d7c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/ExtraSubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/ExtraSubmarineInfo.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; using System.Xml.Linq; namespace Barotrauma @@ -52,6 +54,9 @@ namespace Barotrauma [Serialize(true, IsPropertySaveable.Yes), Editable] public bool AllowDamagedWalls { get; set; } + [Serialize(true, IsPropertySaveable.Yes), Editable] + public bool AllowDamagedDevices { get; set; } + [Serialize(true, IsPropertySaveable.Yes), Editable] public bool AllowDisconnectedWires { get; set; } @@ -73,16 +78,52 @@ namespace Barotrauma class WreckInfo : ExtraSubmarineInfo { + // Unknown -> older submarines before this property was added + public enum HasThalamus { Unknown, Yes, No } + + [Serialize(HasThalamus.Unknown, IsPropertySaveable.Yes)] + public HasThalamus WreckContainsThalamus { get; private set; } + public WreckInfo(SubmarineInfo submarineInfo, XElement element) : base(submarineInfo, element) { Name = $"{nameof(WreckInfo)} ({submarineInfo.Name})"; + TryDetermineThalamusIfUnknown(element); } public WreckInfo(SubmarineInfo submarineInfo) : base(submarineInfo) { Name = $"{nameof(WreckInfo)} ({submarineInfo.Name})"; + TryDetermineThalamusIfUnknown(submarineInfo.SubmarineElement); } public WreckInfo(WreckInfo original) : base(original) { } + + // Attempts to determine if the wreck contains a thalamus item + private void TryDetermineThalamusIfUnknown(XElement element) + { + if (WreckContainsThalamus != HasThalamus.Unknown) { return; } + + if (element == null) + { + // nothing we can do, oh well + WreckContainsThalamus = HasThalamus.Unknown; + return; + } + + foreach (var subElement in element.Elements()) + { + if (!string.Equals(subElement.Name.ToString(), nameof(Item), StringComparison.InvariantCultureIgnoreCase)) { continue; } + + var tags = subElement.GetAttributeIdentifierImmutableHashSet(nameof(ItemPrefab.Tags), ImmutableHashSet.Empty); + + if (tags.Contains(Tags.Thalamus)) + { + WreckContainsThalamus = HasThalamus.Yes; + return; + } + } + + WreckContainsThalamus = HasThalamus.No; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs index 696eda83f..49b043214 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs @@ -1,4 +1,5 @@ -using System; +using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; using System.Xml.Linq; @@ -8,9 +9,15 @@ namespace Barotrauma { public int Price { get; } public bool CanBeBought { get; } - //minimum number of items available at a given store + + /// + /// Minimum number of items available at a given store + /// public int MinAvailableAmount { get; } - //maximum number of items available at a given store + + /// + /// Maximum number of items available at a given store. Defaults to 20% more than the minimum amount. + /// public int MaxAvailableAmount { get; } /// /// Can the item be a Daily Special or a Requested Good @@ -30,9 +37,14 @@ namespace Barotrauma public bool RequiresUnlock { get; } /// - /// Used when both and are set to 0. + /// Used when neither or are defined. /// - public const int DefaultAmount = 5; + private const int DefaultAmount = 5; + + /// + /// How much more the maximum stock is relative to the minimum stock if not defined. Stores will gradually stock up towards the maximum. + /// + private const float DefaultMaxAvailabilityRelativeToMin = 1.2f; private readonly Dictionary minReputation = new Dictionary(); @@ -52,12 +64,11 @@ namespace Barotrauma MinLevelDifficulty = element.GetAttributeInt("minleveldifficulty", 0); BuyingPriceMultiplier = element.GetAttributeFloat("buyingpricemultiplier", 1f); CanBeBought = true; - int minAmount = GetMinAmount(element); - MinAvailableAmount = Math.Min(minAmount, CargoManager.MaxQuantity); - int maxAmount = GetMaxAmount(element); - maxAmount = Math.Min(maxAmount, CargoManager.MaxQuantity); - MaxAvailableAmount = Math.Max(maxAmount, MinAvailableAmount); + MinAvailableAmount = Math.Min(GetMinAmount(element, defaultValue: DefaultAmount), CargoManager.MaxQuantity); + MaxAvailableAmount = MathHelper.Clamp(GetMaxAmount(element, defaultValue: (int)(MinAvailableAmount * DefaultMaxAvailabilityRelativeToMin)), MinAvailableAmount, CargoManager.MaxQuantity); RequiresUnlock = element.GetAttributeBool("requiresunlock", false); + + System.Diagnostics.Debug.Assert(MaxAvailableAmount >= MinAvailableAmount); } public PriceInfo(int price, bool canBeBought, @@ -67,14 +78,15 @@ namespace Barotrauma Price = price; CanBeBought = canBeBought; MinAvailableAmount = Math.Min(minAmount, CargoManager.MaxQuantity); + MaxAvailableAmount = Math.Max(Math.Min(maxAmount, CargoManager.MaxQuantity), minAmount); BuyingPriceMultiplier = buyingPriceMultiplier; - maxAmount = Math.Min(maxAmount, CargoManager.MaxQuantity); - MaxAvailableAmount = Math.Max(maxAmount, minAmount); MinLevelDifficulty = minLevelDifficulty; CanBeSpecial = canBeSpecial; DisplayNonEmpty = displayNonEmpty; StoreIdentifier = new Identifier(storeIdentifier); RequiresUnlock = requiresUnlock; + + System.Diagnostics.Debug.Assert(MaxAvailableAmount >= MinAvailableAmount); } private void LoadReputationRestrictions(XElement priceInfoElement) @@ -95,8 +107,8 @@ namespace Barotrauma var priceInfos = new List(); defaultPrice = null; int basePrice = element.GetAttributeInt("baseprice", 0); - int minAmount = GetMinAmount(element); - int maxAmount = GetMaxAmount(element); + int minAmount = GetMinAmount(element, defaultValue: DefaultAmount); + int maxAmount = GetMaxAmount(element, defaultValue: (int)(DefaultAmount * DefaultMaxAvailabilityRelativeToMin)); int minLevelDifficulty = element.GetAttributeInt("minleveldifficulty", 0); bool canBeSpecial = element.GetAttributeBool("canbespecial", true); float buyingPriceMultiplier = element.GetAttributeFloat("buyingpricemultiplier", 1f); @@ -143,11 +155,11 @@ namespace Barotrauma return priceInfos; } - private static int GetMinAmount(XElement element, int defaultValue = 0) => element != null ? + private static int GetMinAmount(XElement element, int defaultValue) => element != null ? element.GetAttributeInt("minamount", element.GetAttributeInt("minavailable", defaultValue)) : defaultValue; - private static int GetMaxAmount(XElement element, int defaultValue = 0) => element != null ? + private static int GetMaxAmount(XElement element, int defaultValue) => element != null ? element.GetAttributeInt("maxamount", element.GetAttributeInt("maxavailable", defaultValue)) : defaultValue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index 37e00e1b6..c3ce9a0c4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -928,7 +928,7 @@ namespace Barotrauma public bool SectionIsLeakingFromOutside(int sectionIndex) { if (sectionIndex < 0 || sectionIndex >= Sections.Length) { return false; } - return SectionIsLeaking(sectionIndex) && !Sections[sectionIndex].gap.IsRoomToRoom; + return SectionIsLeaking(sectionIndex) && Sections[sectionIndex].gap is { IsRoomToRoom: false }; } public int SectionLength(int sectionIndex) @@ -971,7 +971,7 @@ namespace Barotrauma float prevDamage = section.damage; if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { - SetDamage(sectionIndex, section.damage + damage, attacker); + SetDamage(sectionIndex, section.damage + damage, attacker, createWallDamageProjectiles: createWallDamageProjectiles); } #if CLIENT if (damage > 0 && emitParticles) @@ -1003,10 +1003,6 @@ namespace Barotrauma } } #endif - if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) - { - SetDamage(sectionIndex, section.damage + damage, attacker, createWallDamageProjectiles: createWallDamageProjectiles); - } } public int FindSectionIndex(Vector2 displayPos, bool world = false, bool clamp = false) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index be4caa5b5..f786a3325 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -7,6 +7,7 @@ using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; using System.Linq; using System.Xml.Linq; @@ -675,7 +676,7 @@ namespace Barotrauma if (item.GetComponent() != null) { return false; } if (item.body != null && !item.body.Enabled) { return true; } } - if (e.HiddenInGame) { return true; } + if (e.IsHidden) { return true; } return false; }); @@ -1101,6 +1102,11 @@ namespace Barotrauma continue; } item.FlipX(true); + + if (!item.Prefab.CanFlipX && item.Prefab.AllowRotatingInEditor) + { + item.Rotation = -item.Rotation; + } } } @@ -1119,6 +1125,15 @@ namespace Barotrauma } } + public static bool LayerExistsInAnySub(Identifier layer) + { + foreach (MapEntity me in MapEntity.MapEntityList) + { + if (me.Layer == layer) { return true; } + } + return false; + } + public bool LayerExists(Identifier layer) { foreach (MapEntity me in MapEntity.MapEntityList) @@ -1130,20 +1145,45 @@ namespace Barotrauma public void SetLayerEnabled(Identifier layer, bool enabled, bool sendNetworkEvent = false) { - foreach (MapEntity me in MapEntity.MapEntityList) + foreach (MapEntity entity in MapEntity.MapEntityList) { - if (string.IsNullOrEmpty(me.Layer) || me.Submarine != this || me.Layer != layer) { continue; } - me.HiddenInGame = !enabled; -#if CLIENT - //normally this is handled in LightComponent.OnMapLoaded, but this method is called after that - if (me.HiddenInGame && me is Item item) + if (string.IsNullOrEmpty(entity.Layer) || entity.Submarine != this || entity.Layer != layer) { continue; } + entity.IsLayerHidden = !enabled; + + if (entity is WayPoint wp) { - foreach (var lightComponent in item.GetComponents()) + if (enabled) { - lightComponent.Light.Enabled = false; + wp.SpawnType = wp.SpawnType.RemoveFlag(SpawnType.Disabled); + } + else + { + wp.SpawnType = wp.SpawnType.AddFlag(SpawnType.Disabled); } } + else if (entity is Item item) + { + foreach (var connectionPanel in item.GetComponents()) + { + foreach (var connection in connectionPanel.Connections) + { + foreach (var wire in connection.Wires) + { + wire.Item.IsLayerHidden = entity.IsLayerHidden; + } + } + } +#if CLIENT + if (entity.IsLayerHidden) + { + //normally this is handled in LightComponent.OnMapLoaded, but this method is called after that + foreach (var lightComponent in item.GetComponents()) + { + lightComponent.Light.Enabled = false; + } + } #endif + } } #if SERVER if (sendNetworkEvent) @@ -1413,7 +1453,8 @@ namespace Barotrauma { if (!connectedSubs.Contains(item.Submarine)) { continue; } if (!item.HasTag(Tags.CargoContainer)) { continue; } - if (item.NonInteractable || item.HiddenInGame) { continue; } + if (item.HasTag(Tags.DisallowCargo)) { continue; } + if (item.NonInteractable || item.IsHidden) { continue; } var itemContainer = item.GetComponent(); if (itemContainer == null) { continue; } int emptySlots = 0; @@ -1703,9 +1744,12 @@ namespace Barotrauma } } - foreach (Identifier layer in Info.LayersHiddenByDefault) + if (Screen.Selected is { IsEditor : false }) { - SetLayerEnabled(layer, enabled: false); + foreach (Identifier layer in Info.LayersHiddenByDefault) + { + SetLayerEnabled(layer, enabled: false); + } } GameMain.GameSession?.Campaign?.UpgradeManager?.OnUpgradesChanged.Register(upgradeEventIdentifier, _ => ResetCrushDepth()); @@ -1839,10 +1883,37 @@ namespace Barotrauma element.Add(new XAttribute("layerhiddenbydefault", string.Join(", ", Info.LayersHiddenByDefault))); } + if (Info.WreckInfo != null) + { + bool hasThalamus = false; + + var wreckAiEntities = WreckAIConfig.Prefabs.Select(p => p.Entity).ToImmutableHashSet(); + var prefabsOnSub = GetItems(true).Select(i => i.Prefab).Distinct().ToImmutableHashSet(); + + foreach (ItemPrefab prefab in prefabsOnSub) + { + foreach (Identifier entity in wreckAiEntities) + { + if (WreckAI.IsThalamus(prefab, entity)) + { + hasThalamus = true; + break; + } + } + if (hasThalamus) { break; } + } + + element.Add(new XAttribute(nameof(WreckInfo.WreckContainsThalamus), hasThalamus ? WreckInfo.HasThalamus.Yes : WreckInfo.HasThalamus.No)); + } + if (Info.Type == SubmarineType.OutpostModule) { Info.OutpostModuleInfo?.Save(element); } + if (Info.GetExtraSubmarineInfo is { } extraSubInfo) + { + extraSubInfo.Save(element); + } foreach (Item item in Item.ItemList) { @@ -2186,7 +2257,7 @@ namespace Barotrauma { if (potentialContainer.Removed) { continue; } if (potentialContainer.NonInteractable) { continue; } - if (potentialContainer.HiddenInGame) { continue; } + if (potentialContainer.IsHidden) { continue; } if (allowConnectedSubs) { if (!connectedSubs.Contains(potentialContainer.Submarine)) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index d15858f41..030941e51 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -600,6 +600,9 @@ namespace Barotrauma const float MaxWallDamage = 500.0f; const float MinCameraShake = 5f; const float MaxCameraShake = 50.0f; + //delay at the start of the round during which you take no depth damage + //(gives you a bit of time to react and return if you start the round in a level that's too deep) + const float MinRoundDuration = 60.0f; if (Submarine.RealWorldDepth < Level.Loaded.RealWorldCrushDepth + CosmeticEffectThreshold || Submarine.RealWorldDepth < Submarine.RealWorldCrushDepth + CosmeticEffectThreshold) { @@ -616,7 +619,7 @@ namespace Barotrauma } depthDamageTimer -= deltaTime; - if (depthDamageTimer <= 0.0f) + if (depthDamageTimer <= 0.0f && (GameMain.GameSession == null || GameMain.GameSession.RoundDuration > MinRoundDuration)) { foreach (Structure wall in Structure.WallList) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index 90ad144f5..ffc1f3f7c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -11,7 +11,7 @@ using Barotrauma.Extensions; namespace Barotrauma { [Flags] - public enum SpawnType { Path = 0, Human = 1, Enemy = 2, Cargo = 4, Corpse = 8, Submarine = 16, ExitPoint = 32 }; + public enum SpawnType { Path = 0, Human = 1, Enemy = 2, Cargo = 4, Corpse = 8, Submarine = 16, ExitPoint = 32, Disabled = 64 }; partial class WayPoint : MapEntity { @@ -932,6 +932,9 @@ namespace Barotrauma { return WayPointList.GetRandom(wp => (ignoreSubmarine || wp.Submarine == sub) && + //checking for the disabled flag is not strictly necessary because we check for equality of the spawn type, + //but lets do that anyway in case we change the handling of the spawn type at some point + !wp.spawnType.HasFlag(SpawnType.Disabled) && wp.spawnType == spawnType && (spawnPointTag.IsNullOrEmpty() || wp.Tags.Any(t => t == spawnPointTag)) && (assignedJob == null || (assignedJob != null && wp.AssignedJob == assignedJob)), diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs index 15968ee86..8cc97e37a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs @@ -60,6 +60,15 @@ namespace Barotrauma.Networking { get { + if (Type == ChatMessageType.Radio && Sender is Item) + { + if (translatedText.IsNullOrEmpty()) + { + translatedText = TextManager.Get(Text).Fallback(Text).Value; + } + + return translatedText; + } if (Type.HasFlag(ChatMessageType.Server) || Type.HasFlag(ChatMessageType.Error) || Type.HasFlag(ChatMessageType.ServerLog)) { if (translatedText.IsNullOrEmpty()) @@ -80,7 +89,8 @@ namespace Barotrauma.Networking public PlayerConnectionChangeType ChangeType; public string IconStyle; - public readonly Character Sender; + public Character SenderCharacter => Sender as Character; + public readonly Entity Sender; public readonly Client SenderClient; public readonly string SenderName; @@ -125,7 +135,7 @@ namespace Barotrauma.Networking public ChatMode ChatMode { get; set; } = ChatMode.None; - protected ChatMessage(string senderName, string text, ChatMessageType type, Character sender, Client client, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None, Color? textColor = null) + protected ChatMessage(string senderName, string text, ChatMessageType type, Entity sender, Client client, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None, Color? textColor = null) { Text = text; Type = type; @@ -139,7 +149,7 @@ namespace Barotrauma.Networking customTextColor = textColor; } - public static ChatMessage Create(string senderName, string text, ChatMessageType type, Character sender, Client client = null, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None, Color? textColor = null) + public static ChatMessage Create(string senderName, string text, ChatMessageType type, Entity sender, Client client = null, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None, Color? textColor = null) { return new ChatMessage(senderName, text, type, sender, client ?? GameMain.NetworkMember?.ConnectedClients?.Find(c => c.Character != null && c.Character == sender), changeType, textColor); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs index ba54f79c6..793e69267 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -33,11 +33,13 @@ namespace Barotrauma.Networking ERROR, //tell the server that an error occurred CREW, //hiring UI MEDICAL, //medical clinic - TRANSFER_MONEY, // wallet transfers - REWARD_DISTRIBUTION, // wallet reward distribution + TRANSFER_MONEY, // wallet transfers + REWARD_DISTRIBUTION, // wallet reward distribution + RESET_REWARD_DISTRIBUTION, CIRCUITBOX, READY_CHECK, - READY_TO_SPAWN + READY_TO_SPAWN, + TAKEOVERBOT } enum ClientNetSegment @@ -73,6 +75,7 @@ namespace Barotrauma.Networking FILE_TRANSFER, VOICE, + VOICE_AMPLITUDE_DEBUG, PING_REQUEST, //ping the client CLIENT_PINGS, //tell the client the pings of all other clients @@ -205,9 +208,9 @@ namespace Barotrauma.Networking public TimeSpan UpdateInterval => new TimeSpan(0, 0, 0, 0, MathHelper.Clamp(1000 / ServerSettings.TickRate, 1, 500)); - public void AddChatMessage(string message, ChatMessageType type, string senderName = "", Client senderClient = null, Character senderCharacter = null, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None, Color? textColor = null) + public void AddChatMessage(string message, ChatMessageType type, string senderName = "", Client senderClient = null, Entity senderEntity = null, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None, Color? textColor = null) { - AddChatMessage(ChatMessage.Create(senderName, message, type, senderCharacter, senderClient, changeType: changeType, textColor: textColor)); + AddChatMessage(ChatMessage.Create(senderName, message, type, senderEntity, senderClient, changeType: changeType, textColor: textColor)); } public abstract void AddChatMessage(ChatMessage message); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs index f7c5235c6..1b6c15a17 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs @@ -39,15 +39,24 @@ namespace Barotrauma.Networking } - public OrderChatMessage(Order order, string text, Character targetCharacter, Character sender, bool isNewOrder = true) - : base(sender?.Name, text, ChatMessageType.Order, sender, GameMain.NetworkMember.ConnectedClients.Find(c => c.Character == sender)) + public OrderChatMessage(Order order, string text, Character targetCharacter, Entity sender, bool isNewOrder = true) + : base(NameFromEntityOrNull(sender), text, ChatMessageType.Order, sender, GameMain.NetworkMember.ConnectedClients.Find(c => c.Character == sender)) { Order = order; TargetCharacter = targetCharacter; IsNewOrder = isNewOrder; } - public static void WriteOrder(IWriteMessage msg, Order order, Character targetCharacter, bool isNewOrder) + public static string NameFromEntityOrNull(Entity entity) + => entity switch + { + null => null, + Character character => character.Name, + Item it => it.Name, + _ => throw new ArgumentException("Entity is not a character or item", nameof(entity)) + }; + + public static void WriteOrder(IWriteMessage msg, Order order, Entity targetCharacter, bool isNewOrder) { msg.WriteIdentifier(order.Prefab.Identifier); msg.WriteUInt16(targetCharacter == null ? (UInt16)0 : targetCharacter.ID); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs index cd9f1691f..fc84926dd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs @@ -23,6 +23,16 @@ namespace Barotrauma.Networking /// public static float SkillLossPercentageOnImmediateRespawn => GameMain.NetworkMember?.ServerSettings?.SkillLossPercentageOnImmediateRespawn ?? 10.0f; + public static RespawnMode RespawnMode => GameMain.NetworkMember?.ServerSettings?.RespawnMode ?? RespawnMode.MidRound; + + public static bool UseDeathPrompt + { + get + { + return GameMain.GameSession?.GameMode is CampaignMode && Level.Loaded != null; + } + } + public enum State { Waiting, @@ -71,14 +81,6 @@ namespace Barotrauma.Networking public State CurrentState { get; private set; } - public static bool UseRespawnPrompt - { - get - { - return GameMain.GameSession?.GameMode is CampaignMode && Level.Loaded != null && Level.Loaded?.Type != LevelData.LevelType.Outpost; - } - } - private float maxTransportTime; private float updateReturnTimer; @@ -94,7 +96,7 @@ namespace Barotrauma.Networking { this.networkMember = networkMember; - if (shuttleInfo != null) + if (shuttleInfo != null && networkMember.ServerSettings is not { RespawnMode: RespawnMode.Permadeath }) { RespawnShuttle = new Submarine(shuttleInfo, true); RespawnShuttle.PhysicsBody.FarseerBody.OnCollision += OnShuttleCollision; @@ -345,12 +347,39 @@ namespace Barotrauma.Networking } } + public static float GetReducedSkill(CharacterInfo characterInfo, Skill skill, float skillLossPercentage, float? currentSkillLevel = null) + { + var skillPrefab = characterInfo.Job.Prefab.Skills.Find(s => skill.Identifier == s.Identifier); + float currentLevel = currentSkillLevel ?? skill.Level; + if (skillPrefab == null || currentLevel < skillPrefab.LevelRange.End) { return currentLevel; } + return MathHelper.Lerp(currentLevel, skillPrefab.LevelRange.End, skillLossPercentage / 100.0f); + } + partial void RespawnCharactersProjSpecific(Vector2? shuttlePos); public void RespawnCharacters(Vector2? shuttlePos) { RespawnCharactersProjSpecific(shuttlePos); } + public static AfflictionPrefab GetRespawnPenaltyAfflictionPrefab() + { + return AfflictionPrefab.Prefabs.First(a => a.AfflictionType == "respawnpenalty"); + } + + public static Affliction GetRespawnPenaltyAffliction() + { + return GetRespawnPenaltyAfflictionPrefab()?.Instantiate(10.0f); + } + + public static void GiveRespawnPenaltyAffliction(Character character) + { + var respawnPenaltyAffliction = GetRespawnPenaltyAffliction(); + if (respawnPenaltyAffliction != null) + { + character.CharacterHealth.ApplyAffliction(targetLimb: null, respawnPenaltyAffliction); + } + } + public Vector2 FindSpawnPos() { if (Level.Loaded == null || Submarine.MainSub == null) { return Vector2.Zero; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index f97f4dfff..d6e9cc668 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -30,6 +30,14 @@ namespace Barotrauma.Networking SomethingDifferent = 4 } + public enum RespawnMode + { + MidRound, + BetweenRounds, + Permadeath, + } + + internal enum LootedMoneyDestination { Bank, @@ -404,6 +412,13 @@ namespace Barotrauma.Networking set { tickRate = MathHelper.Clamp(value, 1, 60); } } + [Serialize(true, IsPropertySaveable.Yes, description: "Do clients need to be authenticated (e.g. based on Steam ID or an EGS ownership token). Can be disabled if you for example want to play the game in a local network without a connection to external services.")] + public bool RequireAuthentication + { + get; + set; + } + [Serialize(true, IsPropertySaveable.Yes)] public bool RandomizeSeed { @@ -461,6 +476,27 @@ namespace Barotrauma.Networking private set; } + [Serialize(true, IsPropertySaveable.Yes)] + /// + /// Are players allowed to take over bots when permadeath is enabled? + /// + public bool AllowBotTakeoverOnPermadeath + { + get; + private set; + } + + [Serialize(false, IsPropertySaveable.Yes)] + /// + /// This is an optional setting for permadeath mode. When it's enabled, a player client whose character dies cannot + /// respawn or get a new character in any way in that game (unlike in normal permadeath mode), and can only spectate. + /// + public bool IronmanMode + { + get; + private set; + } + [Serialize(60.0f, IsPropertySaveable.Yes)] public float AutoRestartInterval { @@ -603,15 +639,15 @@ namespace Barotrauma.Networking get; set; } - private bool allowRespawn; - [Serialize(true, IsPropertySaveable.Yes)] - public bool AllowRespawn + private RespawnMode respawnMode; + [Serialize(RespawnMode.MidRound, IsPropertySaveable.Yes)] + public RespawnMode RespawnMode { - get { return allowRespawn; } + get { return respawnMode; } set { - if (allowRespawn == value) { return; } - allowRespawn = value; + if (respawnMode == value) { return; } + respawnMode = value; ServerDetailsChanged = true; } } @@ -692,6 +728,13 @@ namespace Barotrauma.Networking get; set; } + + [Serialize(true, IsPropertySaveable.Yes)] + public bool AllowDragAndDropGive + { + get; + set; + } [Serialize(false, IsPropertySaveable.Yes)] public bool DestructibleOutposts @@ -859,13 +902,28 @@ namespace Barotrauma.Networking get; private set; } - + + /// + /// The number of seconds a disconnected player's Character remains in the world until despawned (via "braindeath"). + /// [Serialize(300.0f, IsPropertySaveable.Yes)] public float KillDisconnectedTime { get; private set; } + + /// + /// The number of seconds a disconnected player's Character remains in the world until despawned, in permadeath mode. + /// The Character is helpless and vulnerable, this should be short enough to avoid unintended permadeath, but + /// also long enough to discourage disconnecting just to avoid a potential incoming permadeath. + /// + [Serialize(10.0f, IsPropertySaveable.Yes)] + public float DespawnDisconnectedPermadeathTime + { + get; + private set; + } [Serialize(600.0f, IsPropertySaveable.Yes)] public float KickAFKTime @@ -962,6 +1020,9 @@ namespace Barotrauma.Networking [Serialize(999999, IsPropertySaveable.Yes)] public int MaximumMoneyTransferRequest { get; set; } + [Serialize(0f, IsPropertySaveable.Yes)] + public float NewCampaignDefaultSalary { get; set; } + public CampaignSettings CampaignSettings { get; set; } = CampaignSettings.Empty; private bool allowSubVoting; @@ -1186,7 +1247,7 @@ namespace Barotrauma.Networking set("subselectionmode", SubSelectionMode); set("voicechatenabled", VoiceChatEnabled); set("allowspectating", AllowSpectating); - set("allowrespawn", AllowRespawn); + set("allowrespawn", RespawnMode is RespawnMode.MidRound or RespawnMode.BetweenRounds); set("traitors", TraitorProbability.ToString(CultureInfo.InvariantCulture)); set("friendlyfireenabled", AllowFriendlyFire); set("karmaenabled", KarmaEnabled); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voip/VoipConfig.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voip/VoipConfig.cs index ab92eb553..9390803f3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voip/VoipConfig.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voip/VoipConfig.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text; +using Concentus.Structs; namespace Barotrauma.Networking { @@ -9,5 +10,14 @@ namespace Barotrauma.Networking 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,20); + + public const int FREQUENCY = 48000; //48Khz + public const int BITRATE = 16000; //16Kbps + public const int BUFFER_SIZE = (8 * MAX_COMPRESSED_SIZE * FREQUENCY) / BITRATE; //20ms window + + public static OpusDecoder CreateDecoder() + { + return new OpusDecoder(FREQUENCY, 1); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/Physics.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/Physics.cs index d04088c0c..c94691e3a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/Physics.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/Physics.cs @@ -16,6 +16,8 @@ namespace Barotrauma public const Category CollisionLevel = Category.Cat8; public const Category CollisionRepairableWall = Category.Cat9; + public const Category DefaultItemCollidesWith = CollisionWall | CollisionLevel | CollisionPlatform | CollisionRepairableWall; + public static float DisplayToRealWorldRatio = 1.0f / 100.0f; public const float DisplayToSimRation = 100.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/Editable/ConditionallyEditable.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/Editable/ConditionallyEditable.cs index 4ac5dbc79..ba2feb90a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/Editable/ConditionallyEditable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/Editable/ConditionallyEditable.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using Barotrauma.Items.Components; namespace Barotrauma; @@ -28,7 +29,8 @@ sealed class ConditionallyEditable : Editable OnlyByStatusEffectsAndNetwork, HasIntegratedButtons, IsToggleableController, - HasConnectionPanel + HasConnectionPanel, + DeteriorateUnderStress } public bool IsEditable(ISerializableEntity entity) @@ -55,10 +57,12 @@ sealed class ConditionallyEditable : Editable ConditionType.HasIntegratedButtons => GetComponent(entity) is { HasIntegratedButtons: true }, ConditionType.IsToggleableController - => GetComponent(entity) is Controller { IsToggle: true } controller && + => GetComponent(entity) is Controller { IsToggle: true } controller && controller.Item.GetComponent() != null, ConditionType.HasConnectionPanel => GetComponent(entity) != null, + ConditionType.DeteriorateUnderStress + => entity is Item repairableItem && repairableItem.Components.Any(c => c is IDeteriorateUnderStress), _ => false }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/Editable/Editable.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/Editable/Editable.cs index 5786df7b1..231fa94da 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/Editable/Editable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/Editable/Editable.cs @@ -1,5 +1,4 @@ using System; -using Barotrauma.Items.Components; namespace Barotrauma; @@ -11,8 +10,14 @@ class Editable : Attribute public int MinValueInt = int.MinValue, MaxValueInt = int.MaxValue; public float MinValueFloat = float.MinValue, MaxValueFloat = float.MaxValue; - public bool ForceShowPlusMinusButtons = false; + public bool ForceShowPlusMinusButtons; public float ValueStep; + + /// + /// Should the value customized in the editor be applied to the new item swapped in place of this item. + /// Used e.g. for transferring the auto operate properties from one turret to another installed on place of it. + /// + public bool TransferToSwappedItem; /// /// Labels of the components of a vector property (defaults to x,y,z,w) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty/SerializableProperty.cs index d32bd3fa8..760da254a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty/SerializableProperty.cs @@ -9,9 +9,6 @@ using System.Globalization; using System.Linq; using System.Reflection; using System.Xml.Linq; -using Barotrauma.Networking; - -//TODO: come back to this later, clever use of reflection would make this nicer >:) namespace Barotrauma { @@ -45,14 +42,25 @@ namespace Barotrauma /// Setting the value to a non-empty string will let the user select the text from one whose tag starts with the given string (e.g. RoomName. would show all texts with a RoomName.* tag) public Serialize(object defaultValue, IsPropertySaveable isSaveable, string description = "", string translationTextTag = "", bool alwaysUseInstanceValues = false) { - this.DefaultValue = defaultValue; - this.IsSaveable = isSaveable; - this.TranslationTextTag = translationTextTag.ToIdentifier(); + DefaultValue = defaultValue; + IsSaveable = isSaveable; + TranslationTextTag = translationTextTag.ToIdentifier(); Description = description; AlwaysUseInstanceValues = alwaysUseInstanceValues; } } + [AttributeUsage(AttributeTargets.Property)] + public sealed class Header : Attribute + { + public readonly LocalizedString Text; + + public Header(string text = "", string localizedTextTag = null) + { + Text = localizedTextTag != null ? TextManager.Get(localizedTextTag) : text; + } + } + public sealed class SerializableProperty { private static readonly ImmutableDictionary supportedTypes = new Dictionary @@ -754,6 +762,9 @@ namespace Barotrauma case nameof(Character.PropulsionSpeedMultiplier): { if (parentObject is Character character) { character.PropulsionSpeedMultiplier = value; return true; } } break; + case nameof(Character.ObstructVisionAmount): + { if (parentObject is Character character) { character.ObstructVisionAmount = value; return true; } } + break; case nameof(Item.Scale): { if (parentObject is Item item) { item.Scale = value; return true; } } break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs b/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs index f954f79c4..33f8a12df 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs @@ -15,7 +15,7 @@ namespace Barotrauma public DeformableSprite DeformableSprite { get; private set; } public Sprite ActiveSprite => Sprite ?? DeformableSprite.Sprite; - public ConditionalSprite(ContentXElement element, ISerializableEntity target, string file = "", bool lazyLoad = false) + public ConditionalSprite(ContentXElement element, ISerializableEntity target, string file = "", bool lazyLoad = false, float sourceRectScale = 1) { Target = target; Exclusive = element.GetAttributeBool("exclusive", Exclusive); @@ -28,10 +28,10 @@ namespace Barotrauma conditionals.AddRange(PropertyConditional.FromXElement(subElement)); break; case "sprite": - Sprite = new Sprite(subElement, file: file, lazyLoad: lazyLoad); + Sprite = new Sprite(subElement, file: file, lazyLoad: lazyLoad, sourceRectScale: sourceRectScale); break; case "deformablesprite": - DeformableSprite = new DeformableSprite(subElement, filePath: file, lazyLoad: lazyLoad); + DeformableSprite = new DeformableSprite(subElement, filePath: file, lazyLoad: lazyLoad, sourceRectScale: sourceRectScale); break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Sprite/DeformableSprite.cs b/Barotrauma/BarotraumaShared/SharedSource/Sprite/DeformableSprite.cs index 502cfa9e6..618ca17de 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Sprite/DeformableSprite.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Sprite/DeformableSprite.cs @@ -18,9 +18,9 @@ namespace Barotrauma public Sprite Sprite { get; private set; } - public DeformableSprite(ContentXElement element, int? subdivisionsX = null, int? subdivisionsY = null, string filePath = "", bool lazyLoad = false, bool invert = false) + public DeformableSprite(ContentXElement element, int? subdivisionsX = null, int? subdivisionsY = null, string filePath = "", bool lazyLoad = false, bool invert = false, float sourceRectScale = 1) { - Sprite = new Sprite(element, file: filePath, lazyLoad: lazyLoad); + Sprite = new Sprite(element, file: filePath, lazyLoad: lazyLoad, sourceRectScale: sourceRectScale); InitProjSpecific(element, subdivisionsX, subdivisionsY, lazyLoad, invert); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs index 8d3d19ab4..6231f5abc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs @@ -107,7 +107,7 @@ namespace Barotrauma static partial void AddToList(Sprite sprite); - public Sprite(ContentXElement element, string path = "", string file = "", bool lazyLoad = false) + public Sprite(ContentXElement element, string path = "", string file = "", bool lazyLoad = false, float sourceRectScale = 1) { if (element is null) { @@ -137,7 +137,7 @@ namespace Barotrauma LoadTexture(ref sourceVector, ref shouldReturn); } if (shouldReturn) { return; } - sourceRect = new Rectangle((int)sourceVector.X, (int)sourceVector.Y, (int)sourceVector.Z, (int)sourceVector.W); + sourceRect = new Rectangle((int)(sourceVector.X * sourceRectScale), (int)(sourceVector.Y * sourceRectScale), (int)(sourceVector.Z * sourceRectScale), (int)(sourceVector.W * sourceRectScale)); size = SourceElement.GetAttributeVector2("size", Vector2.One); RelativeSize = size; size.X *= sourceRect.Width; diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 3d27874be..e7d287f79 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -3,6 +3,7 @@ using Barotrauma.Extensions; using Barotrauma.Items.Components; using Barotrauma.Networking; using FarseerPhysics; +using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -2062,14 +2063,18 @@ namespace Barotrauma { foreach (Affliction affliction in character.CharacterHealth.GetAllAfflictions()) { - if (!characterSpawnInfo.TransferAfflictions && characterSpawnInfo.TransferBuffs && affliction.Prefab.IsBuff) + if (affliction.Prefab.IsBuff) { - newCharacter.CharacterHealth.ApplyAffliction(newCharacter.AnimController.MainLimb, affliction.Prefab.Instantiate(affliction.Strength)); + if (!characterSpawnInfo.TransferBuffs) { continue; } } - if (characterSpawnInfo.TransferAfflictions) + else { - newCharacter.CharacterHealth.ApplyAffliction(newCharacter.AnimController.MainLimb, affliction.Prefab.Instantiate(affliction.Strength)); + if (!characterSpawnInfo.TransferAfflictions) { continue; } } + //ApplyAffliction modified the strength based on max vitality, let's undo that before transferring the affliction + //(otherwise e.g. a character with 1000 vitality would only get a tenth of the strength) + float afflictionStrength = affliction.Strength * (newCharacter.MaxVitality / 100.0f); + newCharacter.CharacterHealth.ApplyAffliction(newCharacter.AnimController.MainLimb, affliction.Prefab.Instantiate(afflictionStrength)); } } if (i == characterSpawnInfo.Count) // Only perform the below actions if this is the last character being spawned. @@ -2293,8 +2298,13 @@ namespace Barotrauma { var sourceEntity = (sourceBody?.UserData as ISpatialEntity) ?? entity; Vector2 spawnPos = sourceEntity.SimPosition; + List ignoredBodies = null; + if (!projectile.DamageUser) + { + ignoredBodies = user?.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList(); + } projectile.Shoot(user, spawnPos, spawnPos, rotation, - ignoredBodies: user?.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList(), createNetworkEvent: true); + ignoredBodies: ignoredBodies, createNetworkEvent: true); projectile.Item.Submarine = projectile.LaunchSub = sourceEntity?.Submarine; } else if (newItem.body != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Tags.cs b/Barotrauma/BarotraumaShared/SharedSource/Tags.cs index 2af009b39..f0e84dab5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Tags.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Tags.cs @@ -1,4 +1,4 @@ -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; namespace Barotrauma; @@ -63,6 +63,7 @@ public static class Tags public static readonly Identifier Crate = "crate".ToIdentifier(); public static readonly Identifier DontSellItems = "dontsellitems".ToIdentifier(); public static readonly Identifier CargoContainer = "cargocontainer".ToIdentifier(); + public static readonly Identifier DisallowCargo = "disallowcargo".ToIdentifier(); public static readonly Identifier CargoMissionItem = "cargomission".ToIdentifier(); @@ -101,6 +102,8 @@ public static class Tags public static readonly Identifier TraitorMissionItem = "traitormissionitem".ToIdentifier(); public static readonly Identifier TraitorGuidelinesForSecurity = "traitorguidelinesforsecurity".ToIdentifier(); + public static readonly Identifier ProvocativeToHumanAI = "provocativetohumanai".ToIdentifier(); + /// /// Container where the initial gear (diving suit, oxygen tank, etc) of respawning players is placed /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/FallbackLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/FallbackLString.cs index d16faf612..ce728cbdd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/FallbackLString.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/FallbackLString.cs @@ -8,8 +8,12 @@ namespace Barotrauma public bool PrimaryIsLoaded { get; private set; } - public FallbackLString(LocalizedString primary, LocalizedString fallback) + private readonly bool useDefaultLanguageIfFound; + + /// If the text is available in the default language (English), should it be used instead of this fallback? + public FallbackLString(LocalizedString primary, LocalizedString fallback, bool useDefaultLanguageIfFound = true) { + this.useDefaultLanguageIfFound = useDefaultLanguageIfFound; if (primary is FallbackLString { primary: { } innerPrimary, fallback: { } innerFallback }) { this.primary = innerPrimary; @@ -35,7 +39,11 @@ namespace Barotrauma { cachedValue = primary.Value; PrimaryIsLoaded = primary.Loaded; - if (!primary.Loaded) + + bool defaultLanguageFallbackAvailable = primary is TagLString { UsingDefaultLanguageAsFallback: true }; + + if (!primary.Loaded && + (!defaultLanguageFallbackAvailable || !useDefaultLanguageIfFound)) { cachedValue = fallback.Value; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/LocalizedString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/LocalizedString.cs index a34f3dff8..870fd8285 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/LocalizedString.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/LocalizedString.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System; using System.Collections.Generic; @@ -108,9 +108,14 @@ namespace Barotrauma return new JoinLString(separator, subStrs); } - public LocalizedString Fallback(LocalizedString fallback) + /// + /// Use this text instead if the original text cannot be found. + /// + /// The text to use as a fallback + /// Should the default language (English) text be used instead of this fallback if there is a text available in the default language? + public LocalizedString Fallback(LocalizedString fallback, bool useDefaultLanguageIfFound = true) { - return new FallbackLString(this, fallback); + return new FallbackLString(this, fallback, useDefaultLanguageIfFound); } public IReadOnlyList Split(params char[] separators) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TagLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TagLString.cs index 2d978ed4e..f8ecc7c55 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TagLString.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TagLString.cs @@ -11,6 +11,11 @@ namespace Barotrauma { private readonly ImmutableArray tags; + /// + /// Did we end up using the text in the default language (English) due to the text not being found in the selected language? + /// + public bool UsingDefaultLanguageAsFallback { get; private set; } + public TagLString(params Identifier[] tags) { this.tags = tags.ToImmutableArray(); @@ -31,6 +36,8 @@ namespace Barotrauma { UpdateLanguage(); + UsingDefaultLanguageAsFallback = false; + (string value, bool loaded) tryLoad(LanguageIdentifier lang) { IReadOnlyList candidates = Array.Empty(); @@ -69,8 +76,9 @@ namespace Barotrauma cachedValue = value; if (!loaded && Language != TextManager.DefaultLanguage) { - (value, _) = tryLoad(TextManager.DefaultLanguage); + (value, bool fallbackLoaded) = tryLoad(TextManager.DefaultLanguage); cachedValue = value; + UsingDefaultLanguageAsFallback = fallbackLoaded; //Notice how we don't set loadedSuccessfully again here. //This is by design; falling back to English means that //this text did NOT load successfully, so Loaded must diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs index 61be54ef6..d78b15131 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs @@ -60,7 +60,9 @@ namespace Barotrauma = new[] { (SpeciallyHandledCharCategory.CJK, UnicodeToIntRanges( + UnicodeRanges.HalfwidthandFullwidthForms, UnicodeRanges.HangulJamo, + UnicodeRanges.HangulCompatibilityJamo, UnicodeRanges.CjkRadicalsSupplement, UnicodeRanges.CjkSymbolsandPunctuation, UnicodeRanges.EnclosedCjkLettersandMonths, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/NetCollection.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/NetStructs/NetCollection.cs similarity index 100% rename from Barotrauma/BarotraumaShared/SharedSource/Utils/NetCollection.cs rename to Barotrauma/BarotraumaShared/SharedSource/Utils/NetStructs/NetCollection.cs diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/NetStructs/NetDictionary.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/NetStructs/NetDictionary.cs new file mode 100644 index 000000000..f28e52f30 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/NetStructs/NetDictionary.cs @@ -0,0 +1,42 @@ +#nullable enable +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Barotrauma +{ + /// + /// A serializable dictionary that can be sent over the network. + /// + /// The backing array of key-value pairs that gets serialized. + /// Key + /// Value + /// + /// This isn't a full implementation of a dictionary, but rather a simple wrapper around a list of key-value pairs + /// that can be serialized and deserialized in an INetSerializableStruct. + /// Normally there wouldn't be duplicate keys in a dictionary, but this implementation doesn't enforce that. + /// + [NetworkSerialize] + public readonly record struct NetDictionary(ImmutableArray> Pairs) : INetSerializableStruct where T : notnull + { + public Dictionary ToDictionary() + => Pairs.ToDictionary( + static pair => pair.First, + static pair => pair.Second); + + public ImmutableDictionary ToImmutableDictionary() + => Pairs.ToImmutableDictionary( + static pair => pair.First, + static pair => pair.Second); + } + + public static class DictionaryExtensions + { + public static NetDictionary ToNetDictionary(this Dictionary source) where T : notnull + => new NetDictionary(source.Select(static pair => new NetPair(pair.Key, pair.Value)).ToImmutableArray()); + + public static NetDictionary ToNetDictionary(this ImmutableDictionary source) where T : notnull + => new NetDictionary(source.Select(static pair => new NetPair(pair.Key, pair.Value)).ToImmutableArray()); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/NetLimitedString.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/NetStructs/NetLimitedString.cs similarity index 100% rename from Barotrauma/BarotraumaShared/SharedSource/Utils/NetLimitedString.cs rename to Barotrauma/BarotraumaShared/SharedSource/Utils/NetStructs/NetLimitedString.cs diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/NetStructs/NetPair.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/NetStructs/NetPair.cs new file mode 100644 index 000000000..0c473247f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/NetStructs/NetPair.cs @@ -0,0 +1,6 @@ +#nullable enable +namespace Barotrauma +{ + [NetworkSerialize] + public readonly record struct NetPair(T First, U Second) : INetSerializableStruct; +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs index af4033b2b..5b4b98bed 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs @@ -250,42 +250,92 @@ namespace Barotrauma.IO { public static string GetCurrentDirectory() { + // Intentionally crash with all exceptions, if this fails. return System.IO.Directory.GetCurrentDirectory(); } public static void SetCurrentDirectory(string path) - { + { + // Intentionally crash with all exceptions, if this fails. System.IO.Directory.SetCurrentDirectory(path); } public static string[] GetFiles(string path) { - return System.IO.Directory.GetFiles(path); + try + { + return System.IO.Directory.GetFiles(path); + } + catch (UnauthorizedAccessException e) + { + DebugConsole.ThrowError($"Cannot get files at \"{path}\": unauthorized access. The folder/file(s) might be read-only!", e); + return Array.Empty(); + } } public static string[] GetFiles(string path, string pattern, System.IO.SearchOption option = System.IO.SearchOption.AllDirectories) { - return System.IO.Directory.GetFiles(path, pattern, option); + try + { + return System.IO.Directory.GetFiles(path, pattern, option); + } + catch (UnauthorizedAccessException e) + { + DebugConsole.ThrowError($"Cannot get files at \"{path}\": unauthorized access. The folder/file(s) might be read-only!", e); + return Array.Empty(); + } } public static string[] GetDirectories(string path, string searchPattern = "*", System.IO.SearchOption searchOption = System.IO.SearchOption.TopDirectoryOnly) { - return System.IO.Directory.GetDirectories(path, searchPattern, searchOption); + try + { + return System.IO.Directory.GetDirectories(path, searchPattern, searchOption); + } + catch (UnauthorizedAccessException e) + { + DebugConsole.ThrowError($"Cannot get directories at \"{path}\": unauthorized access. The folder(s) might be read-only!", e); + return Array.Empty(); + } } public static string[] GetFileSystemEntries(string path) { - return System.IO.Directory.GetFileSystemEntries(path); + try + { + return System.IO.Directory.GetFileSystemEntries(path); + } + catch (UnauthorizedAccessException e) + { + DebugConsole.ThrowError($"Cannot get file system entries at \"{path}\": unauthorized access. The file/folder might be read-only!", e); + return Array.Empty(); + } } public static IEnumerable EnumerateDirectories(string path, string pattern) { - return System.IO.Directory.EnumerateDirectories(path, pattern); + try + { + return System.IO.Directory.EnumerateDirectories(path, pattern); + } + catch (UnauthorizedAccessException e) + { + DebugConsole.ThrowError($"Cannot enumerate directories at \"{path}\": unauthorized access. The folder(s) might be read-only!", e); + return Array.Empty(); + } } public static IEnumerable EnumerateFiles(string path, string pattern) { - return System.IO.Directory.EnumerateFiles(path, pattern); + try + { + return System.IO.Directory.EnumerateFiles(path, pattern); + } + catch (UnauthorizedAccessException e) + { + DebugConsole.ThrowError($"Cannot enumerate files at \"{path}\": unauthorized access. The file(s)/folder(s) might be read-only!", e); + return Array.Empty(); + } } public static bool Exists(string path) @@ -301,7 +351,15 @@ namespace Barotrauma.IO Validation.CanWrite(path, true); return null; } - return System.IO.Directory.CreateDirectory(path); + try + { + return System.IO.Directory.CreateDirectory(path); + } + catch (UnauthorizedAccessException e) + { + DebugConsole.ThrowError($"Cannot create directory at \"{path}\": unauthorized access. The file/folder might be read-only!", e); + return null; + } } public static void Delete(string path, bool recursive=true) @@ -312,14 +370,21 @@ namespace Barotrauma.IO return; } //TODO: validate recursion? - System.IO.Directory.Delete(path, recursive); + try + { + System.IO.Directory.Delete(path, recursive); + } + catch (UnauthorizedAccessException e) + { + DebugConsole.ThrowError($"Cannot delete \"{path}\": unauthorized access. The file/folder might be read-only!", e); + } } public static bool TryDelete(string path, bool recursive = true) { try { - Directory.Delete(path, recursive); + Delete(path, recursive); return true; } catch @@ -330,7 +395,15 @@ namespace Barotrauma.IO public static DateTime GetLastWriteTime(string path) { - return System.IO.Directory.GetLastWriteTime(path); + try + { + return System.IO.Directory.GetLastWriteTime(path); + } + catch (UnauthorizedAccessException e) + { + DebugConsole.ThrowError($"Cannot get last write time at \"{path}\": unauthorized access. The file/folder might be read-only!", e); + return new DateTime(); + } } } @@ -340,14 +413,21 @@ namespace Barotrauma.IO public static bool Exists(string path) => System.IO.File.Exists(path); - public static void Copy(string src, string dest, bool overwrite=false) + public static void Copy(string src, string dest, bool overwrite = false) { if (!Validation.CanWrite(dest, false)) { DebugConsole.ThrowError($"Cannot copy \"{src}\" to \"{dest}\": modifying the contents of this folder/using this extension is not allowed."); return; } - System.IO.File.Copy(src, dest, overwrite); + try + { + System.IO.File.Copy(src, dest, overwrite); + } + catch (UnauthorizedAccessException e) + { + DebugConsole.ThrowError($"Cannot copy \"{src}\" to \"{dest}\": unauthorized access. The file/folder might be read-only!", e); + } } public static void Move(string src, string dest) @@ -362,7 +442,14 @@ namespace Barotrauma.IO DebugConsole.ThrowError($"Cannot move \"{src}\" to \"{dest}\": modifying the contents of the destination folder is not allowed"); return; } - System.IO.File.Move(src, dest); + try + { + System.IO.File.Move(src, dest); + } + catch (UnauthorizedAccessException e) + { + DebugConsole.ThrowError($"Cannot move \"{src}\" to \"{dest}\": unauthorized access. The file/folder might be read-only!", e); + } } public static void Delete(ContentPath path) => Delete(path.Value); @@ -374,7 +461,14 @@ namespace Barotrauma.IO DebugConsole.ThrowError($"Cannot delete file \"{path}\": modifying the contents of this folder/using this extension is not allowed."); return; } - System.IO.File.Delete(path); + try + { + System.IO.File.Delete(path); + } + catch (UnauthorizedAccessException e) + { + DebugConsole.ThrowError($"Cannot delete {path}: unauthorized access. The file/folder might be read-only!", e); + } } public static DateTime GetLastWriteTime(string path) @@ -407,7 +501,15 @@ namespace Barotrauma.IO System.IO.FileAccess.Read : access; var shareVal = share ?? (access == System.IO.FileAccess.Read ? System.IO.FileShare.Read : System.IO.FileShare.None); - return new FileStream(path, System.IO.File.Open(path, mode, access, shareVal)); + try + { + return new FileStream(path, System.IO.File.Open(path, mode, access, shareVal)); + } + catch (UnauthorizedAccessException e) + { + DebugConsole.ThrowError($"Cannot open {path} (stream): unauthorized access. The file/folder might be read-only!", e); + return null; + } } public static FileStream? OpenRead(string path) @@ -432,7 +534,14 @@ namespace Barotrauma.IO DebugConsole.ThrowError($"Cannot write all bytes to \"{path}\": modifying the files in this folder/with this extension is not allowed."); return; } - System.IO.File.WriteAllBytes(path, contents); + try + { + System.IO.File.WriteAllBytes(path, contents); + } + catch (UnauthorizedAccessException e) + { + DebugConsole.ThrowError($"Cannot write at {path}: unauthorized access. The file/folder might be read-only!", e); + } } public static void WriteAllText(string path, string contents, System.Text.Encoding? encoding = null) @@ -442,7 +551,14 @@ namespace Barotrauma.IO DebugConsole.ThrowError($"Cannot write all text to \"{path}\": modifying the files in this folder/with this extension is not allowed."); return; } - System.IO.File.WriteAllText(path, contents, encoding ?? System.Text.Encoding.UTF8); + try + { + System.IO.File.WriteAllText(path, contents, encoding ?? System.Text.Encoding.UTF8); + } + catch (UnauthorizedAccessException e) + { + DebugConsole.ThrowError($"Cannot write at {path}: unauthorized access. The file/folder might be read-only!", e); + } } public static void WriteAllLines(string path, IEnumerable contents, System.Text.Encoding? encoding = null) @@ -452,22 +568,53 @@ namespace Barotrauma.IO DebugConsole.ThrowError($"Cannot write all lines to \"{path}\": modifying the files in this folder/with this extension is not allowed."); return; } - System.IO.File.WriteAllLines(path, contents, encoding ?? System.Text.Encoding.UTF8); + try + { + System.IO.File.WriteAllLines(path, contents, encoding ?? System.Text.Encoding.UTF8); + } + catch (UnauthorizedAccessException e) + { + DebugConsole.ThrowError($"Cannot write at {path}: unauthorized access. The file/folder might be read-only!", e); + } } public static byte[] ReadAllBytes(string path) { - return System.IO.File.ReadAllBytes(path); + try + { + return System.IO.File.ReadAllBytes(path); + } + catch (UnauthorizedAccessException e) + { + DebugConsole.ThrowError($"Cannot read {path}: unauthorized access. The file/folder might be read-only!", e); + return Array.Empty(); + } } public static string ReadAllText(string path, System.Text.Encoding? encoding = null) { - return System.IO.File.ReadAllText(path, encoding ?? System.Text.Encoding.UTF8); + try + { + return System.IO.File.ReadAllText(path, encoding ?? System.Text.Encoding.UTF8); + } + catch (UnauthorizedAccessException e) + { + DebugConsole.ThrowError($"Cannot read {path}: unauthorized access. The file/folder might be read-only!", e); + return string.Empty; + } } public static string[] ReadAllLines(string path, System.Text.Encoding? encoding = null) { - return System.IO.File.ReadAllLines(path, encoding ?? System.Text.Encoding.UTF8); + try + { + return System.IO.File.ReadAllLines(path, encoding ?? System.Text.Encoding.UTF8); + } + catch (UnauthorizedAccessException e) + { + DebugConsole.ThrowError($"Cannot read {path}: unauthorized access. The file/folder might be read-only!", e); + return Array.Empty(); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs index 262d7b87a..85fa4395f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -394,7 +394,7 @@ namespace Barotrauma objects = objects.OrderBy(p => (p as PrefabWithUintIdentifier)?.UintIdentifier ?? 0); } List objectList = objects.ToList(); - List weights = objectList.Select(o => weightMethod(o)).ToList(); + List weights = objectList.Select(weightMethod).ToList(); return SelectWeightedRandom(objectList, weights, random); } @@ -774,6 +774,32 @@ namespace Barotrauma return self.Equals(other); } + /// + /// Converts a 16-bit audio sample to float value between -1 and 1. + /// + public static float ShortAudioSampleToFloat(short value) + { + return value / 32767f; + } + + /// + /// Converts a float value between -1 and 1 to a 16-bit audio sample. + /// + public static short FloatToShortAudioSample(float value) + { + int temp = (int)(32767 * value); + if (temp > short.MaxValue) + { + temp = short.MaxValue; + } + else if (temp < short.MinValue) + { + temp = short.MinValue; + } + return (short)temp; + } + + /// + /// Returns closest point on a rectangle to a given point. + /// If the point is inside the rectangle, the point itself is returned. + /// + /// + /// + /// + public static Vector2 GetClosestPointOnRectangle(RectangleF rect, Vector2 point) + { + Vector2 closest = new Vector2( + MathHelper.Clamp(point.X, rect.Left, rect.Right), + MathHelper.Clamp(point.Y, rect.Top, rect.Bottom)); + + if (point.X < rect.Left) + { + closest.X = rect.Left; + } + else if (point.X > rect.Right) + { + closest.X = rect.Right; + } + + if (point.Y < rect.Top) + { + closest.Y = rect.Top; + } + else if (point.Y > rect.Bottom) + { + closest.Y = rect.Bottom; + } + + return closest; + } } } diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index e0d8290f7..0e6507e08 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,12 +1,192 @@ ------------------------------------------------------------------------------------------------------------------------------------------------- +v1.5.7.0 +------------------------------------------------------------------------------------------------------------------------------------------------- + +- Fixed certain modules failing to spawn in colonies. + +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.5.6.0 +------------------------------------------------------------------------------------------------------------------------------------------------- + +- Fixed "Junction Junkie"" talent giving an incorrect number of XP. +- Fixed "Machine Maniac" and "Quickfixer" talents not affecting fabricator and duct block repairs. +- Fixed "Better Than New" talent not affecting reactor repairs. +- Reverted the changes to colony module tags introduced in the previous build. + +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.5.5.0 +------------------------------------------------------------------------------------------------------------------------------------------------- + +Changes and additions: +- Added a new, optional, permadeath mode in the multiplayer campaign. When your character dies, they stay dead, along with all their XP and talents. You will have to acquire a new character to respawn and continue playing. This can happen in two ways: you can wait to hire a new character for yourself from the next non-hostile outpost, or take control of a bot to resume playing right away. +- Added a new, optional, iron man mode in the multiplayer campaign for those looking for an insane extra challenge. Players who have died in Ironman mode can only spectate, and hope that the rest of the crew will make it. +- Added a new wreck mission: clear Thalamus. +- Added more beacon stations with bandits. +- Added giving items to others by dragging and dropping. It's always enabled when targeting pets and bots (on the same team), but can also be enabled in the server settings for targeting players. +- Added a slider at the bottom of the tab menu that allows you to set the salary of all crew members whose salary has not been manually set, and also determines new characters salary for late joiners. +- Added support for new outputs in item containers: + - "contained_conditions" (sum of the conditions of contained items) + - "contained_conditions_percentage" (sum of the condition percentages of contained items) + - "contained_items" (total number of contained items) +- Turret auto operate settings now transfer to the other turrets that are swapped in place of the original. +- Turret auto operate functionality can now be enabled or disabled with signals. +- Reduced the minimum length of the outpost hallways. Should get rid of the horizontal hallways and reduce the length of some vertical hallways. +- Monsters can now hear when the players are using the text chat and in-game voice chat. +- Changes to carrying turret ammunition: + - Can now be carried with one hand, instead of requiring two. + - Each shell/box now slows the character by 20%. + - Adjusted the hold positions. +- Made small and medium mudraptor eggs glow the same way as the large version. The small version can now spawn in mudraptor nest missions, and they're extremely difficult to find without the glow. +- Improved the hireable husks and clowns. Added titles, made the gear more appropriate, and added high-level versions of the NPCs. +- Optimization: Fixed medical clinic UI refreshing and the server sending updates to clients whenever the number of afflictions on a character changes (which can in extreme cases be several times per frame). +- Pumps and engines now deteriorate faster when being operated at full power for a while. +- Changes to the highlighting to make it easier to interact with items near pets. +- Made combat diving suits and slipsuits reduce visibility less than the other suits. +- Added support for hosting servers in a local network without a connection to Steam or EOS. You need to set requireauthentication="false" in the server config to allow clients to join without Steam/EOS authentication. +- Updated Crawler and Crawler Husk textures to a higher resolution. +- Changed the logic of assigning jobs for the bots in the multiplayer game mode. Previously the jobs were assigned based on the number of spawnpoints for that job in the sub, now the game instead tries to distribute jobs evenly to the bots. +- Changed the logic of generating hireable characters in the outposts: + - All outposts now offer at least one hireable for a given job, if that job is missing from the crew. + - Added medics, security and captains as hireables in mines. + +Balance: +- HMG has less severance chance and always applies a slow to characters, rather than just when carried in hands. +- Assault Rifle magazine size is now 30, up from 20. +- Flamer and Prototype Steam Cannons are better as weapons when used with Incendium Fuel Tank. +- Lead and Uranium can now be launched with the Scrap Cannon too, increased the Scrap Cannon damage with some materials. +- Auto-Shotgun is now less accurate than the Riot Shotgun. +- Grenade Launcher can now hold 6 grenades, and fires faster. +- Bandolier is now held in the bag slot, replacing the Toolbelt/Backpack but only accepting munition-type items (includes grenades). +- Reduced bonuses given by money-related talents of the Captain's "Politician" tree. Stacking all bonuses was too effective. +- Adjusted Captain's "Politician" talent tree. +- Fixed the talent "Your reputation..." not giving double money for cities. +- Minor speed buffs to Combat Diving Suit, PUCS, Slipsuit (only when swimming). +- Added a bit more level generation variation to later biomes. +- Changed the Great Sea a tiny bit in appearance (tweaking the chunks available). +- Fixed some XML oversights, some plants had 0 commonness in all biomes and thus never appeared (for seemingly no reason). + +AI: +- Outpost NPCs now blame the character who equipped a diving mask with welding fuel inside it for the deaths caused by it. +- Security bots don't inspect characters for stolen items in the first 30 seconds of a round. Prevents "unfair" inspections you have no chance to react to if you happen to spawn right next to a security NPC with stolen items on you. +- Outpost guards will now hold fire for a while, give two warnings, and after that attack the target, unless it complies (e.g. by ragdolling). They still prefer stun weapons while trying to arrest, but can also use lethal weapons, unless the target is on the same team. +- Targets that have escaped, resisted being arrested, or acted aggressively, will now be considered as criminals. Criminals will be immediately arrested with no warnings, if they do something illegal. The criminal status persists only for one round. +- The outpost guards should now also always confiscate all weapons and stolen items when they arrest a target. Previously this happened inconsistently. +- Fixed outpost security guards only attacking thieves when they can stun the target. +- Fixed outpost guards not letting go of the target they’re inspecting for stolen items when their objective changes (e.g. when someone is attacked). +- Fixed inconsistent guard reactions on interacting with handcuffs. They are now always considered illegal items, unless equipped (handcuffed). +- Fixed/changed: guards can now follow the player to the player sub. They will now wait for a while, after which they'll return back to the outpost, if able to. +- Fixed bots sometimes yelling incorrectly that they can't find a path when following/holding a position. +- Human enemies and outpost guards can now react to noises (ai targets that have the tag "ProvocativeToHumanAI"). +- Fixed automatically created hulls in the docking ports not being marked as wet rooms, allowing the bots to drop the suits there. +- Bots no longer automatically take diving suits off in shuttles. +- Bots no longer take off diving suits while climbing. They should now drop the suits in the next platform instead. +- Bots no longer cancel (looping) orders outside of the submarine. Some orders are still cancelled. +- Added a separate job for outpost managers. Fixes outpost managers trying to operate the navigation panels in the outposts. +- Fixed bots getting stuck in some outpost stairs. +- Fixed bots sometimes stopping in the stairs while idling around. +- Bots don't take items from magnetic suspension crates or the crates recovered from a wreck in the "lost cargo" missions. +- Fixed bots "cleaning up" items from magnetic suspension crates and crates salvaged from a wreck. + +Submarine layers (previously "groups"): +- Added CheckDifficultyAction. Can be used to check the difficulty of the current level. +- Option to require a specific layer to be present for an event or event set to be triggered. +- Stop updating items that are in a hidden layer (previously e.g. reactors and other devices would work fine despite being hidden) +- Hide wires that are connected to a hidden item. These are still visible in the connection panel though (just locked). +- Added DamageBeaconStationAction. Can be used to disconnect wires and break devices and walls in beacon stations. +- Fixed the "layers visible by default" sometimes showing the layers incorrectly. +- Fixed entities getting hidden when loading a sub with some hidden-by-default layers in the sub editor. +- Disabling a layer with spawnpoints in it now also disables the spawnpoints. + +Circuit box UX: +- Fixed selecting wires causing you to also select components if they overlapped. +- Added character count indicator to circuit box labels and turn the text red if it goes over. +- Show a prefab icon on the cursor of the wire that is being dragged when you start dragging a wire. +- Input and output connections can be renamed by right clicking. +- Labels can be renamed by double-clicking. + +Tools: +- Fixed editing water or fires using the console commands not working while on the freecam mode. +- Water edit mode: you can now double click with the primary mouse button to set a room flooding with water instantly. Double click with the secondary mouse button to completely drain the room. +- Fire edit mode: you can now use the secondary mouse button to gradually extinguish the flames in a room. Double click with the secondary mouse button to put off the flames instantly. +- Copy-pasting multiple commands into the console executes all of them in order. +- Added a parameter for specifying a location in the "teleportcharacter" console command. +- The auto-completions in the console can now be browsed backward by holding left shift. +- Added some subcategories shown in the value inspector views. Not yet used extensively. +- Fixed turret rotation limits seemingly inverting when the angle wraps around at 360 or -360 degrees. + +Modding: +- Fixed "holdpos" attribute no longer working on throwable item components. +- Added "PropulsionSpeed" StatValue and use it instead of "PropulsionSpeedMultiplier" in status effects. The latter doesn't stack, meaning if you wear multiple items that affect the propulsion speed, the last one would take priority. +- Added a new attribute "ShouldBeOpen" to doors. Can be used by status effects to tell the door to open/close. +- The game now prefers English texts when a localized text isn't found. This means that if your mod is only translated to English, e.g. mission and item names use the English text if someone is playing the game in another language, as opposed to displaying the text configured in the xml directly (e.g. missionname.mycustommission). +- Fixed "TransferAfflictions" setting of StatusEffect (which can be used to transfer afflictions from the character executing the effect to a new one spawned by the effect) not working correctly if the characters have a different amount of vitality. +- Instead of having to define the pirate sub files in the mission, you can also just leave it out and let the mission choose a random enemy sub. The reward, preferred difficulty, and tags that restrict which mission a sub can be used in, can be configured in the sub editor. +- Changed how outpost modules are configured for abandoned outposts: Special outpost module types are now used: "AbandonedAirlock", "AbandonedAdminModule", etc. Previously these special modules used the same module types as normal outposts, but were restricted to only spawn in outposts of the appropriate type. That meant a mod could not add a new module that's used in all types of “normal” outposts by setting the location type to "Any", because it would also be used in abandoned outposts. +- Made vision obstruction effect adjustable: instead of being just on/off, there's a ObstructVisionAmount value that can be set to a value between 0 and 1. A value of 0.5 roughly corresponds to the old obstruction effect, and setting the obstruction using the old ObstructVision boolean sets it to that value. +- Better support for including non-human characters in the crew: CharacterInfo (which defines a characters job, skills and such, and is required for characters in the crew) can be enabled on any type of character by adding HasCharacterInfo="true" to the character config. +- Fixed crashing when you try to spawn a character defined to use human AI but doesn't have CharacterInfo or a job. +- Added an option to supply "addtocrew" argument to the "spawncharacter" console command. Makes it easier to test the above changes, and allows spawning "nonhuman crewmates" using console commands. +- Added a new parameter for upscaling character textures: SourceRectScale. It's intentionally not exposed in the character editor, but can be used together with the existing TextureScale to adjust the scaling of the textures. E.g. TextureScale 0.5 and SourceRectScale 2.0 doubles the resolution without having to adjust the other values of the ragdoll. +- Fixed DamagedByRepairTools not working if the item doesn't also take damage from projectiles or melee weapons. +- Fixed artifact events causing a crash if the item the event is supposed to spawn can't be found. + +Fixes: +- Fixed the game sometimes crashing on welding a leak. +- Fixed store stocks of most items increasing constantly, all the way up to 100. +- Fixed ID cards not getting assigned the appropriate tags when you choose to get them delivered immediately. +- Fixed antibiotics not applying "drunkweakness" as intended. +- Fixed characters holding throwables, like grenades, weirdly in an unintentional pose. +- Fixed "exhibitionism" event failing if clothes or a diving suit are held in the hand slots. +- Fixed outposts giving an endless supply of missions if you keep taking up the missions, and then saving the game and reloading it again. +- Fixed flipping of the item not being taken into account when launching a projectile (making left-facing projectiles launch backwards). +- Fixed inability to hire/fire/rename/heal "twin bots" (bots with the same name, appearance and job). +- Fixed characters moving faster on underwater scooters while wearing a backpack and an exosuit than when wearing other equipment or none at all. Also affected the movement speed of the characters while wearing/holding other items. +- Fixed some CJK characters, for example ㅎ and A not drawing in the game properly. +- Fixed item container sorting behaving inconsistently when there's instances/stacks of the same item in multiple slots (randomly shuffling the items around). +- Fixed label tags sent via Wi-Fi components not using the localized text in the chat interface. +- Fixed "minleveldifficulty" and "maxleveldifficulty" attributes being ignored in the single mission mode. +- Fixed vertical gaps not emitting particles if the top of the gap is not above the top of the hull the water is flowing to. +- Fixed some draw order issues on alien hatches and doors. +- Fixed decorative items on held items glowing for no reason. +- Fixed a potential softlock if you accidentally start a round in a level that's too deep for the sub. The crush depth warnings on the campaign map now take the start/end position of the level into account, and the sub doesn't take depth damage in the first minute of the round, giving you a chance to return if you end up in a level that's too deep. +- Fixed BeaconStation and Wreck info (e.g. place on the ceiling and the difficulty settings) resetting when you enter the sub editor's test mode. +- Fixed skin tint (both the normal skin color and tints applied by afflictions) affecting the color of the damage overlay (i.e. darker-skinned characters seemed to have darker blood). +- Fixed melee weapons losing collision with platforms and level walls after being swung. +- Moved the alien terminals a bit farther from the guardian pods, so that the player operating the terminal won't get damaged by the exploding pod. +- Fixed inability to open some item interfaces in the sub editor (more specifically, items without an item container). +- Fixed attempting to flip items that cannot be flipped on the X axis, for example crate shelves, not doing anything and looking broken when the submarine is flipped on the X axis (for example in PvP). +- Fixed sprite bleeding on some structures and items. +- Fixed clients sometimes displaying missions as having failed even though they succeeded server-side. +- Fixed fabricating getting interrupted if you start fabricating an item whose recipe is unlocked by a talent on some bot, and then switch to that bot. +- Fixed items that are marked for "relocation" (automatically moved to the main sub) after a bot drops them in the outpost disappearing if you save and quit. Now we also handle the relocation when saving and quitting mid-round, not just when starting a new round. +- AutoInteractWithContained fixes: + - Ignore AutoInteractWithContained in editors (e.g. don't force picking up an item from an artifact container when trying to rewire it in the editor). + - Enabled AutoInteractWithContained on fire extinguisher brackets and weapon holders (interacting with one automatically picks up the item). +- Fixed all walls taking double damage. +- Fixed portable pump UI flickering on and off when picked up in MP. +- Adjusted Camel airlock layout: removed the inner horizontal wall and moved the wires and components. +- Fixed the oxygen generator not being wired on KopraKoht beacon station. +- Fixed respawn shuttle spawning when a respawn triggers, even if all the characters spawned directly in the main sub (e.g. because they had an existing character who was previously on the sub). +- Fixed docking ports and hatches being very difficult to interact with when they overlap with a door or a hatch. +- Fixed flak cannon shrapnel not damaging the character who fired the cannon. +- Fixed campaign map sometimes auto-opening at the beginning of the round (more specifically, when traversing backwards to certain kinds of levels). +- Fixed crashing when the game doesn't have access to certain folders or files (e.g. if the folder the saves are stored in is set to be read-only). Should now throw a console error instead. +- Fixed selected container staying open if you lose access to it by e.g. putting the ID card that's required for access into another item in your inventory. +- Fixed characters teleporting outside when they pass from hull to another, and there's a tiny sliver of "outside space" between the hulls. +- Fixed hanging end disappearing from a wire when you connect the other end. +- Fixed marking a stack for deconstruction only marking the first item for deconstruction server-side, even though the deconstruct icon is displayed on all client-side. +- Fixed an issue that caused duplicate characters in the HR manager menu. Happened when you moved a hire to the pending list, closed the menu and then reopened it. + +------------------------------------------------------------------------------------------------------------------------------------------------- v1.4.6.0 ------------------------------------------------------------------------------------------------------------------------------------------------- -- Fixed monsters sometimes spawning immediately after the round starts (Often happened between levels, when there was no outpost between them). + +- Fixed monsters sometimes spawning immediately after the round starts (often happened between levels, when there was no outpost between them). - The 'Art of Submarine Warfare' book granted by the 'War Stories' talent is now a separate item and the original book has been reverted to its original state. - Fixed Thalamus' fleshgun ropes not being able to stick to a submarine anymore. - Fixed Thalamus' flesh spike crashing the game in the multiplayer game mode. - Added a scrollbar to the submarine warning list. Fixes the list not fitting on the screen, when there were multiple, long errors. -- Fixed sever list filters not saving properly. +- Fixed server list filters not saving properly. - Fixed marking a stack of items to be ignored or deconstructed only taking into account the first item of the stack, instead applying to all items of the stack. - Removed duplicate localization lines that caused old versions of text to show up in some places. - Fixes to the Chinese localization. @@ -115,7 +295,6 @@ Medical system: - Added "adrenaline rush" as an effect for adrenaline. "Adrenaline rush" keeps the patient conscious for its duration, removes all active stun when inflicted and applies short-term stun resistance. - Husk infections can be treated with sufforin and cyanide (but you must be careful to have a cure at hand!). - Antibiotic glue can be used multiple times. -- Made Pomegrenade Extract a bit more useful: gives the "slow metabolism" buff. - Ethanol and rum can be poured on limbs to treat infections and burns. - Removed skill requirements from tonic liquid. - Alien blood causes organ damage, making it less viable as a risk-free cheap alternative for blood packs. diff --git a/Barotrauma/BarotraumaTest/EnumTests.cs b/Barotrauma/BarotraumaTest/EnumTests.cs new file mode 100644 index 000000000..27b7f7598 --- /dev/null +++ b/Barotrauma/BarotraumaTest/EnumTests.cs @@ -0,0 +1,111 @@ +using Barotrauma; +using FluentAssertions; +using Barotrauma.Extensions; +using FluentAssertions; +using Xunit; + +namespace TestProject; + +public class EnumTests +{ + [Fact] + public void TestFlags() + { + TestMissionType(); + TestLevelPositionType(); + TestAlignmentType(); + } + + private static void TestMissionType() + { + const MissionType beacon = MissionType.Beacon; + beacon.HasFlag(MissionType.Cargo).Should().BeFalse(); + beacon.HasAnyFlag(MissionType.Cargo).Should().BeFalse(); + + beacon.HasFlag(MissionType.Beacon).Should().BeTrue(); + beacon.HasAnyFlag(MissionType.Beacon).Should().BeTrue(); + + const MissionType beaconOrCargo = MissionType.Beacon | MissionType.Cargo; + beaconOrCargo.HasFlag(MissionType.Monster).Should().BeFalse(); + beaconOrCargo.HasAnyFlag(MissionType.Monster).Should().BeFalse(); + MissionType testEnum = MissionType.Beacon; + testEnum.HasFlag(MissionType.Cargo).Should().BeFalse(); + testEnum.HasAnyFlag(MissionType.Cargo).Should().BeFalse(); + + beaconOrCargo.HasFlag(MissionType.Beacon).Should().BeTrue(); + beaconOrCargo.HasAnyFlag(MissionType.Beacon).Should().BeTrue(); + testEnum.HasFlag(MissionType.Beacon).Should().BeTrue(); + testEnum.HasAnyFlag(MissionType.Beacon).Should().BeTrue(); + + beaconOrCargo.HasFlag(MissionType.Cargo).Should().BeTrue(); + beaconOrCargo.HasAnyFlag(MissionType.Cargo).Should().BeTrue(); + + const MissionType all = MissionType.All; + all.HasFlag(MissionType.All).Should().BeTrue(); + all.HasAnyFlag(MissionType.All).Should().BeTrue(); + + all.HasFlag(MissionType.Beacon).Should().BeTrue(); + all.HasAnyFlag(MissionType.Beacon).Should().BeTrue(); + + all.HasFlag(MissionType.Beacon | MissionType.Salvage).Should().BeTrue(); + all.HasAnyFlag(MissionType.Beacon | MissionType.Salvage).Should().BeTrue(); + + const MissionType manyTypes = MissionType.Beacon | MissionType.Monster; + beaconOrCargo.HasFlag(manyTypes).Should().BeFalse(); + beaconOrCargo.HasAnyFlag(manyTypes).Should().BeTrue(); + + beaconOrCargo.HasFlag(MissionType.All).Should().BeFalse(); + beaconOrCargo.HasAnyFlag(MissionType.All).Should().BeTrue(); + } + + private static void TestLevelPositionType() + { + Level.PositionType abyss = Level.PositionType.Abyss; + abyss.HasFlag(Level.PositionType.Ruin).Should().BeFalse(); + abyss.HasAnyFlag(Level.PositionType.Ruin).Should().BeFalse(); + + abyss.HasFlag(Level.PositionType.Abyss).Should().BeTrue(); + abyss.HasAnyFlag(Level.PositionType.Abyss).Should().BeTrue(); + + abyss = Level.PositionType.Abyss | Level.PositionType.Ruin; + abyss.HasFlag(Level.PositionType.Cave).Should().BeFalse(); + abyss.HasAnyFlag(Level.PositionType.Cave).Should().BeFalse(); + + abyss.HasFlag(Level.PositionType.Abyss).Should().BeTrue(); + abyss.HasAnyFlag(Level.PositionType.Abyss).Should().BeTrue(); + + abyss.HasFlag(Level.PositionType.Ruin).Should().BeTrue(); + abyss.HasAnyFlag(Level.PositionType.Ruin).Should().BeTrue(); + + const Level.PositionType abyssOrOutpost = Level.PositionType.Abyss | Level.PositionType.Outpost; + abyss.HasFlag(abyssOrOutpost).Should().BeFalse(); + abyss.HasAnyFlag(abyssOrOutpost).Should().BeTrue(); + + (Level.PositionType.Abyss.AddFlag(Level.PositionType.Outpost) == abyssOrOutpost).Should().BeTrue(); + (abyssOrOutpost.RemoveFlag(Level.PositionType.Outpost) == Level.PositionType.Abyss).Should().BeTrue(); + } + + private static void TestAlignmentType() + { + const Alignment left = Alignment.Left; + left.HasFlag(Alignment.Left).Should().BeTrue(); + left.HasAnyFlag(Alignment.Left).Should().BeTrue(); + + left.HasFlag(Alignment.Center).Should().BeFalse(); + left.HasAnyFlag(Alignment.Center).Should().BeFalse(); + + left.HasFlag(Alignment.TopLeft).Should().BeFalse(); + left.HasAnyFlag(Alignment.TopLeft).Should().BeTrue(); + + const Alignment leftOrCenter = Alignment.Left | Alignment.Center; + left.HasFlag(leftOrCenter).Should().BeFalse(); + left.HasAnyFlag(leftOrCenter).Should().BeTrue(); + + const Alignment topLeft = Alignment.TopLeft; + topLeft.HasFlag(left).Should().BeTrue(); + topLeft.HasAnyFlag(left).Should().BeTrue(); + + topLeft.HasFlag(leftOrCenter).Should().BeFalse(); + topLeft.HasAnyFlag(leftOrCenter).Should().BeTrue(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaTest/GenericToolBoxTests.cs b/Barotrauma/BarotraumaTest/GenericToolBoxTests.cs index 24ae767ff..f597af7fa 100644 --- a/Barotrauma/BarotraumaTest/GenericToolBoxTests.cs +++ b/Barotrauma/BarotraumaTest/GenericToolBoxTests.cs @@ -5,6 +5,7 @@ using Xunit; using Barotrauma; using FluentAssertions; using FsCheck; +using Microsoft.Xna.Framework; namespace TestProject; @@ -56,4 +57,44 @@ public sealed class GenericToolBoxTests ToolBox.StatIdentifierMatches(pair.First, pair.Second).Should().BeFalse(); }).VerboseCheckThrowOnFailure(); } + + [Fact] + public void PointOnRectClosestToPoint() + { + RectangleF rect1 = new(0, 0, 5, 5); + Vector2 point1 = new(10, 10); + + // Should be the bottom right corner of the rectangle + Test(rect1, point1, expectedClosest: new Vector2(5, 5)); + + RectangleF rect2 = new(0, 0, 5, 5); + Vector2 point2 = new(2, 10); + + // Should be the bottom edge of the rectangle at x = 2 since the point is between the bottom left and bottom right corners + Test(rect2, point2, expectedClosest: new Vector2(2, 5)); + + RectangleF rect3 = new(0, 0, 5, 5); + Vector2 point3 = new(-10, -3); + + // Should be the top left corner of the rectangle + Test(rect3, point3, expectedClosest: new Vector2(0, 0)); + + RectangleF rect4 = new(0, 0, 100, 100); + Vector2 point4 = new(55, 52); + + // Should be the point itself since it's inside the rectangle + Test(rect4, point4, expectedClosest: point4); + + RectangleF rect5 = new(0, 0, 100, 100); + Vector2 point5 = new(55, 102); + + // Should be the top edge of the rectangle at y = 100 since the point is between the top left and top right corners + Test(rect5, point5, expectedClosest: new Vector2(55, 100)); + + void Test(RectangleF rect, Vector2 point, Vector2 expectedClosest) + { + var closest = ToolBox.GetClosestPointOnRectangle(rect, point); + closest.Should().BeEquivalentTo(expectedClosest); + } + } } diff --git a/HelperScripts/cleanup_obj.sh b/HelperScripts/cleanup_obj.sh old mode 100755 new mode 100644 diff --git a/Libraries/BarotraumaLibs/BarotraumaCore/Extensions/EnumExtensions.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Extensions/EnumExtensions.cs new file mode 100644 index 000000000..9035b785b --- /dev/null +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Extensions/EnumExtensions.cs @@ -0,0 +1,40 @@ +using System; + +namespace Barotrauma.Extensions +{ + public static class EnumExtensions + { + /// + /// Enum.HasFlag() checks if all flags matches. This method checks if any of them matches. + /// E.g. when myEnum = SomeEnum.First | SomeEnum.Second, myEnum.HasFlag(SomeEnum.First | SomeEnum.Third) returns false, because not all of the flags match, but HasAnyFlag(SomeEnum.First | SomeEnum.Third) returns true, because some of the flags match. + /// + public static bool HasAnyFlag(this T type, T value) where T : Enum + { + int typeValue = Convert.ToInt32(type); + int flagValue = Convert.ToInt32(value); + return (typeValue & flagValue) != 0; + } + + /// + /// Adds a flag value to an enum. + /// Note that enums are value types, so you need to use the value returned from this method. + /// + public static T AddFlag(this T @enum, T flag) where T : Enum + { + int enumValue = Convert.ToInt32(@enum); + int flagValue = Convert.ToInt32(flag); + return (T)(object)(enumValue | flagValue); + } + + /// + /// Removes a flag value from an enum. + /// Note that enums are value types, so you need to use the value returned from this method. + /// + public static T RemoveFlag(this T @enum, T flag) where T : Enum + { + int enumValue = Convert.ToInt32(@enum); + int flagValue = Convert.ToInt32(flag); + return (T)(object)(enumValue & ~flagValue); + } + } +} \ No newline at end of file diff --git a/Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/AccountId/UnauthenticatedAccountId.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/AccountId/UnauthenticatedAccountId.cs new file mode 100644 index 000000000..88c5431fe --- /dev/null +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/AccountId/UnauthenticatedAccountId.cs @@ -0,0 +1,45 @@ +#nullable enable + +using System; + +namespace Barotrauma.Networking +{ + /// + /// Represents an account ID of a client that has not been authenticated in any way. The account ID is only based on the client's name. + /// Only used on servers that allow joining without Steam/EOS authentication (such as when playing in a local network with no internet connection). + /// + public sealed class UnauthenticatedAccountId : AccountId + { + private const string Prefix = "UNAUTHENTICATED_"; + + private readonly string clientName; + + public override string StringRepresentation => Prefix + clientName; + + public override string EosStringRepresentation => StringRepresentation; + + public UnauthenticatedAccountId(string clientName) + { + this.clientName = clientName; + } + + public override bool Equals(object? obj) + => obj is UnauthenticatedAccountId otherId + && otherId.clientName.Equals(clientName); + + public override int GetHashCode() + { + return clientName.GetHashCode(); + } + + public new static Option Parse(string str) + { + if (str.IsNullOrWhiteSpace()) { return Option.None; } + if (!str.StartsWith(Prefix, StringComparison.InvariantCultureIgnoreCase)) + { + return Option.None; + } + return Option.Some(new UnauthenticatedAccountId(str[Prefix.Length..])); + } + } +} \ No newline at end of file diff --git a/Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/Address/Address.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/Address/Address.cs index 67b3870e4..e7902a883 100644 --- a/Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/Address/Address.cs +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/Address/Address.cs @@ -18,7 +18,17 @@ namespace Barotrauma.Networking public override string ToString() => StringRepresentation; public static bool operator ==(Address a, Address b) - => a.Equals(b); + { + if (a is null || b is null) + { + return a is null == b is null; + } + else + { + return a.Equals(b); + } + } + public static bool operator !=(Address a, Address b) => !(a == b);