From d6a886bf6b4b4c36556f64905c83644e0fb59b53 Mon Sep 17 00:00:00 2001 From: Regalis11 Date: Wed, 21 Jun 2023 16:14:31 +0300 Subject: [PATCH 01/53] v1.0.21.0 (summer patch hotfix) --- .../Characters/CharacterNetworking.cs | 31 ++++++++- .../BarotraumaClient/ClientSource/GUI/GUI.cs | 15 ++++- .../ClientSource/GUI/HUDLayoutSettings.cs | 5 +- .../ClientSource/GameSession/CrewManager.cs | 1 + .../ClientSource/Map/Lights/ConvexHull.cs | 2 +- .../ClientSource/Map/Lights/LightManager.cs | 12 ++-- .../ClientSource/Map/Lights/LightSource.cs | 35 ++++++---- .../BarotraumaClient/LinuxClient.csproj | 2 +- Barotrauma/BarotraumaClient/MacClient.csproj | 2 +- .../BarotraumaClient/WindowsClient.csproj | 2 +- .../BarotraumaServer/LinuxServer.csproj | 2 +- Barotrauma/BarotraumaServer/MacServer.csproj | 2 +- .../Characters/CharacterNetworking.cs | 19 ++++-- .../BarotraumaServer/WindowsServer.csproj | 2 +- .../Characters/AI/AIController.cs | 3 - .../Characters/AI/EnemyAIController.cs | 12 ---- .../AI/Objectives/AIObjectiveFixLeak.cs | 3 +- .../AI/Objectives/AIObjectiveGoTo.cs | 29 +++++---- .../AI/Objectives/AIObjectiveRepairItem.cs | 3 +- .../AI/Objectives/AIObjectiveRescue.cs | 2 +- .../AI/Objectives/AIObjectiveRescueAll.cs | 2 +- .../SharedSource/Characters/AI/PetBehavior.cs | 41 ++++++------ .../Health/Afflictions/AfflictionPrefab.cs | 54 ++++++++++----- .../Characters/Health/CharacterHealth.cs | 2 +- .../SharedSource/GameSession/GameSession.cs | 2 +- .../Components/Holdable/LevelResource.cs | 2 +- .../SharedSource/Map/Levels/LevelData.cs | 4 +- .../Map/Outposts/OutpostGenerator.cs | 65 ++++++++++++------- Barotrauma/BarotraumaShared/changelog.txt | 21 +++++- 29 files changed, 241 insertions(+), 136 deletions(-) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 8cd302262..8be87d764 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -201,9 +201,9 @@ namespace Barotrauma keys[(int)InputType.Use].Held = useInput; keys[(int)InputType.Use].SetState(false, useInput); + bool crouching = msg.ReadBoolean(); if (AnimController is HumanoidAnimController) { - bool crouching = msg.ReadBoolean(); keys[(int)InputType.Crouch].Held = crouching; keys[(int)InputType.Crouch].SetState(false, crouching); } @@ -269,7 +269,34 @@ namespace Barotrauma if (readStatus) { ReadStatus(msg); - AIController?.ClientRead(msg); + bool isEnemyAi = msg.ReadBoolean(); + if (isEnemyAi) + { + byte aiState = msg.ReadByte(); + if (AIController is EnemyAIController enemyAi) + { + enemyAi.State = (AIState)aiState; + } + else + { + DebugConsole.AddWarning($"Received enemy AI data for a character with no {nameof(EnemyAIController)}. Ignoring..."); + } + bool isPet = msg.ReadBoolean(); + if (isPet) + { + byte happiness = msg.ReadByte(); + byte hunger = msg.ReadByte(); + if ((AIController as EnemyAIController)?.PetBehavior is PetBehavior petBehavior) + { + petBehavior.Happiness = (float)happiness / byte.MaxValue * petBehavior.MaxHappiness; + petBehavior.Hunger = (float)hunger / byte.MaxValue * petBehavior.MaxHunger; + } + else + { + DebugConsole.AddWarning($"Received pet AI data for a character with no {nameof(PetBehavior)}. Ignoring..."); + } + } + } } msg.ReadPadBits(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index 838c2ee27..c235f0a35 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -111,7 +111,7 @@ namespace Barotrauma /// public static float AspectRatioAdjustment => HorizontalAspectRatio < 1.4f ? (1.0f - (1.4f - HorizontalAspectRatio)) : 1.0f; - public static bool IsUltrawide => HorizontalAspectRatio > 2.0f; + public static bool IsUltrawide => HorizontalAspectRatio > 2.3f; public static int UIWidth { @@ -2438,13 +2438,15 @@ namespace Barotrauma var pauseMenuInner = new GUIFrame(new RectTransform(new Vector2(0.13f, 0.3f), PauseMenu.RectTransform, Anchor.Center) { MinSize = new Point(250, 300) }); - var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 0.6f), pauseMenuInner.RectTransform, Anchor.Center)) + float padding = 0.06f; + + var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 0.8f), pauseMenuInner.RectTransform, Anchor.BottomCenter) { RelativeOffset = new Vector2(0.0f, padding) }) { Stretch = true, RelativeSpacing = 0.05f }; - new GUIButton(new RectTransform(new Vector2(0.1f, 0.1f), pauseMenuInner.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point((int)(15 * GUI.Scale)) }, + new GUIButton(new RectTransform(new Vector2(0.1f, 0.07f), pauseMenuInner.RectTransform, Anchor.TopRight) { RelativeOffset = new Vector2(padding) }, "", style: "GUIBugButton") { IgnoreLayoutGroups = true, @@ -2520,6 +2522,13 @@ namespace Barotrauma } GUITextBlock.AutoScaleAndNormalize(buttonContainer.Children.Where(c => c is GUIButton).Select(c => ((GUIButton)c).TextBlock)); + //scale to ensure there's enough room for all the buttons + pauseMenuInner.RectTransform.MinSize = new Point( + pauseMenuInner.RectTransform.MinSize.X, + Math.Max( + (int)(buttonContainer.Children.Sum(c => c.Rect.Height + buttonContainer.Rect.Height * buttonContainer.RelativeSpacing)), + pauseMenuInner.RectTransform.MinSize.X)); + } void CreateButton(string textTag, GUIComponent parent, Action action, string verificationTextTag = null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs index 8442731b2..f6bb9831e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs @@ -160,8 +160,9 @@ namespace Barotrauma int crewAreaY = ButtonAreaTop.Bottom + Padding; int crewAreaHeight = ObjectiveAnchor.Top - Padding - crewAreaY; - float crewAreaWidthMultiplier = GUI.IsUltrawide ? GUI.HorizontalAspectRatio : 1.0f; - CrewArea = new Rectangle(Padding, crewAreaY, (int)(Math.Max(400 * GUI.Scale, 220) * crewAreaWidthMultiplier), crewAreaHeight); + CrewArea = new Rectangle(Padding, crewAreaY, + (int)MathHelper.Clamp(400 * GUI.Scale, 220, GameMain.GraphicsHeight * 0.4f), + crewAreaHeight); InventoryAreaLower = new Rectangle(ChatBoxArea.Right + Padding * 7, inventoryTopY, GameMain.GraphicsWidth - Padding * 9 - ChatBoxArea.Width, GameMain.GraphicsHeight - inventoryTopY); int healthWindowWidth = (int)(GameMain.GraphicsWidth * 0.5f); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index eddd741f2..b38ef3af7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -95,6 +95,7 @@ namespace Barotrauma { CanBeFocused = false }; + crewArea.RectTransform.NonScaledSize = HUDLayoutSettings.CrewArea.Size; // AbsoluteOffset is set in UpdateProjectSpecific based on crewListOpenState crewList = new GUIListBox(new RectTransform(Vector2.One, crewArea.RectTransform), style: null, isScrollBarOnDefaultSide: false) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs index 7faf33912..82ad83280 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs @@ -561,7 +561,7 @@ namespace Barotrauma.Lights if (IsSegmentFacing(losVertices[0].Pos, losVertices[1].Pos, lightSourcePos)) { - Array.Reverse(ShadowVertices); + Array.Reverse(ShadowVertices, 0, ShadowVertexCount); } CalculateLosPenumbraVertices(lightSourcePos); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index e846bfa4f..e179ffe94 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -482,7 +482,7 @@ namespace Barotrauma.Lights { foreach (MapEntity e in (Submarine.VisibleEntities ?? MapEntity.mapEntityList)) { - if (e is Item item && item.GetComponent() is Wire wire) + if (e is Item item && !item.HiddenInGame && item.GetComponent() is Wire wire) { wire.DebugDraw(spriteBatch, alpha: 0.4f); } @@ -719,6 +719,7 @@ namespace Barotrauma.Lights { foreach (var ch in convexHulls) { + if (!ch.Enabled) { continue; } Vector2 currentViewPos = pos; Vector2 defaultViewPos = ViewTarget.DrawPosition; if (ch.ParentEntity?.Submarine != null) @@ -742,10 +743,13 @@ namespace Barotrauma.Lights { if (!convexHull.Enabled || !convexHull.Intersects(camView)) { continue; } - Vector2 relativeLightPos = pos; - if (convexHull.ParentEntity?.Submarine != null) { relativeLightPos -= convexHull.ParentEntity.Submarine.Position; } + Vector2 relativeViewPos = pos; + if (convexHull.ParentEntity?.Submarine != null) + { + relativeViewPos -= convexHull.ParentEntity.Submarine.DrawPosition; + } - convexHull.CalculateLosVertices(relativeLightPos); + convexHull.CalculateLosVertices(relativeViewPos); for (int i = 0; i < convexHull.ShadowVertexCount; i++) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs index 9574a3d6a..20c9eb63f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs @@ -206,6 +206,8 @@ namespace Barotrauma.Lights private readonly List convexHullsInRange; + private readonly HashSet visibleConvexHulls = new HashSet(); + public Texture2D texture; public SpriteEffects LightSpriteEffect; @@ -717,6 +719,8 @@ namespace Barotrauma.Lights public void RayCastTask(Vector2 drawPos, float rotation) { + visibleConvexHulls.Clear(); + Vector2 drawOffset = Vector2.Zero; float boundsExtended = TextureRange; if (OverrideLightTexture != null) @@ -904,17 +908,12 @@ namespace Barotrauma.Lights bool isPoint1 = MathUtils.LineToPointDistanceSquared(seg1.Start.WorldPos, seg1.End.WorldPos, p.WorldPos) < 25.0f; bool isPoint2 = MathUtils.LineToPointDistanceSquared(seg2.Start.WorldPos, seg2.End.WorldPos, p.WorldPos) < 25.0f; + bool markAsVisible = false; if (isPoint1 && isPoint2) { //hit at the current segmentpoint -> place the segmentpoint into the list verts.Add(p.WorldPos); - - foreach (ConvexHullList hullList in convexHullsInRange) - { - hullList.IsHidden.Remove(p.ConvexHull); - hullList.IsHidden.Remove(seg1.ConvexHull); - hullList.IsHidden.Remove(seg2.ConvexHull); - } + markAsVisible = true; } else if (intersection1.index != intersection2.index) { @@ -922,13 +921,13 @@ namespace Barotrauma.Lights //we definitely want to generate new geometry here verts.Add(isPoint1 ? p.WorldPos : intersection1.pos); verts.Add(isPoint2 ? p.WorldPos : intersection2.pos); - - foreach (ConvexHullList hullList in convexHullsInRange) - { - hullList.IsHidden.Remove(p.ConvexHull); - hullList.IsHidden.Remove(seg1.ConvexHull); - hullList.IsHidden.Remove(seg2.ConvexHull); - } + markAsVisible = true; + } + if (markAsVisible) + { + visibleConvexHulls.Add(p.ConvexHull); + visibleConvexHulls.Add(seg1.ConvexHull); + visibleConvexHulls.Add(seg2.ConvexHull); } //if neither of the conditions above are met, we just assume //that the raycasts both resulted on the same segment @@ -1396,6 +1395,14 @@ namespace Barotrauma.Lights return; } + foreach (var visibleConvexHull in visibleConvexHulls) + { + foreach (var convexHullList in convexHullsInRange) + { + convexHullList.IsHidden.Remove(visibleConvexHull); + } + } + CalculateLightVertices(verts); LastRecalculationTime = (float)Timing.TotalTime; diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 4b5dccfa1..0d5ab41b5 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.0.20.1 + 1.0.21.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 6d8354723..231ef9033 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.0.20.1 + 1.0.21.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index cec27c949..6ec5a61ad 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.0.20.1 + 1.0.21.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index f1b4c157c..b1013bee7 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.0.20.1 + 1.0.21.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 592c084d0..6faa3fa7e 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.0.20.1 + 1.0.21.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index c597f599e..38598bd86 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -376,10 +376,9 @@ namespace Barotrauma tempBuffer.WriteBoolean(aiming); tempBuffer.WriteBoolean(shoot); tempBuffer.WriteBoolean(use); - if (AnimController is HumanoidAnimController) - { - tempBuffer.WriteBoolean(((HumanoidAnimController)AnimController).Crouching); - } + + tempBuffer.WriteBoolean(AnimController is HumanoidAnimController { Crouching: true }); + tempBuffer.WriteBoolean(attack); Vector2 relativeCursorPos = cursorPosition - AimRefPosition; @@ -430,7 +429,17 @@ namespace Barotrauma if (writeStatus) { WriteStatus(tempBuffer); - AIController?.ServerWrite(tempBuffer); + tempBuffer.WriteBoolean(AIController is EnemyAIController); + if (AIController is EnemyAIController enemyAi) + { + tempBuffer.WriteByte((byte)enemyAi.State); + tempBuffer.WriteBoolean(enemyAi.PetBehavior is PetBehavior); + if (enemyAi.PetBehavior is PetBehavior petBehavior) + { + tempBuffer.WriteByte((byte)((petBehavior.Happiness / petBehavior.MaxHappiness) * byte.MaxValue)); + tempBuffer.WriteByte((byte)((petBehavior.Hunger / petBehavior.MaxHunger) * byte.MaxValue)); + } + } HealthUpdatePending = false; } } diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 4c66ef099..ea593140b 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.0.20.1 + 1.0.21.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs index a5069b8f3..f079663fc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs @@ -521,8 +521,5 @@ namespace Barotrauma protected virtual void OnStateChanged(AIState from, AIState to) { } protected virtual void OnTargetChanged(AITarget previousTarget, AITarget newTarget) { } - - public virtual void ClientRead(IReadMessage msg) { } - public virtual void ServerWrite(IWriteMessage msg) { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index caa1b2919..908f6dcff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -4055,18 +4055,6 @@ namespace Barotrauma } return null; } - - public override void ServerWrite(IWriteMessage msg) - { - msg.WriteByte((byte)State); - PetBehavior?.ServerWrite(msg); - } - - public override void ClientRead(IReadMessage msg) - { - State = (AIState)msg.ReadByte(); - PetBehavior?.ClientRead(msg); - } } //the "memory" of the Character diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index f08a264c6..a1db6b5c1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -200,7 +200,8 @@ namespace Barotrauma Leak.linkedTo.Any(e => e is Hull h && (character.CurrentHull == h || h.linkedTo.Contains(character.CurrentHull))), endNodeFilter = IsSuitableEndNode, // The Go To objective can be abandoned if the leak is fixed (in which case we don't want to use the dialogue) - SpeakCannotReachCondition = () => !CheckObjectiveSpecific() + // Only report about contextual targets. + SpeakCannotReachCondition = () => isPriority && !CheckObjectiveSpecific() }, onAbandon: () => { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index dfc1966bf..ecde45351 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -223,23 +223,30 @@ namespace Barotrauma Hull targetHull = GetTargetHull(); if (!IsFollowOrder) { + // Abandon if going through unsafe paths or targeting unsafe hulls. bool isUnreachable = HumanAIController.UnreachableHulls.Contains(targetHull); if (!objectiveManager.CurrentObjective.IgnoreUnsafeHulls) { - if (HumanAIController.UnsafeHulls.Contains(targetHull)) + // Wait orders check this so that the bot temporarily leaves the unsafe hull. + // Non-orders (that are not set to ignore the unsafe hulls) abandon. In practice this means e.g. repair and clean up item subobjectives (of the looping parent objective). + // Other orders are only abandoned if the hull is unreachable, because the path is invalid or not found at all. + if (IsWaitOrder || !objectiveManager.HasOrders()) { - isUnreachable = true; - HumanAIController.AskToRecalculateHullSafety(targetHull); - } - else if (PathSteering?.CurrentPath != null) - { - foreach (WayPoint wp in PathSteering.CurrentPath.Nodes) + if (HumanAIController.UnsafeHulls.Contains(targetHull)) { - if (wp.CurrentHull == null) { continue; } - if (HumanAIController.UnsafeHulls.Contains(wp.CurrentHull)) + isUnreachable = true; + HumanAIController.AskToRecalculateHullSafety(targetHull); + } + else if (PathSteering?.CurrentPath != null) + { + foreach (WayPoint wp in PathSteering.CurrentPath.Nodes) { - isUnreachable = true; - HumanAIController.AskToRecalculateHullSafety(wp.CurrentHull); + if (wp.CurrentHull == null) { continue; } + if (HumanAIController.UnsafeHulls.Contains(wp.CurrentHull)) + { + isUnreachable = true; + HumanAIController.AskToRecalculateHullSafety(wp.CurrentHull); + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs index 9c06fb8b2..63533f1e1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -222,7 +222,8 @@ namespace Barotrauma { var objective = new AIObjectiveGoTo(Item, character, objectiveManager) { - TargetName = Item.Name + TargetName = Item.Name, + SpeakCannotReachCondition = () => isPriority }; if (repairTool != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index cba0a97f4..eef63cd47 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -287,7 +287,7 @@ namespace Barotrauma if (affliction.Prefab == null) { throw new Exception("Affliction prefab was null"); } float bestSuitability = 0.0f; Item bestItem = null; - foreach (KeyValuePair treatmentSuitability in affliction.Prefab.TreatmentSuitability) + foreach (KeyValuePair treatmentSuitability in affliction.Prefab.TreatmentSuitabilities) { if (currentTreatmentSuitabilities.ContainsKey(treatmentSuitability.Key) && currentTreatmentSuitabilities[treatmentSuitability.Key] > bestSuitability) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs index 97ce2855f..9e3faff48 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs @@ -109,6 +109,7 @@ namespace Barotrauma foreach (Affliction affliction in allAfflictions) { if (affliction.Prefab.IsBuff) { continue; } + if (!affliction.Prefab.HasTreatments) { continue; } if (!ignoreTreatmentThreshold) { //other afflictions of the same type increase the "treatability" @@ -116,7 +117,6 @@ namespace Barotrauma float totalAfflictionStrength = character.CharacterHealth.GetTotalAdjustedAfflictionStrength(affliction); if (totalAfflictionStrength < affliction.Prefab.TreatmentThreshold) { continue; } } - if (affliction.Prefab.TreatmentSuitability.None(kvp => kvp.Value > 0)) { continue; } if (allAfflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Identifier))) { continue; } yield return affliction; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs index 4f64dac4d..8b9598c81 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs @@ -435,37 +435,34 @@ namespace Barotrauma spawnPoint ??= WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine?.Info.Type == SubmarineType.Player).GetRandomUnsynced(); spawnPos = spawnPoint?.WorldPosition ?? Submarine.MainSub.WorldPosition; } - var pet = Character.Create(speciesName, spawnPos, seed, spawnInitialItems: false); - var petBehavior = (pet?.AIController as EnemyAIController)?.PetBehavior; - if (petBehavior != null) + + var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName.ToIdentifier()); + if (characterPrefab == null) { - petBehavior.Owner = owner; - var petBehaviorElement = subElement.Element("petbehavior"); - if (petBehaviorElement != null) + DebugConsole.ThrowError($"Failed to load the pet \"{speciesName}\". Character prefab not found."); + continue; + } + var pet = Character.Create(characterPrefab, spawnPos, seed, spawnInitialItems: false); + if (pet != null) + { + var petBehavior = (pet.AIController as EnemyAIController)?.PetBehavior; + if (petBehavior != null) { - petBehavior.Hunger = petBehaviorElement.GetAttributeFloat("hunger", 50.0f); - petBehavior.Happiness = petBehaviorElement.GetAttributeFloat("happiness", 50.0f); + petBehavior.Owner = owner; + var petBehaviorElement = subElement.Element("petbehavior"); + if (petBehaviorElement != null) + { + petBehavior.Hunger = petBehaviorElement.GetAttributeFloat("hunger", 50.0f); + petBehavior.Happiness = petBehaviorElement.GetAttributeFloat("happiness", 50.0f); + } } } - var inventoryElement = subElement.Element("inventory"); if (inventoryElement != null) { pet.SpawnInventoryItems(pet.Inventory, inventoryElement.FromPackage(null)); - } + } } } - - public void ServerWrite(IWriteMessage msg) - { - msg.WriteRangedSingle(Happiness, 0.0f, MaxHappiness, 8); - msg.WriteRangedSingle(Hunger, 0.0f, MaxHunger, 8); - } - - public void ClientRead(IReadMessage msg) - { - Happiness = msg.ReadRangedSingle(0.0f, MaxHappiness, 8); - Hunger = msg.ReadRangedSingle(0.0f, MaxHunger, 8); - } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 3b65cc320..76a771c27 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -7,6 +7,7 @@ using System.Xml.Linq; using Barotrauma.Extensions; using System.Collections.Immutable; using Barotrauma.Items.Components; +using System.Linq; namespace Barotrauma { @@ -841,20 +842,16 @@ namespace Barotrauma /// public readonly Sprite AfflictionOverlay; - public IEnumerable> TreatmentSuitability + public ImmutableDictionary TreatmentSuitabilities { - get - { - foreach (var itemPrefab in ItemPrefab.Prefabs) - { - float suitability = itemPrefab.GetTreatmentSuitability(Identifier) + itemPrefab.GetTreatmentSuitability(AfflictionType); - if (!MathUtils.NearlyEqual(suitability, 0.0f)) - { - yield return new KeyValuePair(itemPrefab.Identifier, suitability); - } - } - } - } + get; + private set; + } = new Dictionary().ToImmutableDictionary(); + + /// + /// Can this affliction be treated with some item? + /// + public bool HasTreatments { get; private set; } public AfflictionPrefab(ContentXElement element, AfflictionsFile file, Type type) : base(file, element.GetAttributeIdentifier("identifier", "")) { @@ -974,6 +971,22 @@ namespace Barotrauma constructor = type.GetConstructor(new[] { typeof(AfflictionPrefab), typeof(float) }); } + private void RefreshTreatmentSuitabilities() + { + var newTreatmentSuitabilities = new Dictionary(); + + foreach (var itemPrefab in ItemPrefab.Prefabs) + { + float suitability = itemPrefab.GetTreatmentSuitability(Identifier) + itemPrefab.GetTreatmentSuitability(AfflictionType); + if (!MathUtils.NearlyEqual(suitability, 0.0f)) + { + newTreatmentSuitabilities.TryAdd(itemPrefab.Identifier, suitability); + } + } + HasTreatments = newTreatmentSuitabilities.Any(kvp => kvp.Value > 0); + TreatmentSuitabilities = newTreatmentSuitabilities.ToImmutableDictionary(); + } + public LocalizedString GetDescription(float strength, Description.TargetType targetType) { foreach (var description in Descriptions) @@ -993,9 +1006,16 @@ namespace Barotrauma return defaultDescription; } - public static void LoadAllEffects() + /// + /// Should be called before each round: loads all StatusEffects and refreshes treatment suitabilities. + /// + public static void LoadAllEffectsAndTreatmentSuitabilities() { - Prefabs.ForEach(p => p.LoadEffects()); + foreach (var prefab in Prefabs) + { + prefab.RefreshTreatmentSuitabilities(); + prefab.LoadEffects(); + } } public static void ClearAllEffects() @@ -1003,7 +1023,7 @@ namespace Barotrauma Prefabs.ForEach(p => p.ClearEffects()); } - public void LoadEffects() + private void LoadEffects() { ClearEffects(); foreach (var subElement in configElement.Elements()) @@ -1032,7 +1052,7 @@ namespace Barotrauma } } - public void ClearEffects() + private void ClearEffects() { effects.Clear(); periodicEffects.Clear(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 95dad7ad3..7f0ad42ad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -1136,7 +1136,7 @@ namespace Barotrauma } } - foreach (KeyValuePair treatment in affliction.Prefab.TreatmentSuitability) + foreach (KeyValuePair treatment in affliction.Prefab.TreatmentSuitabilities) { float suitability = treatment.Value * strength; if (treatment.Value > strength) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 7c7c9e453..4e0370674 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -392,7 +392,7 @@ namespace Barotrauma DateTime startTime = DateTime.Now; #endif RoundDuration = 0.0f; - AfflictionPrefab.LoadAllEffects(); + AfflictionPrefab.LoadAllEffectsAndTreatmentSuitabilities(); MirrorLevel = mirrorLevel; if (SubmarineInfo == null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs index 353d25d9a..67c3ff90d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs @@ -135,7 +135,7 @@ namespace Barotrauma.Items.Components private void CreateTriggerBody() { System.Diagnostics.Debug.Assert(trigger == null, "LevelResource trigger already created!"); - var body = item.body ?? holdable.Body; + var body = item.body ?? holdable?.Body; if (body != null && Attached) { trigger = new PhysicsBody(body.Width, body.Height, body.Radius, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index b7a5aea58..3e3d51b4c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -178,7 +178,7 @@ namespace Barotrauma if (eventSet is null) { continue; } int count = childElement.GetAttributeInt("count", 0); if (count < 1) { continue; } - FinishedEvents.Add(eventSet, count); + FinishedEvents.TryAdd(eventSet, count); } static EventSet FindSetRecursive(EventSet parentSet, Identifier setIdentifier) @@ -365,7 +365,7 @@ namespace Barotrauma if (FinishedEvents.Any()) { var finishedEventsElement = new XElement(nameof(FinishedEvents)); - foreach (var (set, count) in FinishedEvents) + foreach (var (set, count) in FinishedEvents.DistinctBy(f => f.Key.Identifier)) { var element = new XElement(nameof(FinishedEvents), new XAttribute("set", set.Identifier), diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index 5f410a025..6b896fac7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -1353,44 +1353,38 @@ namespace Barotrauma if (startWaypoint.WorldPosition.X > endWaypoint.WorldPosition.X) { - var temp = startWaypoint; - startWaypoint = endWaypoint; - endWaypoint = temp; + (endWaypoint, startWaypoint) = (startWaypoint, endWaypoint); } if (hallwayLength > 100 && isHorizontal) { + //if the hallway is longer than 100 pixels, generate some waypoints inside it + //for vertical hallways this isn't necessarily, it's done as a part of the ladder generation in AlignLadders WayPoint prevWayPoint = startWaypoint; + WayPoint firstWayPoint = null; for (float x = leftHull.Rect.Right + 50; x < rightHull.Rect.X - 50; x += 100.0f) { var newWayPoint = new WayPoint(new Vector2(x, hullBounds.Y + 110.0f), SpawnType.Path, sub); + firstWayPoint ??= newWayPoint; prevWayPoint.linkedTo.Add(newWayPoint); newWayPoint.linkedTo.Add(prevWayPoint); prevWayPoint = newWayPoint; } + if (firstWayPoint != null) + { + firstWayPoint.linkedTo.Add(startWaypoint); + startWaypoint.linkedTo.Add(firstWayPoint); + } if (prevWayPoint != null) { prevWayPoint.linkedTo.Add(endWaypoint); endWaypoint.linkedTo.Add(prevWayPoint); } } - - WayPoint closestWaypoint = null; - float closestDistSqr = 30.0f * 30.0f; - foreach (WayPoint waypoint in WayPoint.WayPointList) + else { - if (waypoint == startWaypoint) { continue; } - float dist = Vector2.DistanceSquared(waypoint.WorldPosition, startWaypoint.WorldPosition); - if (dist < closestDistSqr) - { - closestWaypoint = waypoint; - closestDistSqr = dist; - } - } - if (closestWaypoint != null) - { - startWaypoint.linkedTo.Add(closestWaypoint); - closestWaypoint.linkedTo.Add(startWaypoint); + startWaypoint.linkedTo.Add(endWaypoint); + endWaypoint.linkedTo.Add(startWaypoint); } } } @@ -1444,7 +1438,7 @@ namespace Barotrauma { foreach (MapEntity me in entities[module]) { - if (!(me is Gap gap)) { continue; } + if (me is not Gap gap) { continue; } var door = gap.ConnectedDoor; if (door != null && !door.UseBetweenOutpostModules) { continue; } if (placedModules.Any(m => m.PreviousGap == gap || m.ThisGap == gap)) @@ -1524,15 +1518,38 @@ namespace Barotrauma static void RemoveLinkedEntity(MapEntity linked) { - if (linked is Item linkedItem && linkedItem.Connections != null) + if (linked is Item linkedItem) { - foreach (Connection connection in linkedItem.Connections) + if (linkedItem.Connections != null) { - foreach (Wire w in connection.Wires.ToArray()) + foreach (Connection connection in linkedItem.Connections) { - w?.Item.Remove(); + foreach (Wire w in connection.Wires.ToArray()) + { + w?.Item.Remove(); + } } } + //if we end up removing a ladder, remove its waypoints too + if (linkedItem.GetComponent() is Ladder ladder) + { + var ladderWaypoints = WayPoint.WayPointList.FindAll(wp => wp.Ladders == ladder); + foreach (var ladderWaypoint in ladderWaypoints) + { + //got through all waypoints linked to the ladder waypoints, and link them together + //so we don't end up breaking up any paths by removing the ladder waypoints + for (int i = 0; i < ladderWaypoint.linkedTo.Count; i++) + { + if (ladderWaypoint.linkedTo[i] is not WayPoint waypoint1 || waypoint1.Ladders == ladder) { continue; } + for (int j = i + 1; j < ladderWaypoint.linkedTo.Count; j++) + { + if (ladderWaypoint.linkedTo[j] is not WayPoint waypoint2 || waypoint2.Ladders == ladder) { continue; } + waypoint1.ConnectTo(waypoint2); + } + } + } + ladderWaypoints.ForEach(wp => wp.Remove()); + } } linked.Remove(); } diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 75b4957f2..a81ba4c7a 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,22 @@ +--------------------------------------------------------------------------------------------------------- +v1.0.21.0 +--------------------------------------------------------------------------------------------------------- + +Fixes: +- Fixed LOS effect sometimes "lagging behind" when the sub is moving fast. +- Fixed some minor visual issues (occasional jitter/flickering) on the LOS effect. +- Fixed some issues in the bot AI that we're causing a large performance hit particularly in situations when there's lots of bots in a sub with leaks. +- Fixed bots abandoning their orders (such as operating a turret) if the room is unsafe (e.g. flooded). +- Fixed an issue in character syncing that occasionally caused disconnects with the error message "Exception thrown while reading segment EntityPosition, tried to read too much data from segment". +- Fixed wires set to be hidden in-game (e.g. invisible circuits built outside the sub) being visible on the Electrician's Goggles. +- Fixed an issue with level resources that caused crashes with certain mods (e.g. ones that include subs with piezo crystals). +- Fixed NPCs waiting on some outpost modules never reaching their targets, causing peculiar behavior. +- Fixed waypoints sometimes not getting connected between outpost modules if there's a very short hallway between them. Addresses some cities missing connections between waypoints, causing AI to be unable to navigate through the modules. +- Fixed some UI layout issues (most noticeably, ultra-wide crew list) on certain resolutions like 3440x1440. +- Fixed campaign saves occasionally failing to load with the error "an item with the same key has already been added". Seemed to only occur when using certain mods. +- Fixed crashing when you e.g. use a pet from some mod in the campaign, disable the mod and reload the save. +- Waypoint adjustments to most submarines, outposts, wrecks, and beacons. Especially on ladders. Should take care of the remaining AI issues on ladders (the old subs in the saves don't get updated, but the fixes apply to new subs that you don't yet own. And ofc all the subs in a new game!) + --------------------------------------------------------------------------------------------------------- v1.0.20.1 --------------------------------------------------------------------------------------------------------- @@ -5,7 +24,7 @@ v1.0.20.1 Fixes: - Fixed hidden structures not colliding anymore. - Wiring debugger: Fixed tooltip rendering under the outer frame of the connection panel. -- Wiring debugger: Fixed the glow sprite on connections having an inconsistent size in different resolutions- +- Wiring debugger: Fixed the glow sprite on connections having an inconsistent size in different resolutions. - Fixed jailbreak_sootman event getting stuck at the 1st SpawnAction, preventing most of the event from working at all. --------------------------------------------------------------------------------------------------------- From c67f6688fdf4dcffa224711a0d4f4181d9a1fad4 Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Wed, 21 Jun 2023 16:15:10 +0300 Subject: [PATCH 02/53] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 589a3ce6e..39413a43f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -54,7 +54,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.0.20.1 + - v1.0.21.0 - Other validations: required: true From 5aabc1759f042a7d096efc8ca05eda89171178b7 Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Fri, 7 Jul 2023 08:58:12 +0300 Subject: [PATCH 03/53] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 39413a43f..ddcd9a18a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -55,6 +55,7 @@ body: 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.0.21.0 + - Unstable (v1.1.3.0) - Other validations: required: true From 60b0e2ae3ed5163c823e8d59bc87a26dac4ab8c4 Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Mon, 17 Jul 2023 15:22:09 +0300 Subject: [PATCH 04/53] Update bug_report.yml Added a dropdown for selecting whether the issue occurs in sp, mp listen server, mp dedicated server, all of these or none of these. --- .github/ISSUE_TEMPLATE/bug_report.yml | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index ddcd9a18a..404d7a0d6 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -47,7 +47,26 @@ body: - Happens regularly - Happens every time I play validations: - required: true + required: true + - type: dropdown + id: mporsp + attributes: + label: Single player or multiplayer? + description: Did the issue happen in single player, multiplayer, or both? How was the server being hosted? + options: + - Single player + - Multiplayer hosted from the in-game menu (= using a listen server) + - Multiplayer hosted using a dedicated server + - Happens in both single player and multiplayer + - Happens outside single player or multiplayer game modes (e.g. game launches on startup, something broken in the main menu) + - Other + validations: + required: true + - type: input + id: othermporsp + attributes: + label: "-" + description: If you selected "Other" in the above dropdown, please clarify here. - type: dropdown id: version attributes: From a1407bf009e89873d587135ac9d88fc9dced01a6 Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Sat, 19 Aug 2023 12:31:05 +0300 Subject: [PATCH 05/53] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 404d7a0d6..69e0d97d2 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -74,7 +74,7 @@ body: 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.0.21.0 - - Unstable (v1.1.3.0) + - Unstable (v1.1.8.0) - Other validations: required: true From 8376935346b4903902d0a9fa601bde884f35b105 Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Fri, 25 Aug 2023 14:35:57 +0300 Subject: [PATCH 06/53] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 69e0d97d2..f70d03d55 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -74,7 +74,7 @@ body: 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.0.21.0 - - Unstable (v1.1.8.0) + - Unstable (v1.1.9.0) - Other validations: required: true From 0294a78afe66ddc22062e14123aa8d083f6215af Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Tue, 29 Aug 2023 18:21:59 +0300 Subject: [PATCH 07/53] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index f70d03d55..bba42f860 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -74,7 +74,7 @@ body: 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.0.21.0 - - Unstable (v1.1.9.0) + - Unstable (v1.1.10.0) - Other validations: required: true From 639d4d076dae209ba5558af73b810e568a269074 Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Fri, 22 Sep 2023 17:52:21 +0300 Subject: [PATCH 08/53] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index bba42f860..c327e282c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -74,7 +74,7 @@ body: 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.0.21.0 - - Unstable (v1.1.10.0) + - Unstable (v1.1.14.0) - Other validations: required: true From ca2c30edca3e0edf6ad69cf35ea62024793c4730 Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Fri, 13 Oct 2023 19:06:29 +0300 Subject: [PATCH 09/53] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index c327e282c..37575d177 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -74,7 +74,7 @@ body: 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.0.21.0 - - Unstable (v1.1.14.0) + - Unstable (v1.1.16.0) - Other validations: required: true From 331565f726920b6fee11fccf9d26415670f436b8 Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Thu, 19 Oct 2023 17:23:12 +0300 Subject: [PATCH 10/53] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 37575d177..ceda01f73 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.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.0.21.0 - - Unstable (v1.1.16.0) + - v1.1.18.0 (Treacherous Tides) - Other validations: required: true From de2bf3964fca4c4003677fcc24a2b0b337a83054 Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Fri, 20 Oct 2023 16:13:03 +0300 Subject: [PATCH 11/53] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index ceda01f73..070903c0c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -73,7 +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.1.18.0 (Treacherous Tides) + - v1.1.18.1 (Treacherous Tides) - Other validations: required: true From de4726b70ae651f3cfb8e7d7888948f4df180cad Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Wed, 25 Oct 2023 20:17:44 +0300 Subject: [PATCH 12/53] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 070903c0c..3293ce4d3 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -74,6 +74,7 @@ body: 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.1.18.1 (Treacherous Tides) + - v1.1.19.0 (Unstable) - Other validations: required: true From b968376247bc8957d41fbedf1da806485db9d0f1 Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Mon, 30 Oct 2023 17:12:28 +0200 Subject: [PATCH 13/53] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 3293ce4d3..a9673f38d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.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.1.18.1 (Treacherous Tides) - - v1.1.19.0 (Unstable) + - v1.1.19.3 (Treacherous Tides Hotfix 2) - Other validations: required: true From a8f9c97ddacca5f09a3e83bbaba9bb0d6b15c7be Mon Sep 17 00:00:00 2001 From: Regalis11 Date: Mon, 30 Oct 2023 17:38:29 +0200 Subject: [PATCH 14/53] v1.1.19.3 (Treacherous Tides Hotfix 2) --- .../ClientSource/Characters/Character.cs | 5 +- .../CircuitBox/CircuitBoxConnection.cs | 13 +- .../ClientSource/GameSession/RoundSummary.cs | 2 +- .../Items/Components/Signal/CircuitBox.cs | 2 +- .../ClientSource/Items/Inventory.cs | 22 +++- .../BarotraumaClient/ClientSource/Map/Hull.cs | 24 +++- .../BarotraumaClient/ClientSource/Program.cs | 3 + .../BarotraumaClient/LinuxClient.csproj | 2 +- Barotrauma/BarotraumaClient/MacClient.csproj | 2 +- .../BarotraumaClient/WindowsClient.csproj | 2 +- .../BarotraumaServer/LinuxServer.csproj | 2 +- Barotrauma/BarotraumaServer/MacServer.csproj | 2 +- .../Items/Components/Signal/CircuitBox.cs | 6 +- .../ServerSource/Networking/ChatMessage.cs | 115 ++++++++++-------- .../ServerSource/Networking/GameServer.cs | 12 +- .../ServerSource/Networking/Voting.cs | 12 +- .../BarotraumaServer/WindowsServer.csproj | 2 +- .../Characters/AI/HumanAIController.cs | 4 - .../SharedSource/Characters/Character.cs | 24 ++-- .../Characters/Health/CharacterHealth.cs | 13 +- .../Characters/Params/CharacterParams.cs | 3 + .../Events/EventActions/TagAction.cs | 65 ++++++---- .../GameSession/GameModes/CampaignMode.cs | 29 ++++- .../Items/Components/ItemComponent.cs | 6 + .../Items/Components/Machines/Fabricator.cs | 2 +- .../SharedSource/Items/Components/Planter.cs | 3 + .../Items/Components/Signal/CircuitBox.cs | 46 +++++-- .../SharedSource/Items/Inventory.cs | 8 +- .../SharedSource/Items/Item.cs | 94 +++++++++++--- .../SharedSource/Items/ItemInventory.cs | 4 +- .../SharedSource/Items/ItemPrefab.cs | 32 +++-- .../SharedSource/Map/Explosion.cs | 64 +++++++--- .../BarotraumaShared/SharedSource/Map/Gap.cs | 36 +++--- .../BarotraumaShared/SharedSource/Map/Hull.cs | 6 +- .../SharedSource/Map/Levels/LevelData.cs | 2 +- .../SharedSource/Map/Structure.cs | 12 +- .../StatusEffects/StatusEffect.cs | 47 +++---- Barotrauma/BarotraumaShared/changelog.txt | 45 ++++--- 38 files changed, 517 insertions(+), 256 deletions(-) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 115a95e77..a223fe878 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -336,7 +336,10 @@ namespace Barotrauma float pressure = AnimController.CurrentHull == null ? 100.0f : AnimController.CurrentHull.LethalPressure; if (pressure > 0.0f) { - float zoomInEffectStrength = MathHelper.Clamp(pressure / 100.0f, 0.1f, 1.0f); + //lerp in during the 1st second of the pressure timer so the zoom doesn't + //"flicker" in and out if the pressure fluctuates around the minimum threshold + float timerMultiplier = (PressureTimer / 100.0f); + float zoomInEffectStrength = MathHelper.Clamp(pressure / 100.0f * timerMultiplier, 0.0f, 1.0f); cam.Zoom = MathHelper.Lerp(cam.Zoom, cam.DefaultZoom + (Math.Max(pressure, 10) / 150.0f) * Rand.Range(0.9f, 1.1f), zoomInEffectStrength); diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxConnection.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxConnection.cs index 7fea1d1f1..ea89c9f57 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxConnection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxConnection.cs @@ -8,24 +8,25 @@ namespace Barotrauma { internal abstract partial class CircuitBoxConnection { - public string Name => Label.Value.Value; + public string Name => Connection.Name; + public CircuitBoxLabel Label { get; private set; } private Sprite? knobSprite, screwSprite, connectorSprite; - private static int padding => GUI.IntScale(8); + private static int Padding => GUI.IntScale(8); private Option tooltip = Option.None; private partial void InitProjSpecific(CircuitBox circuitBox) { - Label = new CircuitBoxLabel(Connection.Name, GUIStyle.SubHeadingFont); + Label = new CircuitBoxLabel(Connection.DisplayName, GUIStyle.SubHeadingFont); knobSprite = circuitBox.ConnectionSprite; screwSprite = circuitBox.ConnectionScrewSprite; connectorSprite = circuitBox.WireConnectorSprite; - Length = Rect.Width + padding + Label.Size.X; + Length = Rect.Width + Padding + Label.Size.X; } public void Draw(SpriteBatch spriteBatch, Vector2 drawPos, Vector2 parentPos, Color color) @@ -41,11 +42,11 @@ namespace Barotrauma float xPos; if (IsOutput) { - xPos = drawRect.Left - padding - Label.Size.X; + xPos = drawRect.Left - Padding - Label.Size.X; } else { - xPos = drawRect.Right + padding; + xPos = drawRect.Right + Padding; } Vector2 stringPos = new Vector2(xPos, drawRect.Center.Y - Label.Size.Y / 2f); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index 96fb2522a..33f02d0fe 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -98,7 +98,7 @@ namespace Barotrauma if (gameSession.Missions.Any(m => m is CombatMission)) { crewHeader.Text = CombatMission.GetTeamName(CharacterTeamType.Team1); - GUIFrame crewFrame2 = new GUIFrame(new RectTransform(new Vector2(0.35f, 0.45f), background.RectTransform, Anchor.TopCenter, minSize: new Point(minWidth, minHeight))); + GUIFrame crewFrame2 = new GUIFrame(new RectTransform(crewFrame.RectTransform.RelativeSize, background.RectTransform, Anchor.TopCenter, minSize: new Point(minWidth, minHeight))); rightPanels.Add(crewFrame2); GUIFrame crewFrameInner2 = new GUIFrame(new RectTransform(new Point(crewFrame2.Rect.Width - padding * 2, crewFrame2.Rect.Height - padding * 2), crewFrame2.RectTransform, Anchor.Center), style: "InnerFrame"); var crewContent2 = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), crewFrameInner2.RectTransform, Anchor.Center)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs index 7dafcf617..3f466be5c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs @@ -269,7 +269,7 @@ namespace Barotrauma.Items.Components resource = ItemPrefab.Prefabs[Tags.FPGACircuit]; } - AddComponentInternal(ICircuitBoxIdentifiable.FindFreeID(Components), prefab, resource, pos, static delegate { }); + AddComponentInternal(ICircuitBoxIdentifiable.FindFreeID(Components), prefab, resource, pos, Character.Controlled, onItemSpawned: null); return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index ac8f2e935..ecb897421 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -1505,14 +1505,28 @@ namespace Barotrauma int stackAmount = DraggingItems.Count; if (selectedSlot?.ParentInventory != null) { - stackAmount = Math.Min( - stackAmount, - selectedSlot.ParentInventory.HowManyCanBePut(draggedItem.Prefab, selectedSlot.SlotIndex, draggedItem.Condition)); + if (selectedSlot.Item?.OwnInventory != null) + { + int maxAmountPerSlot = 0; + for (int i = 0; i < SelectedSlot.Item.OwnInventory.Capacity; i++) + { + maxAmountPerSlot = Math.Max( + maxAmountPerSlot, + selectedSlot.Item.OwnInventory.HowManyCanBePut(draggedItem.Prefab, i, draggedItem.Condition, ignoreItemsInSlot: true)); + } + stackAmount = Math.Min(stackAmount, maxAmountPerSlot); + } + else + { + stackAmount = Math.Min( + stackAmount, + selectedSlot.ParentInventory.HowManyCanBePut(draggedItem.Prefab, selectedSlot.SlotIndex, draggedItem.Condition, ignoreItemsInSlot: true)); + } } Vector2 stackCountPos = itemPos + Vector2.One * iconSize * 0.25f; string stackCountText = "x" + stackAmount; GUIStyle.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos + Vector2.One, Color.Black); - GUIStyle.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos, GUIStyle.TextColorBright); + GUIStyle.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos, GUIStyle.TextColorBright); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index 5eb9bcd95..25f13500c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -300,13 +300,27 @@ namespace Barotrauma " - Oxygen: " + ((int)OxygenPercentage), new Vector2(drawRect.X + 5, -drawRect.Y + 5), Color.White); GUIStyle.SmallFont.DrawString(spriteBatch, waterVolume + " / " + Volume, new Vector2(drawRect.X + 5, -drawRect.Y + 20), Color.White); - GUI.DrawRectangle(spriteBatch, new Rectangle(drawRect.Center.X, -drawRect.Y + drawRect.Height / 2, 10, (int)(100 * Math.Min(waterVolume / Volume, 1.0f))), Color.Cyan, true); - if (WaterVolume > Volume) + if (WaterVolume > 0) { - float maxExcessWater = Volume * MaxCompress; - GUI.DrawRectangle(spriteBatch, new Rectangle(drawRect.Center.X, -drawRect.Y + drawRect.Height / 2, 10, (int)(100 * (waterVolume - Volume) / maxExcessWater)), GUIStyle.Red, true); + drawProgressBar(50, new Point(0, 0), Math.Min(waterVolume / Volume, 1.0f), Color.Cyan); + if (WaterVolume > Volume) + { + float maxExcessWater = Volume * MaxCompress; + drawProgressBar(50, new Point(0, 0), (waterVolume - Volume) / maxExcessWater, GUIStyle.Red); + } + } + if (lethalPressure > 0) + { + drawProgressBar(50, new Point(20, 0), lethalPressure / 100.0f, Color.Red); + } + + void drawProgressBar(int height, Point offset, float fillAmount, Color color) + { + GUI.DrawRectangle(spriteBatch, new Rectangle(drawRect.Center.X - 2 + offset.X, -drawRect.Y - 2 + drawRect.Height / 2 + offset.Y, 14, height+4), Color.Black * 0.8f, depth: 0.01f, isFilled: true); + + int barHeight = (int)(fillAmount * height); + GUI.DrawRectangle(spriteBatch, new Rectangle(drawRect.Center.X + offset.X, -drawRect.Y + drawRect.Height / 2 + height - barHeight + offset.Y, 10, barHeight), color, isFilled: true); } - GUI.DrawRectangle(spriteBatch, new Rectangle(drawRect.Center.X, -drawRect.Y + drawRect.Height / 2, 10, 100), Color.Black); foreach (FireSource fs in FireSources) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Program.cs b/Barotrauma/BarotraumaClient/ClientSource/Program.cs index a3d44b5d6..4dd29adfa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Program.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Program.cs @@ -261,6 +261,9 @@ namespace Barotrauma { crashHeader += " " + exception.TargetSite.ToString(); } + //log the message separately, so the same error messages get grouped as the same error in GA + //(the full crash report tends to always have some differences between clients, so they get displayed separately) + GameAnalyticsManager.AddErrorEvent(GameAnalyticsManager.ErrorSeverity.Critical, crashHeader); GameAnalyticsManager.AddErrorEvent(GameAnalyticsManager.ErrorSeverity.Critical, crashHeader + "\n\n" + sb.ToString()); GameAnalyticsManager.ShutDown(); } diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 9a1fe727a..a5b67175d 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.1.18.1 + 1.1.19.3 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 7c9514990..12fa0df4c 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.1.18.1 + 1.1.19.3 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index a388bc1f5..a469892ba 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.1.18.1 + 1.1.19.3 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 7fd3c7774..7b072552b 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.1.18.1 + 1.1.19.3 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 1f5c8ac99..16781acf4 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.1.18.1 + 1.1.19.3 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs index 010111c87..c961063c5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs @@ -1,11 +1,11 @@ #nullable enable +using Barotrauma.Networking; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using Barotrauma.Networking; -using Microsoft.Xna.Framework; namespace Barotrauma.Items.Components { @@ -138,7 +138,7 @@ namespace Barotrauma.Items.Components return; } - bool result = AddComponentInternal(id, prefab, resource.Prefab, data.Position, it => + bool result = AddComponentInternal(id, prefab, resource.Prefab, data.Position, c.Character, it => { CreateServerEvent(new CircuitBoxServerCreateComponentEvent(it.ID, resource.Prefab.UintIdentifier, id, data.Position)); }); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs index e9216ab7b..93151a357 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs @@ -80,58 +80,10 @@ namespace Barotrauma.Networking { c.LastSentChatMessages.RemoveRange(0, c.LastSentChatMessages.Count - 10); } - - float similarity = 0.0f; - for (int i = 0; i < c.LastSentChatMessages.Count; i++) - { - float closeFactor = 1.0f / (c.LastSentChatMessages.Count - i); - - if (string.IsNullOrEmpty(txt)) - { - similarity += closeFactor; - } - else - { - int levenshteinDist = ToolBox.LevenshteinDistance(txt, c.LastSentChatMessages[i]); - similarity += Math.Max((txt.Length - levenshteinDist) / (float)txt.Length * closeFactor, 0.0f); - } - } - //order/report messages can be sent a little faster than normal messages without triggering the spam filter - if (orderMsg != null) - { - similarity *= 0.25f; - } - - bool isSpamExempt = RateLimiter.IsExempt(c); - - if (similarity + c.ChatSpamSpeed > 5.0f && !isSpamExempt) - { - GameMain.Server.KarmaManager.OnSpamFilterTriggered(c); - - c.ChatSpamCount++; - if (c.ChatSpamCount > 3) - { - //kick for spamming too much - GameMain.Server.KickClient(c, TextManager.Get("SpamFilterKicked").Value); - } - else - { - ChatMessage denyMsg = Create("", TextManager.Get("SpamFilterBlocked").Value, ChatMessageType.Server, null); - c.ChatSpamTimer = 10.0f; - GameMain.Server.SendDirectChatMessage(denyMsg, c); - } - return; - } - - c.ChatSpamSpeed += similarity + 0.5f; - - if (c.ChatSpamTimer > 0.0f && !isSpamExempt) - { - ChatMessage denyMsg = Create("", TextManager.Get("SpamFilterBlocked").Value, ChatMessageType.Server, null); - c.ChatSpamTimer = 10.0f; - GameMain.Server.SendDirectChatMessage(denyMsg, c); - return; - } + //order/report messages can be sent a little faster than normal messages without triggering the spam filter; + float similarityMultiplier = orderMsg != null ? 0.25f : 1.0f; + HandleSpamFilter(c, txt, out bool flaggedAsSpam, similarityMultiplier); + if (flaggedAsSpam) { return; } if (type == ChatMessageType.Order) { @@ -177,6 +129,65 @@ namespace Barotrauma.Networking } } + /// + /// Increase the client's chat spam speed and check whether the spam filter should kick in + /// + public static void HandleSpamFilter(Client c, string messageText, out bool flaggedAsSpam, float similarityMultiplier = 1.0f) + { + float similarity = 0.0f; + for (int i = 0; i < c.LastSentChatMessages.Count; i++) + { + float closeFactor = 1.0f / (c.LastSentChatMessages.Count - i); + + if (string.IsNullOrEmpty(messageText)) + { + similarity += closeFactor; + } + else + { + int levenshteinDist = ToolBox.LevenshteinDistance(messageText, c.LastSentChatMessages[i]); + similarity += Math.Max((messageText.Length - levenshteinDist) / (float)messageText.Length * closeFactor, 0.0f); + } + } + + similarity *= similarityMultiplier; + + bool isSpamExempt = RateLimiter.IsExempt(c); + + if (similarity + c.ChatSpamSpeed > 5.0f && !isSpamExempt) + { + GameMain.Server.KarmaManager.OnSpamFilterTriggered(c); + + c.ChatSpamCount++; + if (c.ChatSpamCount > 3) + { + //kick for spamming too much + GameMain.Server.KickClient(c, TextManager.Get("SpamFilterKicked").Value); + } + else + { + ChatMessage denyMsg = Create("", TextManager.Get("SpamFilterBlocked").Value, ChatMessageType.Server, null); + c.ChatSpamTimer = 10.0f; + GameMain.Server.SendDirectChatMessage(denyMsg, c); + } + flaggedAsSpam = true; + return; + } + + c.ChatSpamSpeed += similarity + 0.5f; + + if (c.ChatSpamTimer > 0.0f && !isSpamExempt) + { + ChatMessage denyMsg = Create("", TextManager.Get("SpamFilterBlocked").Value, ChatMessageType.Server, null); + c.ChatSpamTimer = 10.0f; + GameMain.Server.SendDirectChatMessage(denyMsg, c); + flaggedAsSpam = true; + return; + } + + flaggedAsSpam = false; + } + public int EstimateLengthBytesServer(Client c) { int length = 1 + //(byte)ServerNetObject.CHAT_MESSAGE diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 34f6cd497..c08936ba3 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -369,6 +369,9 @@ namespace Barotrauma.Networking if (!character.ClientDisconnected) { continue; } 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); if (!character.IsDead) { character.KillDisconnectedTimer += deltaTime; @@ -379,18 +382,19 @@ namespace Barotrauma.Networking character.Kill(CauseOfDeathType.Disconnected, null); continue; } - if (owner != null && owner.InGame && !owner.NeedsMidRoundSync && - (!ServerSettings.AllowSpectating || !owner.SpectateOnly)) + if (canOwnerTakeControl) { SetClientCharacter(owner, character); } } - else if (owner != null && + else if (canOwnerTakeControl && character.CauseOfDeath?.Type == CauseOfDeathType.Disconnected && character.CharacterHealth.VitalityDisregardingDeath > 0) { + //create network event immediately to ensure the character is revived client-side + //before the client gains control of it (normally status events are created periodically) + character.Revive(removeAfflictions: false, createNetworkEvent: true); SetClientCharacter(owner, character); - character.Revive(removeAfflictions: false); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs index 5afb8d321..81c472caf 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs @@ -345,9 +345,15 @@ namespace Barotrauma sender.SetVote(voteType, client); if (client?.Character != null) { - GameMain.Server.SendChatMessage( - TextManager.GetWithVariable("traitor.blamebutton.dialog", "[name]", client.Character.DisplayName).Value, - ChatMessageType.Radio, senderClient: sender, senderCharacter: sender.Character); + string msg = TextManager.GetWithVariable("traitor.blamebutton.dialog", "[name]", client.Character.DisplayName).Value; + ChatMessage.HandleSpamFilter(sender, msg, out bool flaggedAsSpam); + if (!flaggedAsSpam) + { + GameMain.Server.SendChatMessage( + msg, + ChatMessageType.Radio, senderClient: sender, senderCharacter: sender.Character); + sender.LastSentChatMessages.Add(msg); + } } } break; diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 72495dd7f..6948b9453 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.1.18.1 + 1.1.19.3 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 43378e91a..698f938b4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -167,10 +167,6 @@ namespace Barotrauma public HumanAIController(Character c) : base(c) { - if (!c.IsHuman) - { - throw new Exception($"Tried to create a human ai controller for a non-human: {c.SpeciesName}!"); - } insideSteering = new IndoorsSteeringManager(this, true, false); outsideSteering = new SteeringManager(this); objectiveManager = new AIObjectiveManager(c); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 2aeee3c03..679e76a1b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -1223,19 +1223,15 @@ namespace Barotrauma public static Character Create(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null, bool spawnInitialItems = true) { Character newCharacter = null; - if (prefab.Identifier != CharacterPrefab.HumanSpeciesName) + if (prefab.Identifier != CharacterPrefab.HumanSpeciesName || hasAi) { var aiCharacter = new AICharacter(prefab, position, seed, characterInfo, id, isRemotePlayer, ragdoll, spawnInitialItems); - var ai = new EnemyAIController(aiCharacter, seed); + + var ai = (prefab.Identifier == CharacterPrefab.HumanSpeciesName || aiCharacter.Params.UseHumanAI) ? + new HumanAIController(aiCharacter) as AIController : + new EnemyAIController(aiCharacter, seed); aiCharacter.SetAI(ai); - newCharacter = aiCharacter; - } - else if (hasAi) - { - var aiCharacter = new AICharacter(prefab, position, seed, characterInfo, id, isRemotePlayer, ragdoll, spawnInitialItems); - var ai = new HumanAIController(aiCharacter); - aiCharacter.SetAI(ai); - newCharacter = aiCharacter; + newCharacter = aiCharacter; } else { @@ -4656,7 +4652,7 @@ namespace Barotrauma } partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool log); - public void Revive(bool removeAfflictions = true) + public void Revive(bool removeAfflictions = true, bool createNetworkEvent = false) { if (Removed) { @@ -4705,7 +4701,11 @@ namespace Barotrauma limb.IsSevered = false; } - GameMain.GameSession?.ReviveCharacter(this); + GameMain.GameSession?.ReviveCharacter(this); + if (createNetworkEvent && GameMain.NetworkMember is { IsServer: true }) + { + GameMain.NetworkMember.CreateEntityEvent(this, new CharacterStatusEventData()); + } } public override void Remove() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index d7567b91c..d1092d476 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -148,11 +148,6 @@ namespace Barotrauma return minVitality; } return vitality; - - } - private set - { - vitality = value; } } @@ -254,7 +249,7 @@ namespace Barotrauma public CharacterHealth(Character character) { this.Character = character; - Vitality = 100.0f; + vitality = 100.0f; DoesBleed = true; UseHealthWindow = false; @@ -271,7 +266,7 @@ namespace Barotrauma this.Character = character; InitIrremovableAfflictions(); - Vitality = UnmodifiedMaxVitality; + vitality = UnmodifiedMaxVitality; minVitality = character.IsHuman ? -100.0f : 0.0f; @@ -971,7 +966,7 @@ namespace Barotrauma public void CalculateVitality() { - Vitality = MaxVitality; + vitality = MaxVitality; IsParalyzed = false; if (Unkillable || Character.GodMode) { return; } @@ -984,7 +979,7 @@ namespace Barotrauma { vitalityDecrease *= GetVitalityMultiplier(affliction, limbHealth); } - Vitality -= vitalityDecrease; + vitality -= vitalityDecrease; affliction.CalculateDamagePerSecond(vitalityDecrease); if (affliction.Strength >= affliction.Prefab.MaxStrength && diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 0fedaedf3..c3042739a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -50,6 +50,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes, description: "Can the creature live without water or does it die on dry land?"), Editable] public bool NeedsWater { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "Note: non-humans with a human AI aren't fully supported. Enabling this on a non-human character may lead to issues.")] + public bool UseHumanAI { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "Is this creature an artificial creature, like robot or machine that shouldn't be affected by afflictions that affect only organic creatures? Overrides DoesBleed."), Editable] public bool IsMachine { get; set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs index e608b7cde..508ff6a43 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs @@ -36,7 +36,19 @@ namespace Barotrauma private bool isFinished = false; - private bool targetNotFound = false; + /// + /// If the action tags some entities directly (not trying to find targets on the fly), + /// we may be able to determine that targets can not be found even if we'd recheck + /// + private bool cantFindTargets = false; + + /// + /// If the TagAction adds a target predicate (a criteria that keeps finding targets on the fly), + /// we must keep checking if targets have been found to determine if the action can continue or not + /// + private bool mustRecheckTargets = false; + + private bool taggingDone = false; public TagAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { @@ -198,6 +210,7 @@ namespace Barotrauma else { ParentEvent.AddTargetPredicate(tag, predicate); + mustRecheckTargets = true; } } @@ -205,7 +218,7 @@ namespace Barotrauma { if (entities.None()) { - targetNotFound = true; + cantFindTargets = true; return; } if (ChoosePercentage > 0.0f) @@ -230,7 +243,7 @@ namespace Barotrauma { if (entities.None()) { - targetNotFound = true; + cantFindTargets = true; return; } @@ -250,7 +263,7 @@ namespace Barotrauma { if (entities.None()) { - targetNotFound = true; + cantFindTargets = true; return; } ParentEvent.AddTarget(tag, entities.GetRandomUnsynced()); @@ -260,26 +273,29 @@ namespace Barotrauma public override void Update(float deltaTime) { - if (isFinished || targetNotFound) { return; } + if (isFinished || cantFindTargets) { return; } - string[] criteriaSplit = Criteria.Split(';'); - - targetNotFound = false; - foreach (string entry in criteriaSplit) + if (!taggingDone) { - string[] kvp = entry.Split(':'); - Identifier key = kvp[0].Trim().ToIdentifier(); - Identifier value = kvp.Length > 1 ? kvp[1].Trim().ToIdentifier() : Identifier.Empty; - if (Taggers.TryGetValue(key, out Action tagger)) + cantFindTargets = false; + string[] criteriaSplit = Criteria.Split(';'); + foreach (string entry in criteriaSplit) { - tagger(value); - } - else - { - string errorMessage = $"Error in TagAction (event \"{ParentEvent.Prefab.Identifier}\") - unrecognized target criteria \"{key}\"."; - DebugConsole.ThrowError(errorMessage); - GameAnalyticsManager.AddErrorEventOnce($"TagAction.Update:InvalidCriteria_{ParentEvent.Prefab.Identifier}_{key}", GameAnalyticsManager.ErrorSeverity.Error, errorMessage); + string[] kvp = entry.Split(':'); + Identifier key = kvp[0].Trim().ToIdentifier(); + Identifier value = kvp.Length > 1 ? kvp[1].Trim().ToIdentifier() : Identifier.Empty; + if (Taggers.TryGetValue(key, out Action tagger)) + { + tagger(value); + } + else + { + string errorMessage = $"Error in TagAction (event \"{ParentEvent.Prefab.Identifier}\") - unrecognized target criteria \"{key}\"."; + DebugConsole.ThrowError(errorMessage); + GameAnalyticsManager.AddErrorEventOnce($"TagAction.Update:InvalidCriteria_{ParentEvent.Prefab.Identifier}_{key}", GameAnalyticsManager.ErrorSeverity.Error, errorMessage); + } } + taggingDone = true; } if (ContinueIfNoTargetsFound) @@ -288,7 +304,14 @@ namespace Barotrauma } else { - isFinished = !targetNotFound; + if (mustRecheckTargets) + { + isFinished = ParentEvent.GetTargets(Tag).Any(); + } + else + { + isFinished = !cantFindTargets; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index fcf412f27..2722ee955 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -1353,6 +1353,7 @@ namespace Barotrauma if (item.HiddenInGame) { 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; } @@ -1362,6 +1363,15 @@ namespace Barotrauma if (item.Components.Any(c => c is Wire w && w.Connections.Any(c => c != null))) { continue; } itemsToTransfer.Add((item, item.Container)); item.Submarine = null; + + static bool AnyParentInventoryDisableTransfer(Item item) + { + if (item.ParentInventory?.Owner is not Item parentOwner) { return false; } + return HasProblematicComponent(parentOwner) || AnyParentInventoryDisableTransfer(parentOwner); + + static bool HasProblematicComponent(Item it) + => it.Components.Any(static c => c.DontTransferInventoryBetweenSubs); + } } foreach (var (item, container) in itemsToTransfer) { @@ -1369,8 +1379,15 @@ namespace Barotrauma { // Drop the item if it's not inside another item set to be transferred. item.Drop(null, createNetworkEvent: false, setTransform: false); + //dropping items sets the sub, set it back to null + item.Submarine = null; + foreach (var itemContainer in item.GetComponents()) + { + itemContainer.Inventory.FindAllItems((_) => true, recursive: true).ForEach(it => it.Submarine = null); + } } } + System.Diagnostics.Debug.Assert(itemsToTransfer.None(it => it.item.Submarine != null), "Item that was set to be transferred was not removed from the sub!"); currentSub.Info.NoItems = true; } // Serialize the current sub @@ -1408,6 +1425,7 @@ namespace Barotrauma { newContainer = newSub.FindContainerFor(item, onlyPrimary: true, checkTransferConditions: true, allowConnectedSubs: true); } + string newContainerName = newContainer == null ? "(null)" : $"{newContainer.Prefab.Identifier} ({newContainer.Tags})"; if (item.Container == null && (newContainer == null || !newContainer.OwnInventory.TryPutItem(item, user: null, createNetworkEvent: false))) { var cargoContainer = CargoManager.GetOrCreateCargoContainerFor(item.Prefab, spawnHull, ref availableContainers); @@ -1416,13 +1434,16 @@ namespace Barotrauma Vector2 simPos = ConvertUnits.ToSimUnits(CargoManager.GetCargoPos(spawnHull, item.Prefab)); item.SetTransform(simPos, 0.0f, findNewHull: false, setPrevTransform: false); } - else if (cargoContainer.Item.Submarine is Submarine containerSub) + else { - // Use the item's sub in case the sub consists of multiple linked subs. - item.Submarine = containerSub; + if (cargoContainer.Item.Submarine is Submarine containerSub) + { + // Use the item's sub in case the sub consists of multiple linked subs. + item.Submarine = containerSub; + } + newContainerName = cargoContainer.Item.Prefab.Identifier.ToString(); } } - string newContainerName = newContainer == null ? "(null)" : $"{newContainer.Prefab.Identifier} ({newContainer.Tags})"; string msg = "Item transfer log error."; if (oldContainer != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index c73ea2f41..8930f1603 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -71,6 +71,12 @@ namespace Barotrauma.Items.Components protected const float CorrectionDelay = 1.0f; protected CoroutineHandle delayedCorrectionCoroutine; + /// + /// If enabled, the contents of the item are not transferred when the player transfers items between subs. + /// Use this if this component uses item containers in a way where removing the item from the container via external means would cause problems. + /// + public virtual bool DontTransferInventoryBetweenSubs => false; + [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "How long it takes to pick up the item (in seconds).")] public float PickingTime { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index ff6719a73..0ef1b1dc2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -416,7 +416,7 @@ namespace Barotrauma.Items.Components if (requiredItem.UseCondition && suitableIngredient.ConditionPercentage - requiredItem.MinCondition * 100 > 0.0f) { suitableIngredient.Condition -= suitableIngredient.Prefab.Health * requiredItem.MinCondition; - continue; + break; } if (suitableIngredient.OwnInventory != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs index 2e432496a..4c7cb459e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs @@ -82,6 +82,9 @@ namespace Barotrauma.Items.Components private List? lightComponents; + // We don't want the seeds to be transferred to a new submarine as seeds are not supposed to leave the container after they have been planted. + public override bool DontTransferInventoryBetweenSubs => true; + public Planter(Item item, ContentXElement element) : base(item, element) { canBePicked = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs index f02fbebae..2cfd5b3da 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs @@ -26,19 +26,37 @@ namespace Barotrauma.Items.Components public override bool IsActive => true; + // We don't want the components and wires to transfer between subs as it would cause issues. + public override bool DontTransferInventoryBetweenSubs => true; + public Option FindInputOutputConnection(Identifier connectionName) { foreach (CircuitBoxInputConnection input in Inputs) { if (input.Name != connectionName) { continue; } - return Option.Some(input); } foreach (CircuitBoxOutputConnection output in Outputs) { if (output.Name != connectionName) { continue; } + return Option.Some(output); + } + return Option.None; + } + + public Option FindInputOutputConnection(Connection connection) + { + foreach (CircuitBoxInputConnection input in Inputs) + { + if (input.Connection != connection) { continue; } + return Option.Some(input); + } + + foreach (CircuitBoxOutputConnection output in Outputs) + { + if (output.Connection != connection) { continue; } return Option.Some(output); } @@ -338,7 +356,7 @@ namespace Barotrauma.Items.Components return; } - SpawnItem(this, prefab, WireContainer, wire => + SpawnItem(prefab, user: null, container: WireContainer, onSpawned: wire => { AddWireDirect(wireId, prefab, Option.Some(wire), one, two); onItemSpawned(wire); @@ -359,7 +377,7 @@ namespace Barotrauma.Items.Components private void AddWireDirect(ushort id, ItemPrefab prefab, Option backingItem, CircuitBoxConnection one, CircuitBoxConnection two) => Wires.Add(new CircuitBoxWire(this, id, backingItem, one, two, prefab)); - private bool AddComponentInternal(ushort id, ItemPrefab prefab, ItemPrefab usedResource, Vector2 pos, Action onItemSpawned) + private bool AddComponentInternal(ushort id, ItemPrefab prefab, ItemPrefab usedResource, Vector2 pos, Character? user, Action? onItemSpawned) { if (id is ICircuitBoxIdentifiable.NullComponentID) { @@ -373,10 +391,10 @@ namespace Barotrauma.Items.Components return false; } - SpawnItem(this, prefab, ComponentContainer, spawnedItem => + SpawnItem(prefab, user, ComponentContainer, spawnedItem => { Components.Add(new CircuitBoxComponent(id, spawnedItem, pos, this, usedResource)); - onItemSpawned(spawnedItem); + onItemSpawned?.Invoke(spawnedItem); }); OnViewUpdateProjSpecific(); @@ -646,7 +664,7 @@ namespace Barotrauma.Items.Components return Option.None; } - public static void SpawnItem(CircuitBox circuitBox, ItemPrefab prefab, ItemContainer? container, Action onSpawned) + public static void SpawnItem(ItemPrefab prefab, Character? user, ItemContainer? container, Action onSpawned) { if (container is null) { @@ -655,13 +673,27 @@ namespace Barotrauma.Items.Components if (IsInGame()) { - Entity.Spawner?.AddItemToSpawnQueue(prefab, container.Inventory, onSpawned: onSpawned); + Entity.Spawner?.AddItemToSpawnQueue(prefab, container.Inventory, onSpawned: it => + { + AssignWifiComponentTeam(it, user); + onSpawned(it); + }); return; } Item forceSpawnedItem = new Item(prefab, Vector2.Zero, null); container.Inventory.TryPutItem(forceSpawnedItem, null); onSpawned(forceSpawnedItem); + AssignWifiComponentTeam(forceSpawnedItem, user); + + static void AssignWifiComponentTeam(Item item, Character? user) + { + if (user == null) { return; } + foreach (WifiComponent wifiComponent in item.GetComponents()) + { + wifiComponent.TeamID = user.TeamID; + } + } } public static void RemoveItem(Item item) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index af251d6ce..4c4c9c0a8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -98,11 +98,11 @@ namespace Barotrauma } /// Defaults to if null - public int HowManyCanBePut(ItemPrefab itemPrefab, int? maxStackSize = null, float? condition = null) + public int HowManyCanBePut(ItemPrefab itemPrefab, int? maxStackSize = null, float? condition = null, bool ignoreItemsInSlot = false) { if (itemPrefab == null) { return 0; } maxStackSize ??= itemPrefab.GetMaxStackSize(inventory); - if (items.Count > 0) + if (items.Count > 0 && !ignoreItemsInSlot) { if (condition.HasValue) { @@ -517,10 +517,10 @@ namespace Barotrauma return count; } - public virtual int HowManyCanBePut(ItemPrefab itemPrefab, int i, float? condition) + public virtual int HowManyCanBePut(ItemPrefab itemPrefab, int i, float? condition, bool ignoreItemsInSlot = false) { if (i < 0 || i >= slots.Length) { return 0; } - return slots[i].HowManyCanBePut(itemPrefab, condition: condition); + return slots[i].HowManyCanBePut(itemPrefab, condition: condition, ignoreItemsInSlot: ignoreItemsInSlot); } /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 81d0cf2f0..379a616a7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -862,7 +862,25 @@ namespace Barotrauma { get { - return ownInventory?.AllItems ?? Enumerable.Empty(); + if (OwnInventories.Length < 2) + { + if (OwnInventory == null) { yield break; } + + foreach (var item in OwnInventory.AllItems) + { + yield return item; + } + } + else + { + foreach (var inventory in OwnInventories) + { + foreach (var item in inventory.AllItems) + { + yield return item; + } + } + } } } @@ -871,6 +889,8 @@ namespace Barotrauma get { return ownInventory; } } + public readonly ImmutableArray OwnInventories = ImmutableArray.Empty; + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Enable if you want to display the item HUD side by side with another item's HUD, when linked together. " + "Disclaimer: It's possible or even likely that the views block each other, if they were not designed to be viewed together!")] @@ -1192,6 +1212,8 @@ namespace Barotrauma ownInventory = itemContainer.Inventory; } + OwnInventories = GetComponents().Select(ic => ic.Inventory).ToImmutableArray(); + qualityComponent = GetComponent(); IsLadder = GetComponent() != null; @@ -2533,8 +2555,7 @@ namespace Barotrauma foreach (Connection c in connectionPanel.Connections) { if (connectionFilter != null && !connectionFilter.Invoke(c)) { continue; } - var recipients = c.Recipients; - foreach (Connection recipient in recipients) + foreach (Connection recipient in c.Recipients) { var component = recipient.Item.GetComponent(); if (component != null && !connectedComponents.Contains(component)) @@ -2587,9 +2608,20 @@ namespace Barotrauma private void GetConnectedComponentsRecursive(Connection c, HashSet alreadySearched, List connectedComponents, bool ignoreInactiveRelays, bool allowTraversingBackwards = true) where T : ItemComponent { alreadySearched.Add(c); - - var recipients = c.Recipients; - foreach (Connection recipient in recipients) + static IEnumerable GetRecipients(Connection c) + { + foreach (Connection recipient in c.Recipients) + { + yield return recipient; + } + //check circuit box inputs/outputs this connection is connected to + foreach (var circuitBoxConnection in c.CircuitBoxConnections) + { + yield return circuitBoxConnection.Connection; + } + } + + foreach (Connection recipient in GetRecipients(c)) { if (alreadySearched.Contains(recipient)) { continue; } var component = recipient.Item.GetComponent(); @@ -2598,23 +2630,53 @@ namespace Barotrauma connectedComponents.Add(component); } - //connected to a wifi component -> see which other wifi components it can communicate with - var wifiComponent = recipient.Item.GetComponent(); - if (wifiComponent != null && wifiComponent.CanTransmit()) + var circuitBox = recipient.Item.GetComponent(); + if (circuitBox != null) { - foreach (var wifiReceiver in wifiComponent.GetTransmittersInRange()) + //if this is a circuit box, check what the connection is connected to inside the box + var potentialCbConnection = circuitBox.FindInputOutputConnection(recipient); + if (potentialCbConnection.TryUnwrap(out var cbConnection)) { - var receiverConnections = wifiReceiver.Item.Connections; - if (receiverConnections == null) { continue; } - foreach (Connection wifiOutput in receiverConnections) + if (cbConnection is CircuitBoxInputConnection inputConnection) { - if ((wifiOutput.IsOutput == recipient.IsOutput) || alreadySearched.Contains(wifiOutput)) { continue; } - GetConnectedComponentsRecursive(wifiOutput, alreadySearched, connectedComponents, ignoreInactiveRelays, allowTraversingBackwards); + foreach (var connectedTo in inputConnection.ExternallyConnectedTo) + { + if (alreadySearched.Contains(connectedTo.Connection)) { continue; } + CheckRecipient(connectedTo.Connection); + } + } + else + { + foreach (var connectedFrom in cbConnection.ExternallyConnectedFrom) + { + if (alreadySearched.Contains(connectedFrom.Connection) || !allowTraversingBackwards) { continue; } + CheckRecipient(connectedFrom.Connection); + } } } } + CheckRecipient(recipient); - recipient.Item.GetConnectedComponentsRecursive(recipient, alreadySearched, connectedComponents, ignoreInactiveRelays, allowTraversingBackwards); + void CheckRecipient(Connection recipient) + { + //connected to a wifi component -> see which other wifi components it can communicate with + var wifiComponent = recipient.Item.GetComponent(); + if (wifiComponent != null && wifiComponent.CanTransmit()) + { + foreach (var wifiReceiver in wifiComponent.GetTransmittersInRange()) + { + var receiverConnections = wifiReceiver.Item.Connections; + if (receiverConnections == null) { continue; } + foreach (Connection wifiOutput in receiverConnections) + { + if ((wifiOutput.IsOutput == recipient.IsOutput) || alreadySearched.Contains(wifiOutput)) { continue; } + GetConnectedComponentsRecursive(wifiOutput, alreadySearched, connectedComponents, ignoreInactiveRelays, allowTraversingBackwards); + } + } + } + + recipient.Item.GetConnectedComponentsRecursive(recipient, alreadySearched, connectedComponents, ignoreInactiveRelays, allowTraversingBackwards); + } } if (ignoreInactiveRelays) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs index 0b8aea197..bc8f2f41f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs @@ -58,12 +58,12 @@ namespace Barotrauma return itemPrefab != null && slots[i].CanBePut(itemPrefab, condition, quality) && slots[i].Items.Count < container.GetMaxStackSize(i); } - public override int HowManyCanBePut(ItemPrefab itemPrefab, int i, float? condition) + public override int HowManyCanBePut(ItemPrefab itemPrefab, int i, float? condition, bool ignoreItemsInSlot = false) { if (itemPrefab == null) { return 0; } if (i < 0 || i >= slots.Length) { return 0; } if (!container.CanBeContained(itemPrefab, i)) { return 0; } - return slots[i].HowManyCanBePut(itemPrefab, maxStackSize: Math.Min(itemPrefab.GetMaxStackSize(this), container.GetMaxStackSize(i)), condition); + return slots[i].HowManyCanBePut(itemPrefab, maxStackSize: Math.Min(itemPrefab.GetMaxStackSize(this), container.GetMaxStackSize(i)), condition, ignoreItemsInSlot); } public override bool IsFull(bool takeStacksIntoAccount = false) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index dd3abc0ad..42cefb7bb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -843,7 +843,7 @@ namespace Barotrauma } private int maxStackSizeCharacterInventory; - [Serialize(-1, IsPropertySaveable.No)] + [Serialize(-1, IsPropertySaveable.No, description: "Maximum stack size when the item is in a character inventory.")] public int MaxStackSizeCharacterInventory { get { return maxStackSizeCharacterInventory; } @@ -851,7 +851,9 @@ namespace Barotrauma } private int maxStackSizeHoldableOrWearableInventory; - [Serialize(-1, IsPropertySaveable.No)] + [Serialize(-1, IsPropertySaveable.No, description: + "Maximum stack size when the item is inside a holdable or wearable item. "+ + "If not set, defaults to MaxStackSizeCharacterInventory.")] public int MaxStackSizeHoldableOrWearableInventory { get { return maxStackSizeHoldableOrWearableInventory; } @@ -864,15 +866,20 @@ namespace Barotrauma { return maxStackSizeCharacterInventory; } - else if (maxStackSizeHoldableOrWearableInventory > 0 && - inventory?.Owner is Item item && (item.GetComponent() != null || item.GetComponent() != null)) + else if (inventory?.Owner is Item item && + (item.GetComponent() is { Attachable: false } || item.GetComponent() != null)) { - return maxStackSizeHoldableOrWearableInventory; - } - else - { - return maxStackSize; + if (maxStackSizeHoldableOrWearableInventory > 0) + { + return maxStackSizeHoldableOrWearableInventory; + } + else if (maxStackSizeCharacterInventory > 0) + { + //if maxStackSizeHoldableOrWearableInventory is not set, it defaults to maxStackSizeCharacterInventory + return maxStackSizeCharacterInventory; + } } + return maxStackSize; } [Serialize(false, IsPropertySaveable.No)] @@ -880,7 +887,7 @@ namespace Barotrauma public ImmutableHashSet AllowDroppingOnSwapWith { get; private set; } - [Serialize(false, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, "If enabled, the item is not transferred when the player transfers items between subs.")] public bool DontTransferBetweenSubs { get; private set; } [Serialize(true, IsPropertySaveable.No)] @@ -1032,6 +1039,7 @@ namespace Barotrauma var levelCommonness = new Dictionary(); var levelQuantity = new Dictionary(); + List loadedRecipes = new List(); foreach (ContentXElement subElement in ConfigElement.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -1114,9 +1122,10 @@ namespace Barotrauma var newRecipe = new FabricationRecipe(subElement, Identifier); if (fabricationRecipes.TryGetValue(newRecipe.RecipeHash, out var prevRecipe)) { + int prevRecipeIndex = loadedRecipes.IndexOf(prevRecipe); DebugConsole.ThrowError( $"Error in item prefab \"{ToString()}\": " + - $"{prevRecipe.TargetItemPrefabIdentifier} has the same hash as {newRecipe.TargetItemPrefabIdentifier}. " + + $"Fabrication recipe #{loadedRecipes.Count + 1} has the same hash as recipe #{prevRecipeIndex + 1}. This is most likely caused by identical, duplicate recipes." + $"This will cause issues with fabrication." ); } @@ -1124,6 +1133,7 @@ namespace Barotrauma { fabricationRecipes.Add(newRecipe.RecipeHash, newRecipe); } + loadedRecipes.Add(newRecipe); break; case "preferredcontainer": var preferredContainer = new PreferredContainer(subElement); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index db32a7ec5..3fab10249 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -130,10 +130,17 @@ namespace Barotrauma /// /// When set to true, the explosion don't deal less damage when the target is behind a solid object. /// - public bool IgnoreCover - { - get; set; - } + public bool IgnoreCover { get; set; } + + /// + /// Does the damage from the explosion decrease with distance from the origin of the explosion? + /// + public bool DistanceFalloff { get; set; } = true; + + /// + /// Structures that don't count as "cover" that reduces damage from the explosion. Only relevant if IgnoreCover is set to false. + /// + public IEnumerable IgnoredCover; /// /// How long the light source created by the explosion lasts. @@ -311,12 +318,15 @@ namespace Barotrauma if (!MathUtils.NearlyEqual(Attack.GetStructureDamage(1.0f), 0.0f) || !MathUtils.NearlyEqual(Attack.GetLevelWallDamage(1.0f), 0.0f)) { - RangedStructureDamage(worldPosition, displayRange, Attack.GetStructureDamage(1.0f), Attack.GetLevelWallDamage(1.0f), attacker, IgnoredSubmarines, Attack.EmitStructureDamageParticles); + RangedStructureDamage(worldPosition, displayRange, Attack.GetStructureDamage(1.0f), Attack.GetLevelWallDamage(1.0f), attacker, + IgnoredSubmarines, + Attack.EmitStructureDamageParticles, + DistanceFalloff); } if (BallastFloraDamage > 0.0f) { - RangedBallastFloraDamage(worldPosition, displayRange, BallastFloraDamage, attacker); + RangedBallastFloraDamage(worldPosition, displayRange, BallastFloraDamage, attacker, DistanceFalloff); } if (EmpStrength > 0.0f) @@ -326,7 +336,7 @@ namespace Barotrauma { float distSqr = Vector2.DistanceSquared(item.WorldPosition, worldPosition); if (distSqr > displayRangeSqr) { continue; } - float distFactor = CalculateDistanceFactor(distSqr, displayRange); + float distFactor = DistanceFalloff ? CalculateDistanceFactor(distSqr, displayRange) : 1.0f; //damage repairable power-consuming items var powered = item.GetComponent(); @@ -362,7 +372,10 @@ namespace Barotrauma float distSqr = Vector2.DistanceSquared(item.WorldPosition, worldPosition); if (distSqr > displayRangeSqr) { continue; } - float distFactor = 1.0f - (float)Math.Sqrt(distSqr) / displayRange; + float distFactor = + DistanceFalloff ? + 1.0f - (float)Math.Sqrt(distSqr) / displayRange : + 1.0f; //repair repairable items if (item.Repairables.Any()) { @@ -415,13 +428,16 @@ namespace Barotrauma if (item.Prefab.DamagedByExplosions && !item.Indestructible) { - float distFactor = 1.0f - dist / displayRange; + float distFactor = + DistanceFalloff ? + 1.0f - dist / displayRange : + 1.0f; float damageAmount = Attack.GetItemDamage(1.0f, item.Prefab.ExplosionDamageMultiplier); Vector2 explosionPos = worldPosition; if (item.Submarine != null) { explosionPos -= item.Submarine.Position; } - damageAmount *= GetObstacleDamageMultiplier(ConvertUnits.ToSimUnits(explosionPos), worldPosition, item.SimPosition); + damageAmount *= GetObstacleDamageMultiplier(ConvertUnits.ToSimUnits(explosionPos), worldPosition, item.SimPosition, IgnoredCover); item.Condition -= damageAmount * distFactor; } } @@ -482,12 +498,15 @@ namespace Barotrauma if (dist > attack.Range) { continue; } - float distFactor = 1.0f - dist / attack.Range; + float distFactor = + DistanceFalloff ? + 1.0f - dist / attack.Range : + 1.0f; //solid obstacles between the explosion and the limb reduce the effect of the explosion if (!IgnoreCover) { - distFactor *= GetObstacleDamageMultiplier(explosionPos, worldPosition, limb.SimPosition); + distFactor *= GetObstacleDamageMultiplier(explosionPos, worldPosition, limb.SimPosition, IgnoredCover); } if (distFactor > 0) { @@ -602,7 +621,8 @@ namespace Barotrauma /// /// Returns a dictionary where the keys are the structures that took damage and the values are the amount of damage taken /// - public static Dictionary RangedStructureDamage(Vector2 worldPosition, float worldRange, float damage, float levelWallDamage, Character attacker = null, IEnumerable ignoredSubmarines = null, bool emitWallDamageParticles = true) + public static Dictionary RangedStructureDamage(Vector2 worldPosition, float worldRange, float damage, float levelWallDamage, Character attacker = null, IEnumerable ignoredSubmarines = null, + bool emitWallDamageParticles = true, bool distanceFalloff = true) { float dist = 600.0f; damagedStructures.Clear(); @@ -616,7 +636,10 @@ namespace Barotrauma { for (int i = 0; i < structure.SectionCount; i++) { - float distFactor = 1.0f - (Vector2.Distance(structure.SectionPosition(i, true), worldPosition) / worldRange); + float distFactor = + distanceFalloff ? + 1.0f - (Vector2.Distance(structure.SectionPosition(i, true), worldPosition) / worldRange) : + 1.0f; if (distFactor <= 0.0f) { continue; } structure.AddDamage(i, damage * distFactor, attacker, emitParticles: emitWallDamageParticles); @@ -680,7 +703,7 @@ namespace Barotrauma return damagedStructures; } - public static void RangedBallastFloraDamage(Vector2 worldPosition, float worldRange, float damage, Character attacker = null) + public static void RangedBallastFloraDamage(Vector2 worldPosition, float worldRange, float damage, Character attacker = null, bool distanceFalloff = true) { List ballastFlorae = new List(); @@ -698,7 +721,10 @@ namespace Barotrauma float branchDist = Vector2.Distance(branchWorldPos, worldPosition); if (branchDist < worldRange) { - float distFactor = 1.0f - (branchDist / worldRange); + float distFactor = + distanceFalloff ? + 1.0f - (branchDist / worldRange) : + 1.0f; if (distFactor <= 0.0f) { return; } Vector2 explosionPos = worldPosition; @@ -715,7 +741,7 @@ namespace Barotrauma } } - private static float GetObstacleDamageMultiplier(Vector2 explosionSimPos, Vector2 explosionWorldPos, Vector2 targetSimPos) + private static float GetObstacleDamageMultiplier(Vector2 explosionSimPos, Vector2 explosionWorldPos, Vector2 targetSimPos, IEnumerable ignoredCover = null) { float damageMultiplier = 1.0f; var obstacles = Submarine.PickBodies(targetSimPos, explosionSimPos, collisionCategory: Physics.CollisionItem | Physics.CollisionItemBlocking | Physics.CollisionWall); @@ -728,6 +754,10 @@ namespace Barotrauma } else if (body.UserData is Structure structure) { + if (ignoredCover != null) + { + if (ignoredCover.Contains(structure)) { continue; } + } int sectionIndex = structure.FindSectionIndex(explosionWorldPos, world: true, clamp: true); if (structure.SectionBodyDisabled(sectionIndex)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index 4c93da0cc..40e897dea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -109,6 +109,8 @@ namespace Barotrauma public float Size => IsHorizontal ? Rect.Height : Rect.Width; + public float PressureDistributionSpeed => Size / 100.0f * open; + private Door connectedDoor; public Door ConnectedDoor { @@ -427,11 +429,9 @@ namespace Barotrauma if (hull1.WaterVolume <= 0.0 && hull2.WaterVolume <= 0.0) { return; } - float size = IsHorizontal ? rect.Height : rect.Width; - //a variable affecting the water flow through the gap //the larger the gap is, the faster the water flows - float sizeModifier = size / 100.0f * open; + float sizeModifier = Size / 100.0f * open; //horizontal gap (such as a regular door) if (IsHorizontal) @@ -440,7 +440,7 @@ namespace Barotrauma float delta = 0.0f; //water level is above the lower boundary of the gap - if (Math.Max(hull1.Surface + hull1.WaveY[hull1.WaveY.Length - 1], hull2.Surface + subOffset.Y + hull2.WaveY[0]) > rect.Y - size) + if (Math.Max(hull1.Surface + hull1.WaveY[hull1.WaveY.Length - 1], hull2.Surface + subOffset.Y + hull2.WaveY[0]) > rect.Y - Size) { int dir = (hull1.Pressure > hull2.Pressure + subOffset.Y) ? 1 : -1; @@ -569,27 +569,35 @@ namespace Barotrauma if (open > 0.0f) { - if (hull1.WaterVolume > hull1.Volume / Hull.MaxCompress && hull2.WaterVolume > hull2.Volume / Hull.MaxCompress) + if (hull1.WaterVolume > hull1.Volume / Hull.MaxCompress && + hull2.WaterVolume > hull2.Volume / Hull.MaxCompress) { + //both hulls full -> distribute pressure float avgLethality = (hull1.LethalPressure + hull2.LethalPressure) / 2.0f; - hull1.LethalPressure = avgLethality; - hull2.LethalPressure = avgLethality; + changePressure(hull1, avgLethality, PressureDistributionSpeed, deltaTime); + changePressure(hull2, avgLethality, PressureDistributionSpeed, deltaTime); + + static void changePressure(Hull hull, float target, float speed, float deltaTime) + { + float diff = target - hull.LethalPressure; + float maxChange = Hull.PressureBuildUpSpeed * speed * deltaTime; + hull.LethalPressure += MathHelper.Clamp(diff, -maxChange, maxChange); + } } else { - hull1.LethalPressure -= Hull.PressureDropSpeed * deltaTime; - hull2.LethalPressure -= Hull.PressureDropSpeed * deltaTime; + //either hull not full -> pressure drops + hull1.LethalPressure -= Hull.PressureDropSpeed * PressureDistributionSpeed * deltaTime; + hull2.LethalPressure -= Hull.PressureDropSpeed * PressureDistributionSpeed * deltaTime; } } } void UpdateRoomToOut(float deltaTime, Hull hull1) { - float size = IsHorizontal ? rect.Height : rect.Width; - //a variable affecting the water flow through the gap //the larger the gap is, the faster the water flows - float sizeModifier = size * open * open; + float sizeModifier = Size * open * open; float delta = 500.0f * sizeModifier * deltaTime; @@ -642,7 +650,7 @@ namespace Barotrauma } else { - hull1.LethalPressure += ((Submarine != null && Submarine.AtDamageDepth) ? 100.0f : Hull.PressureBuildUpSpeed) * deltaTime; + hull1.LethalPressure += ((Submarine != null && Submarine.AtDamageDepth) ? 100.0f : Hull.PressureBuildUpSpeed) * PressureDistributionSpeed * deltaTime; } } else @@ -657,7 +665,7 @@ namespace Barotrauma } if (hull1.WaterVolume >= hull1.Volume / Hull.MaxCompress) { - hull1.LethalPressure += ((Submarine != null && Submarine.AtDamageDepth) ? 100.0f : Hull.PressureBuildUpSpeed) * deltaTime; + hull1.LethalPressure += ((Submarine != null && Submarine.AtDamageDepth) ? 100.0f : Hull.PressureBuildUpSpeed) * PressureDistributionSpeed * deltaTime; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 7de5bf629..7f899899a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -1016,7 +1016,11 @@ namespace Barotrauma if (waterVolume < Volume) { - LethalPressure -= PressureDropSpeed * deltaTime; + //pressure drop speed is inversely proportionate to water percentage + //= pressure drops very fast if the hull is nowhere near full + float waterVolumeFactor = Math.Max((100.0f - WaterPercentage) / 10.0f, 1.0f); + LethalPressure -= + PressureDropSpeed * waterVolumeFactor * deltaTime; if (WaterVolume <= 0.0f) { #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index 3e3d51b4c..77c3a7cf0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -365,7 +365,7 @@ namespace Barotrauma if (FinishedEvents.Any()) { var finishedEventsElement = new XElement(nameof(FinishedEvents)); - foreach (var (set, count) in FinishedEvents.DistinctBy(f => f.Key.Identifier)) + foreach (var (set, count) in FinishedEvents) { var element = new XElement(nameof(FinishedEvents), new XAttribute("set", set.Identifier), diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index a2abd562e..2d4ce70e3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -1245,7 +1245,7 @@ namespace Barotrauma private static void CreateWallDamageExplosion(Gap gap, Character attacker) { - const float explosionRange = 750.0f; + const float explosionRange = 500.0f; float explosionStrength = gap.Open; var linkedHull = gap.linkedTo.FirstOrDefault() as Hull; @@ -1264,20 +1264,22 @@ namespace Barotrauma if (explosionOnBroken == null) { - explosionOnBroken = new Explosion(explosionRange, force: 10.0f, damage: 0.0f, structureDamage: 0.0f, itemDamage: 0.0f); + explosionOnBroken = new Explosion(explosionRange, force: 5.0f, damage: 0.0f, structureDamage: 0.0f, itemDamage: 0.0f); if (AfflictionPrefab.Prefabs.TryGet("lacerations".ToIdentifier(), out AfflictionPrefab lacerations)) { - explosionOnBroken.Attack.Afflictions.Add(lacerations.Instantiate(3.0f), null); + explosionOnBroken.Attack.Afflictions.Add(lacerations.Instantiate(5.0f), null); } else { - explosionOnBroken.Attack.Afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(3.0f), null); + explosionOnBroken.Attack.Afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(5.0f), null); } - explosionOnBroken.IgnoreCover = true; + explosionOnBroken.IgnoreCover = false; explosionOnBroken.OnlyInside = true; + explosionOnBroken.DistanceFalloff = false; explosionOnBroken.DisableParticles(); } + explosionOnBroken.IgnoredCover = gap.ConnectedWall?.ToEnumerable(); explosionOnBroken.Attack.Range = explosionRange * gap.Open; explosionOnBroken.Attack.DamageMultiplier = explosionStrength; explosionOnBroken.Attack.Stun = MathHelper.Clamp(explosionStrength, 0.5f, 1.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 0f9370041..3af1f3aca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -1606,28 +1606,7 @@ namespace Barotrauma } } } - if (removeItem) - { - for (int i = 0; i < targets.Count; i++) - { - if (targets[i] is Item item) { Entity.Spawner?.AddItemToRemoveQueue(item); } - } - } - if (removeCharacter) - { - for (int i = 0; i < targets.Count; i++) - { - var target = targets[i]; - if (target is Character character) - { - Entity.Spawner?.AddEntityToRemoveQueue(character); - } - else if (target is Limb limb) - { - Entity.Spawner?.AddEntityToRemoveQueue(limb.character); - } - } - } + if (breakLimb || hideLimb) { for (int i = 0; i < targets.Count; i++) @@ -2292,6 +2271,30 @@ namespace Barotrauma ApplyProjSpecific(deltaTime, entity, targets, hull, position, playSound: true); + //do this last - the entities spawned by the effect might need the entity for something, so better to remove it last + if (removeItem) + { + for (int i = 0; i < targets.Count; i++) + { + if (targets[i] is Item item) { Entity.Spawner?.AddItemToRemoveQueue(item); } + } + } + if (removeCharacter) + { + for (int i = 0; i < targets.Count; i++) + { + var target = targets[i]; + if (target is Character character) + { + Entity.Spawner?.AddEntityToRemoveQueue(character); + } + else if (target is Limb limb) + { + Entity.Spawner?.AddEntityToRemoveQueue(limb.character); + } + } + } + if (oneShot) { Disabled = true; diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 4210b90eb..f325714d8 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,29 @@ +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.1.19.3 +------------------------------------------------------------------------------------------------------------------------------------------------- + +- Fixed shrapnel from flak cannon ammo always launching upwards. +- Fixed wall damage shrapnel going through walls and being a little excessive overall. +- Fixes to pressure distribution logic: fixes pressure sometimes being lethal in breached rooms that weren't full of water. +- Fixed clients not regaining control of their braindead character after rejoining if the character's taken any amount of damage while braindead. +- Fixes to inconsistent stack sizes (e.g. certain ammo types stacking up to 12 in character inventories, but only 8 holdable/wearable items like backpacks and toolbelts. +- Fixed console errors when switching subs with circuit boxes on board. +- Fixed items that are inside a container that doesn't get transferred getting duplicated during item transfer when switching subs. +- Fixed bots not cleaning up circuit boxes from the floor. +- Fixed stack size being displayed incorrectly in the tooltip when dragging a stack of items to an already-occupied slot. +- Fixed nav terminal's docking button not working if the signal is routed from the terminal to the docking port through a circuit box. +- Fixed double-clicking a component while a circuit box is equipped making the component vanish inside the circuit box. +- Fixed chat messages sent when accusing someone as a traitor not triggering the spam filter. +- Fixed connection names not being translated in circuit boxes (always showed up in English). +- Fixed right-side crew panel overlapping with the mission panel in the PvP mode round summary. +- Fixed depleted and fulgurium fuel rods stacking up to 32 (should be 8 like all other fuel rods). + +Modding: +- Added "UseHumanAI" property to characters (can be used to enable the human AI on characters other than humans). While using the human AI on non-humans isn't a fully supported or tested feature, it was previously possible to do that by creating a human prefab using a different species than human, but that no longer worked as of the Treacherous Tides update. +- Attachable holdable items don't count as "HoldableOrWearableInventories". Fixes e.g. tanks only stacking up to 1 in things like movable cabinets and shelves. Does not affect any vanilla content. +- Fixed fabricator reducing the condition of all the available ingredients if the crafting recipe reduces condition instead of consuming the whole item. +- Fixed TagAction continuing in some situations when it can't find targets, even if it's been configured not to with the "ContinueIfNoTargetsFound" attribute. + ------------------------------------------------------------------------------------------------------------------------------------------------- v1.1.18.1 ------------------------------------------------------------------------------------------------------------------------------------------------- @@ -336,25 +362,6 @@ Fixes: - Fixed crashing when you e.g. use a pet from some mod in the campaign, disable the mod and reload the save. - Waypoint adjustments to most submarines, outposts, wrecks, and beacons. Especially on ladders. Should take care of the remaining AI issues on ladders (the old subs in the saves don't get updated, but the fixes apply to new subs that you don't yet own. And ofc all the subs in a new game!) ---------------------------------------------------------------------------------------------------------- -v1.0.21.0 ---------------------------------------------------------------------------------------------------------- - -Fixes: -- Fixed LOS effect sometimes "lagging behind" when the sub is moving fast. -- Fixed some minor visual issues (occasional jitter/flickering) on the LOS effect. -- Fixed some issues in the bot AI that we're causing a large performance hit particularly in situations when there's lots of bots in a sub with leaks. -- Fixed bots abandoning their orders (such as operating a turret) if the room is unsafe (e.g. flooded). -- Fixed an issue in character syncing that occasionally caused disconnects with the error message "Exception thrown while reading segment EntityPosition, tried to read too much data from segment". -- Fixed wires set to be hidden in-game (e.g. invisible circuits built outside the sub) being visible on the Electrician's Goggles. -- Fixed an issue with level resources that caused crashes with certain mods (e.g. ones that include subs with piezo crystals). -- Fixed NPCs waiting on some outpost modules never reaching their targets, causing peculiar behavior. -- Fixed waypoints sometimes not getting connected between outpost modules if there's a very short hallway between them. Addresses some cities missing connections between waypoints, causing AI to be unable to navigate through the modules. -- Fixed some UI layout issues (most noticeably, ultra-wide crew list) on certain resolutions like 3440x1440. -- Fixed campaign saves occasionally failing to load with the error "an item with the same key has already been added". Seemed to only occur when using certain mods. -- Fixed crashing when you e.g. use a pet from some mod in the campaign, disable the mod and reload the save. -- Waypoint adjustments to most submarines, outposts, wrecks, and beacons. Especially on ladders. Should take care of the remaining AI issues on ladders (the old subs in the saves don't get updated, but the fixes apply to new subs that you don't yet own. And ofc all the subs in a new game!) - --------------------------------------------------------------------------------------------------------- v1.0.20.1 --------------------------------------------------------------------------------------------------------- From 71b710641b6b3df60d03adf0dab772b6f5def528 Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Fri, 10 Nov 2023 18:06:38 +0200 Subject: [PATCH 15/53] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index a9673f38d..8b07571b6 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -74,6 +74,7 @@ body: 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.1.19.3 (Treacherous Tides Hotfix 2) + - v1.2.1.0 (Unstable) - Other validations: required: true From 8aa4257331110900dca074ad4a62f2d513f700fe Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Fri, 24 Nov 2023 20:20:14 +0200 Subject: [PATCH 16/53] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 8b07571b6..ede3443dc 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -74,7 +74,7 @@ body: 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.1.19.3 (Treacherous Tides Hotfix 2) - - v1.2.1.0 (Unstable) + - v1.2.3.0 (Unstable) - Other validations: required: true From af8cc89fce7ab8e854fac977345aedc0132530d7 Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Tue, 5 Dec 2023 17:53:27 +0200 Subject: [PATCH 17/53] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index ede3443dc..872515cac 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -74,7 +74,7 @@ body: 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.1.19.3 (Treacherous Tides Hotfix 2) - - v1.2.3.0 (Unstable) + - v1.2.5.0 (Unstable) - Other validations: required: true From b91e85559da72263c1b64a13d31278842586e5a0 Mon Sep 17 00:00:00 2001 From: Regalis11 Date: Thu, 14 Dec 2023 16:11:27 +0200 Subject: [PATCH 18/53] v1.2.6.0 (Winter Update) --- .../BarotraumaClient/ClientSource/Camera.cs | 19 +- .../ClientSource/Characters/Character.cs | 30 +- .../ClientSource/Characters/CharacterHUD.cs | 2 +- .../ClientSource/Characters/CharacterInfo.cs | 14 +- .../Characters/Health/CharacterHealth.cs | 2 + .../ClientSource/Characters/Limb.cs | 2 +- .../ClientSource/DebugConsole.cs | 45 ++- .../Events/EventActions/ConversationAction.cs | 3 +- .../EventActions/EventObjectiveAction.cs | 10 +- ...lHighlightAction.cs => HighlightAction.cs} | 17 +- .../ClientSource/Events/EventManager.cs | 11 +- .../Events/Missions/CargoMission.cs | 3 +- .../ClientSource/Events/Missions/Mission.cs | 8 +- .../ClientSource/GUI/CrewManagement.cs | 56 +-- .../BarotraumaClient/ClientSource/GUI/GUI.cs | 18 +- .../ClientSource/GUI/GUIComponent.cs | 21 +- .../ClientSource/GUI/GUIMessageBox.cs | 12 + .../ClientSource/GUI/GUINumberInput.cs | 67 ++-- .../ClientSource/GUI/GUIPrefab.cs | 9 +- .../ClientSource/GUI/GUIStyle.cs | 72 ++-- .../ClientSource/GUI/GUITextBlock.cs | 9 +- .../ClientSource/GUI/GUITextBox.cs | 4 + .../ClientSource/GUI/RectTransform.cs | 96 ++++- .../ClientSource/GUI/Store.cs | 85 +++-- .../ClientSource/GUI/SubmarineSelection.cs | 2 +- .../ClientSource/GUI/TabMenu.cs | 9 +- .../ClientSource/GUI/UISprite.cs | 2 +- .../ClientSource/GUI/UpgradeStore.cs | 6 +- .../BarotraumaClient/ClientSource/GameMain.cs | 6 +- .../ClientSource/GameSession/CargoManager.cs | 2 +- .../GameSession/GameModes/CampaignMode.cs | 21 +- .../GameModes/MultiPlayerCampaign.cs | 19 +- .../GameModes/SinglePlayerCampaign.cs | 9 +- .../ClientSource/GameSession/GameSession.cs | 12 +- .../GameSession/ObjectiveManager.cs | 20 +- .../ClientSource/GameSession/RoundSummary.cs | 16 +- .../ClientSource/Items/CharacterInventory.cs | 12 +- .../Items/Components/GeneticMaterial.cs | 6 +- .../Items/Components/ItemComponent.cs | 28 +- .../Items/Components/ItemContainer.cs | 16 +- .../Items/Components/ItemLabel.cs | 2 +- .../Items/Components/LightComponent.cs | 2 +- .../Items/Components/Machines/Engine.cs | 6 +- .../Items/Components/Machines/Fabricator.cs | 104 +++++- .../Items/Components/Machines/Sonar.cs | 65 ++-- .../Items/Components/Machines/Steering.cs | 19 +- .../Items/Components/RepairTool.cs | 2 +- .../Components/Signal/CustomInterface.cs | 7 +- .../Items/Components/StatusHUD.cs | 6 +- .../ClientSource/Items/Components/Turret.cs | 16 + .../ClientSource/Items/Inventory.cs | 86 +++-- .../ClientSource/Items/Item.cs | 177 +++++++--- .../ClientSource/Items/ItemPrefab.cs | 28 +- .../BarotraumaClient/ClientSource/Map/Hull.cs | 19 +- .../ClientSource/Map/ItemAssemblyPrefab.cs | 2 +- .../BackgroundCreatureManager.cs | 2 +- .../ClientSource/Map/Levels/Level.cs | 9 +- .../Map/Levels/LevelObjects/LevelObject.cs | 2 + .../Levels/LevelObjects/LevelObjectManager.cs | 9 +- .../ClientSource/Map/Lights/ConvexHull.cs | 21 ++ .../ClientSource/Map/Lights/LightManager.cs | 14 + .../ClientSource/Map/Lights/LightSource.cs | 130 +++++-- .../ClientSource/Map/Map/Map.cs | 10 +- .../ClientSource/Map/Map/Radiation.cs | 2 +- .../ClientSource/Map/MapEntity.cs | 166 ++++++--- .../ClientSource/Map/MapEntityPrefab.cs | 2 +- .../ClientSource/Map/RoundSound.cs | 20 +- .../ClientSource/Map/Structure.cs | 54 ++- .../ClientSource/Map/StructurePrefab.cs | 21 +- .../ClientSource/Map/Submarine.cs | 14 +- .../ClientSource/Map/SubmarinePreview.cs | 28 +- .../ClientSource/Networking/Client.cs | 6 +- .../ClientSource/Networking/GameClient.cs | 27 +- .../Networking/ServerList/ServerInfo.cs | 108 ++++-- .../ClientSource/Networking/ServerSettings.cs | 4 + .../ClientSource/Particles/ParticleEmitter.cs | 22 +- .../ClientSource/Particles/ParticlePrefab.cs | 3 +- .../BarotraumaClient/ClientSource/Program.cs | 13 +- .../ClientSource/Screens/CampaignUI.cs | 8 +- .../ClientSource/Screens/GameScreen.cs | 20 +- .../ClientSource/Screens/LevelEditorScreen.cs | 12 +- .../ClientSource/Screens/MainMenuScreen.cs | 116 ++++-- .../ClientSource/Screens/NetLobbyScreen.cs | 48 ++- .../ServerListScreen/ServerListScreen.cs | 188 +++++++++- .../ClientSource/Screens/SubEditorScreen.cs | 326 +++++++++-------- .../Serialization/SerializableEntityEditor.cs | 31 +- .../ClientSource/Settings/SettingsMenu.cs | 16 +- .../ClientSource/Sounds/OggSound.cs | 13 +- .../ClientSource/Sounds/Sound.cs | 6 +- .../ClientSource/Sounds/SoundChannel.cs | 15 +- .../ClientSource/Sounds/SoundManager.cs | 57 +-- .../ClientSource/Sounds/SoundPlayer.cs | 5 + .../ClientSource/Sounds/SoundPrefab.cs | 2 + .../ClientSource/SpamServerFilter.cs | 330 ++++++++++++++++++ .../ClientSource/Sprite/Sprite.cs | 54 ++- .../StatusEffects/StatusEffect.cs | 2 +- .../ClientSource/Steam/BulkDownloader.cs | 36 +- .../ClientSource/Steam/Lobby.cs | 4 + .../ClientSource/Steam/Workshop.cs | 23 +- .../ClientSource/SubEditorCommands.cs | 83 +++-- .../Utils/{Quad.cs => GraphicsQuad.cs} | 2 +- .../ClientSource/Utils/SpriteRecorder.cs | 11 +- .../ClientSource/Utils/TextureLoader.cs | 9 +- .../ClientSource/Utils/WikiImage.cs | 2 +- .../BarotraumaClient/LinuxClient.csproj | 2 +- Barotrauma/BarotraumaClient/MacClient.csproj | 2 +- .../BarotraumaClient/WindowsClient.csproj | 2 +- .../BarotraumaServer/LinuxServer.csproj | 2 +- Barotrauma/BarotraumaServer/MacServer.csproj | 2 +- .../ServerSource/Characters/CharacterInfo.cs | 4 +- .../ServerSource/DebugConsole.cs | 145 +++++--- .../Events/EventActions/EventLogAction.cs | 5 +- .../Events/EventActions/HighlightAction.cs | 24 ++ .../ServerSource/Events/Missions/Mission.cs | 8 +- .../BarotraumaServer/ServerSource/GameMain.cs | 25 +- .../ServerSource/GameSession/CargoManager.cs | 32 +- .../GameSession/GameModes/CampaignMode.cs | 12 +- .../GameModes/CharacterCampaignData.cs | 16 +- .../GameModes/MultiPlayerCampaign.cs | 107 ++++-- .../Items/Components/Signal/CircuitBox.cs | 3 +- .../ServerSource/Items/Inventory.cs | 8 +- .../ServerSource/Items/Item.cs | 14 + .../ServerSource/Items/ItemEventData.cs | 21 +- .../BarotraumaServer/ServerSource/Map/Hull.cs | 4 +- .../ServerSource/Networking/GameServer.cs | 89 +++-- .../ServerEntityEventManager.cs | 5 +- .../ServerSource/Networking/RespawnManager.cs | 2 +- .../ServerSource/Networking/ServerSettings.cs | 2 +- .../BarotraumaServer/ServerSource/Program.cs | 16 +- .../ServerSource/Steam/SteamManager.cs | 15 +- .../ServerSource/Traitors/TraitorManager.cs | 23 +- .../BarotraumaServer/WindowsServer.csproj | 2 +- .../Data/campaignsettings.xml | 12 +- .../Characters/AI/AIController.cs | 7 +- .../Characters/AI/EnemyAIController.cs | 28 +- .../Characters/AI/HumanAIController.cs | 129 +++---- .../Characters/AI/Objectives/AIObjective.cs | 11 - .../Objectives/AIObjectiveCheckStolenItems.cs | 160 +++++++++ .../AI/Objectives/AIObjectiveCombat.cs | 6 +- .../Objectives/AIObjectiveEscapeHandcuffs.cs | 10 +- .../AI/Objectives/AIObjectiveFindThieves.cs | 152 ++++++++ .../AI/Objectives/AIObjectiveIdle.cs | 2 +- .../AI/Objectives/AIObjectiveLoop.cs | 2 +- .../AI/Objectives/AIObjectiveManager.cs | 4 + .../SharedSource/Characters/AI/Order.cs | 17 +- .../Characters/Animation/AnimController.cs | 3 +- .../Animation/HumanoidAnimController.cs | 255 ++++++++------ .../Characters/Animation/Ragdoll.cs | 144 +++++--- .../SharedSource/Characters/Attack.cs | 68 +++- .../SharedSource/Characters/Character.cs | 228 +++++++----- .../SharedSource/Characters/CharacterInfo.cs | 55 ++- .../Characters/CharacterPrefab.cs | 3 +- .../Health/Afflictions/AfflictionHusk.cs | 27 +- .../Health/Afflictions/AfflictionPrefab.cs | 31 +- .../Characters/Health/CharacterHealth.cs | 14 +- .../Characters/Health/DamageModifier.cs | 13 +- .../SharedSource/Characters/HumanPrefab.cs | 13 +- .../SharedSource/Characters/Jobs/Job.cs | 5 +- .../SharedSource/Characters/Jobs/JobPrefab.cs | 3 +- .../Params/Animation/AnimationParams.cs | 3 +- .../Params/Animation/FishAnimations.cs | 3 +- .../Characters/Params/CharacterParams.cs | 20 +- .../Characters/Params/EditableParams.cs | 6 +- .../Params/Ragdoll/RagdollParams.cs | 50 +-- .../SharedSource/Characters/SkillSettings.cs | 7 + .../AbilityConditionals/AbilityCondition.cs | 5 +- .../AbilityConditionAffliction.cs | 5 +- .../AbilityConditionAttackData.cs | 3 +- .../AbilityConditionCharacter.cs | 59 +++- .../AbilityConditionCharacterNotLooted.cs | 8 +- .../AbilityConditionCharacterUnconcious.cs | 8 +- .../AbilityConditionData.cs | 6 +- .../AbilityConditionItem.cs | 3 +- .../AbilityConditionItemIsStatic.cs | 19 + .../AbilityConditionMission.cs | 3 +- .../AbilityConditionHasPermanentStat.cs | 16 +- .../AbilityConditionHasStatusTag.cs | 3 +- .../AbilityConditionLowestLevel.cs | 13 +- .../Talents/Abilities/CharacterAbility.cs | 26 +- .../Abilities/CharacterAbilityApplyForce.cs | 3 +- ...ilityApplyStatusEffectsToApprenticeship.cs | 3 +- .../CharacterAbilityGainSimultaneousSkill.cs | 3 +- .../CharacterAbilityGiveAffliction.cs | 6 +- .../CharacterAbilityGiveExperience.cs | 22 +- .../Abilities/CharacterAbilityGiveItemStat.cs | 4 +- .../CharacterAbilityGiveItemStatToTags.cs | 4 +- .../Abilities/CharacterAbilityGiveMoney.cs | 3 +- .../CharacterAbilityGivePermanentStat.cs | 41 ++- .../CharacterAbilityGiveReputation.cs | 6 +- .../CharacterAbilityGiveResistance.cs | 6 +- .../CharacterAbilityGiveTalentPoints.cs | 3 +- ...haracterAbilityGiveTalentPointsToAllies.cs | 3 +- .../CharacterAbilityIncreaseSkill.cs | 6 +- .../Abilities/CharacterAbilityMarkAsLooted.cs | 3 +- .../CharacterAbilityModifyResistance.cs | 6 +- .../Abilities/CharacterAbilityModifyValue.cs | 3 +- .../Abilities/CharacterAbilityPutItem.cs | 9 +- .../CharacterAbilityReduceAffliction.cs | 3 +- .../CharacterAbilityResetPermanentStat.cs | 3 +- .../CharacterAbilitySetMetadataInt.cs | 3 +- ...erAbilityUnlockApprenticeshipTalentTree.cs | 3 +- .../AbilityGroups/CharacterAbilityGroup.cs | 48 ++- .../Characters/Talents/CharacterTalent.cs | 17 +- .../Characters/Talents/TalentPrefab.cs | 3 +- .../Characters/Talents/TalentTree.cs | 12 +- .../CircuitBox/ItemSlotIndexPair.cs | 19 +- .../ContentFile/AfflictionsFile.cs | 8 +- .../ContentFile/CharacterFile.cs | 7 +- .../ContentFile/ContentFile.cs | 3 +- .../ContentFile/GenericPrefabFile.cs | 2 +- .../ContentFile/NPCConversationsFile.cs | 1 + .../ContentFile/OrdersFile.cs | 3 +- .../ContentFile/RandomEventsFile.cs | 3 +- .../ContentManagement/ContentFile/TextFile.cs | 25 +- .../ContentPackage/ContentPackage.cs | 5 +- .../ContentPackageManager.cs | 1 + .../ContentManagement/ContentXElement.cs | 3 +- .../SharedSource/DebugConsole.cs | 135 ++++--- .../BarotraumaShared/SharedSource/Enums.cs | 27 +- .../SharedSource/Events/ArtifactEvent.cs | 10 +- .../SharedSource/Events/Event.cs | 2 +- .../EventActions/CheckConditionalAction.cs | 116 ++++-- .../Events/EventActions/CheckDataAction.cs | 9 +- .../Events/EventActions/CheckItemAction.cs | 6 +- .../Events/EventActions/CheckOrderAction.cs | 3 +- .../EventActions/CheckReputationAction.cs | 8 +- .../EventActions/CheckSelectedAction.cs | 4 +- .../CheckTraitorEventStateAction.cs | 3 +- .../EventActions/CheckTraitorVoteAction.cs | 3 +- .../EventActions/CheckVisibilityAction.cs | 19 +- .../Events/EventActions/ConversationAction.cs | 3 +- .../Events/EventActions/CountTargetsAction.cs | 6 +- .../Events/EventActions/EventAction.cs | 39 ++- .../Events/EventActions/EventLogAction.cs | 6 +- .../EventActions/EventObjectiveAction.cs | 8 +- .../Events/EventActions/GiveExpAction.cs | 3 +- .../Events/EventActions/GiveSkillExpAction.cs | 3 +- .../SharedSource/Events/EventActions/GoTo.cs | 18 +- .../Events/EventActions/HighlightAction.cs | 43 +++ .../Events/EventActions/MissionAction.cs | 19 +- .../Events/EventActions/MissionStateAction.cs | 3 +- .../EventActions/ModifyLocationAction.cs | 15 +- .../EventActions/NPCChangeTeamAction.cs | 5 +- .../Events/EventActions/RNGAction.cs | 6 +- .../Events/EventActions/ReputationAction.cs | 6 +- .../SetTraitorEventStateAction.cs | 3 +- .../Events/EventActions/SkillCheckAction.cs | 3 +- .../Events/EventActions/SpawnAction.cs | 9 +- .../Events/EventActions/TagAction.cs | 81 +++-- .../Events/EventActions/TriggerEventAction.cs | 3 +- .../EventActions/TutorialHighlightAction.cs | 33 -- .../WaitForItemFabricatedAction.cs | 3 +- .../EventActions/WaitForItemUsedAction.cs | 31 +- .../SharedSource/Events/EventManager.cs | 17 +- .../SharedSource/Events/EventPrefab.cs | 38 +- .../SharedSource/Events/EventSet.cs | 109 +++++- .../Missions/AbandonedOutpostMission.cs | 16 +- .../Events/Missions/AlienRuinMission.cs | 18 +- .../Events/Missions/BeaconMission.cs | 4 +- .../Events/Missions/CargoMission.cs | 5 +- .../Events/Missions/CombatMission.cs | 2 +- .../Events/Missions/EndMission.cs | 21 +- .../Events/Missions/EscortMission.cs | 16 +- .../Events/Missions/MineralMission.cs | 6 +- .../SharedSource/Events/Missions/Mission.cs | 37 +- .../Events/Missions/MissionPrefab.cs | 18 +- .../Events/Missions/MonsterMission.cs | 12 +- .../Events/Missions/NestMission.cs | 9 +- .../Events/Missions/PirateMission.cs | 54 ++- .../Events/Missions/SalvageMission.cs | 15 +- .../Events/Missions/ScanMission.cs | 15 +- .../SharedSource/Events/MonsterEvent.cs | 99 ++++-- .../SharedSource/Events/ScriptedEvent.cs | 113 ++++-- .../Extensions/IEnumerableExtensions.cs | 5 + .../SharedSource/GameSession/CargoManager.cs | 174 ++++++--- .../SharedSource/GameSession/CrewManager.cs | 11 +- .../GameSession/Data/CampaignMetadata.cs | 9 +- .../GameSession/Data/Reputation.cs | 2 +- .../GameSession/GameModes/CampaignMode.cs | 85 +++-- .../GameSession/GameModes/CampaignSettings.cs | 6 + .../GameModes/MultiPlayerCampaign.cs | 7 +- .../SharedSource/GameSession/GameSession.cs | 11 +- .../SharedSource/GameSession/HireManager.cs | 20 +- .../GameSession/UpgradeManager.cs | 34 +- .../SharedSource/Items/CharacterInventory.cs | 28 +- .../SharedSource/Items/Components/Door.cs | 34 +- .../Items/Components/ElectricalDischarger.cs | 3 +- .../Items/Components/GeneticMaterial.cs | 6 +- .../SharedSource/Items/Components/Growable.cs | 3 +- .../Items/Components/Holdable/IdCard.cs | 3 - .../Items/Components/Holdable/MeleeWeapon.cs | 5 +- .../Items/Components/Holdable/Pickable.cs | 2 +- .../Items/Components/Holdable/RangedWeapon.cs | 3 +- .../Items/Components/Holdable/RepairTool.cs | 20 +- .../Items/Components/Holdable/Throwable.cs | 2 +- .../Items/Components/ItemComponent.cs | 56 ++- .../Items/Components/ItemContainer.cs | 35 +- .../Items/Components/Machines/Controller.cs | 5 +- .../Components/Machines/Deconstructor.cs | 2 +- .../Items/Components/Machines/Engine.cs | 4 +- .../Items/Components/Machines/Fabricator.cs | 125 +++++-- .../Items/Components/Machines/Pump.cs | 4 +- .../Items/Components/Machines/Reactor.cs | 4 +- .../Items/Components/Machines/Steering.cs | 14 +- .../Items/Components/Power/PowerContainer.cs | 2 +- .../Items/Components/Power/PowerTransfer.cs | 3 +- .../Items/Components/Projectile.cs | 8 +- .../SharedSource/Items/Components/Quality.cs | 3 +- .../Items/Components/Repairable.cs | 6 +- .../SharedSource/Items/Components/Scanner.cs | 3 +- .../Items/Components/Signal/ButtonTerminal.cs | 3 +- .../Items/Components/Signal/CircuitBox.cs | 7 +- .../Components/Signal/ConnectionPanel.cs | 6 + .../Items/Components/Signal/LightComponent.cs | 36 +- .../Items/Components/TriggerComponent.cs | 12 +- .../SharedSource/Items/Components/Turret.cs | 136 +++++++- .../SharedSource/Items/Components/Wearable.cs | 4 +- .../SharedSource/Items/Inventory.cs | 11 +- .../SharedSource/Items/Item.cs | 89 ++++- .../SharedSource/Items/ItemEventData.cs | 3 +- .../SharedSource/Items/ItemPrefab.cs | 132 +++++-- .../SharedSource/Items/ItemStatManager.cs | 87 ++++- .../SharedSource/Items/RelatedItem.cs | 4 +- .../SharedSource/Map/DummyFireSource.cs | 3 +- .../SharedSource/Map/FireSource.cs | 17 +- .../BarotraumaShared/SharedSource/Map/Hull.cs | 5 + .../SharedSource/Map/IDamageable.cs | 2 +- .../Map/Levels/DestructibleLevelWall.cs | 2 +- .../SharedSource/Map/Levels/Level.cs | 126 +++++-- .../SharedSource/Map/Levels/LevelData.cs | 2 +- .../Map/Levels/LevelObjects/LevelObject.cs | 2 +- .../Levels/LevelObjects/LevelObjectManager.cs | 4 +- .../SharedSource/Map/Map/Location.cs | 189 +++++++--- .../SharedSource/Map/Map/LocationType.cs | 100 +++++- .../Map/Map/LocationTypeChange.cs | 14 +- .../SharedSource/Map/Map/Map.cs | 22 +- .../SharedSource/Map/MapEntity.cs | 10 +- ...onStationInfo.cs => ExtraSubmarineInfo.cs} | 64 +++- .../Map/Outposts/OutpostGenerationParams.cs | 2 +- .../SharedSource/Map/Structure.cs | 183 +++++++--- .../SharedSource/Map/StructurePrefab.cs | 9 +- .../SharedSource/Map/Submarine.cs | 165 +++++---- .../SharedSource/Map/SubmarineBody.cs | 58 +-- .../SharedSource/Map/SubmarineInfo.cs | 18 +- .../SharedSource/Networking/BanList.cs | 10 + .../Networking/ChildServerRelay.cs | 20 +- .../SharedSource/Networking/Client.cs | 6 +- .../SharedSource/Networking/EntitySpawner.cs | 6 +- .../Networking/OrderChatMessage.cs | 2 +- .../SharedSource/Networking/RespawnManager.cs | 7 + .../SharedSource/Networking/ServerSettings.cs | 14 + .../Editable/ConditionallyEditable.cs | 66 ++++ .../Serialization/Editable/Editable.cs | 55 +++ .../SerializableProperty.cs | 135 +------ .../Serialization/StructSerialization.cs | 2 +- .../SharedSource/Settings/GameSettings.cs | 29 +- .../SharedSource/Sprite/Sprite.cs | 3 +- .../StatusEffects/PropertyConditional.cs | 4 +- .../StatusEffects/StatusEffect.cs | 54 +-- .../SharedSource/SteamAchievementManager.cs | 1 + .../BarotraumaShared/SharedSource/Tags.cs | 14 +- .../Text/LocalizedString/TagLString.cs | 16 +- .../SharedSource/Text/RichString.cs | 13 +- .../SharedSource/Text/TextManager.cs | 63 +++- .../SharedSource/Text/TextPack.cs | 55 ++- .../SharedSource/Traitors/TraitorEvent.cs | 4 +- .../Traitors/TraitorEventPrefab.cs | 5 +- .../SharedSource/Upgrades/Upgrade.cs | 3 +- .../SharedSource/Upgrades/UpgradePrefab.cs | 26 +- .../SharedSource/Utils/MathUtils.cs | 102 +++--- .../SharedSource/Utils/SaveUtil.cs | 9 +- .../SharedSource/Utils/Shapes/Quad2D.cs | 113 ++++++ .../SharedSource/Utils/Shapes/Triangle2D.cs | 21 ++ Barotrauma/BarotraumaShared/changelog.txt | 115 ++++++ .../FabricatorQualityRollTests.cs | 66 ++++ 375 files changed, 7771 insertions(+), 2874 deletions(-) rename Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/{TutorialHighlightAction.cs => HighlightAction.cs} (69%) create mode 100644 Barotrauma/BarotraumaClient/ClientSource/SpamServerFilter.cs rename Barotrauma/BarotraumaClient/ClientSource/Utils/{Quad.cs => GraphicsQuad.cs} (98%) create mode 100644 Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/HighlightAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCheckStolenItems.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindThieves.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemIsStatic.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/HighlightAction.cs delete mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialHighlightAction.cs rename Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/{BeaconStationInfo.cs => ExtraSubmarineInfo.cs} (54%) create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Serialization/Editable/ConditionallyEditable.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Serialization/Editable/Editable.cs rename Barotrauma/BarotraumaShared/SharedSource/Serialization/{ => SerializableProperty}/SerializableProperty.cs (91%) create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Utils/Shapes/Quad2D.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Utils/Shapes/Triangle2D.cs create mode 100644 Barotrauma/BarotraumaTest/FabricatorQualityRollTests.cs diff --git a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs index 272aace14..9ec4022c3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs @@ -6,7 +6,7 @@ using System; namespace Barotrauma { - class Camera : IDisposable + class Camera { public static bool FollowSub = true; @@ -147,21 +147,10 @@ namespace Barotrauma position = Vector2.Zero; CreateMatrices(); - // TODO: this has the potential to cause a resource leak - // by sneakily creating a reference to cameras that we might - // fail to release. - GameMain.Instance.ResolutionChanged += CreateMatrices; UpdateTransform(false); } - private bool disposed = false; - public void Dispose() - { - if (!disposed) { GameMain.Instance.ResolutionChanged -= CreateMatrices; } - disposed = true; - } - public Vector2 TargetPos { get; set; } public Vector2 GetPosition() @@ -207,6 +196,12 @@ namespace Barotrauma public void UpdateTransform(bool interpolate = true, bool updateListener = true) { + if (GameMain.GraphicsWidth != Resolution.X || + GameMain.GraphicsHeight != Resolution.Y) + { + CreateMatrices(); + } + Vector2 interpolatedPosition = interpolate ? Timing.Interpolate(prevPosition, position) : position; float interpolatedZoom = interpolate ? Timing.Interpolate(prevZoom, zoom) : zoom; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index a223fe878..8df8deea0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -136,6 +136,7 @@ namespace Barotrauma set { if (!MathUtils.IsValid(value)) { return; } + if (this != Controlled) { return; } if (Screen.Selected?.Cam != null) { Screen.Selected.Cam.Shake = value; @@ -229,6 +230,8 @@ namespace Barotrauma } } + private float pressureEffectTimer; + private readonly List activeObjectiveEntities = new List(); public IEnumerable ActiveObjectiveEntities { @@ -333,18 +336,22 @@ namespace Barotrauma { if (!IsProtectedFromPressure && (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure > 0.0f)) { - float pressure = AnimController.CurrentHull == null ? 100.0f : AnimController.CurrentHull.LethalPressure; - if (pressure > 0.0f) + //wait until the character has been in pressure for one second so the zoom doesn't + //"flicker" in and out if the pressure fluctuates around the minimum threshold + pressureEffectTimer += deltaTime; + if (pressureEffectTimer > 1.0f) { - //lerp in during the 1st second of the pressure timer so the zoom doesn't - //"flicker" in and out if the pressure fluctuates around the minimum threshold - float timerMultiplier = (PressureTimer / 100.0f); - float zoomInEffectStrength = MathHelper.Clamp(pressure / 100.0f * timerMultiplier, 0.0f, 1.0f); + float pressure = AnimController.CurrentHull == null ? 100.0f : AnimController.CurrentHull.LethalPressure; + float zoomInEffectStrength = MathHelper.Clamp(pressure / 100.0f, 0.0f, 1.0f); cam.Zoom = MathHelper.Lerp(cam.Zoom, cam.DefaultZoom + (Math.Max(pressure, 10) / 150.0f) * Rand.Range(0.9f, 1.1f), zoomInEffectStrength); } } + else + { + pressureEffectTimer = 0.0f; + } if (IsHumanoid) { @@ -521,22 +528,25 @@ namespace Barotrauma if (controlled == this) { controlled = null; - if (!(Screen.Selected?.Cam is null)) + if (Screen.Selected?.Cam is not null) { Screen.Selected.Cam.TargetPos = Vector2.Zero; Lights.LightManager.ViewTarget = null; } } + sounds.ForEach(s => s.Sound?.Dispose()); + sounds.Clear(); + if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.GetCharacters().Contains(this)) { GameMain.GameSession.CrewManager.RemoveCharacter(this); } - - if (GameMain.Client?.Character == this) GameMain.Client.Character = null; - if (Lights.LightManager.ViewTarget == this) Lights.LightManager.ViewTarget = null; + if (GameMain.Client?.Character == this) { GameMain.Client.Character = null; } + + if (Lights.LightManager.ViewTarget == this) { Lights.LightManager.ViewTarget = null; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index 5f0c45241..348cdd874 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -894,7 +894,7 @@ namespace Barotrauma if (!orderIndicatorCount.ContainsKey(target)) { orderIndicatorCount.Add(target, 0); } - Vector2 drawPos = target is Entity ? (target as Entity).DrawPosition : + Vector2 drawPos = target is Entity entity ? entity.DrawPosition : target.Submarine == null ? target.Position : target.Position + target.Submarine.DrawPosition; drawPos += Vector2.UnitX * order.SymbolSprite.size.X * 1.5f * orderIndicatorCount[target]; GUI.DrawIndicator(spriteBatch, drawPos, cam, 100.0f, order.SymbolSprite, order.Color * iconAlpha, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index dce0ec9cd..e18d1e4e2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -217,6 +217,7 @@ namespace Barotrauma if ((int)newLevel > (int)prevLevel) { + Character.Controlled?.SelectedItem?.OnPlayerSkillsChanged(); int increase = Math.Max((int)newLevel - (int)prevLevel, 1); Character?.AddMessage( @@ -518,7 +519,7 @@ namespace Barotrauma attachment.Sprite.Draw(spriteBatch, drawPos, color ?? Color.White, origin, rotate: 0, scale: scale, depth: depth, spriteEffect: spriteEffects); } - public static CharacterInfo ClientRead(Identifier speciesName, IReadMessage inc) + public static CharacterInfo ClientRead(Identifier speciesName, IReadMessage inc, bool requireJobPrefabFound = true) { ushort infoID = inc.ReadUInt16(); string newName = inc.ReadString(); @@ -554,14 +555,19 @@ namespace Barotrauma if (jobIdentifier > 0) { jobPrefab = JobPrefab.Prefabs.Find(jp => jp.UintIdentifier == jobIdentifier); - if (jobPrefab == null) + if (jobPrefab == null && requireJobPrefabFound) { throw new Exception($"Error while reading {nameof(CharacterInfo)} received from the server: could not find a job prefab with the identifier \"{jobIdentifier}\"."); } - foreach (SkillPrefab skillPrefab in jobPrefab.Skills.OrderBy(s => s.Identifier)) + byte skillCount = inc.ReadByte(); + List jobSkills = jobPrefab?.Skills.OrderBy(s => s.Identifier).ToList(); + for (int i = 0; i < skillCount; i++) { float skillLevel = inc.ReadSingle(); - skillLevels.Add(skillPrefab.Identifier, skillLevel); + if (jobSkills != null && i < jobSkills.Count) + { + skillLevels.Add(jobSkills[i].Identifier, skillLevel); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 76919ce12..b0a721fc3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -2170,6 +2170,8 @@ namespace Barotrauma medUIExtra?.Remove(); medUIExtra = null; + Character.OnAttacked -= OnAttacked; + limbIndicatorOverlay?.Remove(); limbIndicatorOverlay = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index f6f7a7331..32ce3df20 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -1216,7 +1216,7 @@ namespace Barotrauma pos: new Vector2(body.DrawPosition.X, -body.DrawPosition.Y), srcRect: w.Sprite.SourceRect, color: Color.White, - rotation: rotation, + rotationRad: rotation, origin: origin, scale: new Vector2(scale, scale), effects: spriteEffect, diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index d355c7cb9..d08948b86 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -34,7 +34,7 @@ namespace Barotrauma bool allowCheats = GameMain.NetworkMember == null && (GameMain.GameSession?.GameMode is TestGameMode || Screen.Selected is { IsEditor: true }); if (!allowCheats && !CheatsEnabled && IsCheat) { - NewMessage("You need to enable cheats using the command \"enablecheats\" before you can use the command \"" + names[0] + "\".", Color.Red); + NewMessage("You need to enable cheats using the command \"enablecheats\" before you can use the command \"" + Names[0] + "\".", Color.Red); #if USE_STEAM NewMessage("Enabling cheats will disable Steam achievements during this play session.", Color.Red); #endif @@ -215,9 +215,9 @@ namespace Barotrauma SoundPlayer.PlayUISound(GUISoundType.Select); } - private static bool IsCommandPermitted(string command, GameClient client) + private static bool IsCommandPermitted(Identifier command, GameClient client) { - switch (command) + switch (command.Value.ToLowerInvariant()) { case "kick": return client.HasPermission(ClientPermissions.Kick); @@ -304,7 +304,7 @@ namespace Barotrauma } }; var textBlock = new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width - 5, 0), textContainer.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(2, 2) }, - msg.Text, textAlignment: Alignment.TopLeft, font: GUIStyle.SmallFont, wrap: true) + RichString.Rich(msg.Text), textAlignment: Alignment.TopLeft, font: GUIStyle.SmallFont, wrap: true) { CanBeFocused = false, TextColor = msg.Color @@ -346,7 +346,7 @@ namespace Barotrauma CanBeFocused = false }; var textBlock = new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width - 170, 0), textContainer.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point(20, 0) }, - command.help, textAlignment: Alignment.TopLeft, font: GUIStyle.SmallFont, wrap: true) + command.Help, textAlignment: Alignment.TopLeft, font: GUIStyle.SmallFont, wrap: true) { CanBeFocused = false, TextColor = Color.White @@ -354,7 +354,7 @@ namespace Barotrauma textContainer.RectTransform.NonScaledSize = new Point(textContainer.RectTransform.NonScaledSize.X, textBlock.RectTransform.NonScaledSize.Y + 5); textBlock.SetTextPos(); new GUITextBlock(new RectTransform(new Point(150, textContainer.Rect.Height), textContainer.RectTransform), - command.names[0], textAlignment: Alignment.TopLeft); + command.Names[0].Value, textAlignment: Alignment.TopLeft); listBox.UpdateScrollBarSize(); listBox.BarScroll = 1.0f; @@ -364,7 +364,7 @@ namespace Barotrauma private static void AssignOnClientExecute(string names, Action onClientExecute) { - Command command = commands.Find(c => c.names.Intersect(names.Split('|')).Count() > 0); + Command command = commands.Find(c => c.Names.Intersect(names.Split('|').ToIdentifiers()).Any()); if (command == null) { throw new Exception("AssignOnClientExecute failed. Command matching the name(s) \"" + names + "\" not found."); @@ -378,7 +378,7 @@ namespace Barotrauma private static void AssignRelayToServer(string names, bool relay) { - Command command = commands.Find(c => c.names.Intersect(names.Split('|')).Count() > 0); + Command command = commands.Find(c => c.Names.Intersect(names.Split('|').ToIdentifiers()).Any()); if (command == null) { DebugConsole.Log("Could not assign to relay to server: " + names); @@ -681,6 +681,7 @@ namespace Barotrauma AssignRelayToServer("savebinds", false); AssignRelayToServer("spreadsheetexport", false); #if DEBUG + AssignRelayToServer("listspamfilters", false); AssignRelayToServer("crash", false); AssignRelayToServer("showballastflorasprite", false); AssignRelayToServer("simulatedlatency", false); @@ -706,6 +707,8 @@ namespace Barotrauma AssignRelayToServer("showmoney", true); AssignRelayToServer("setskill", true); AssignRelayToServer("readycheck", true); + commands.Add(new Command("debugjobassignment", "", (string[] args) => { })); + AssignRelayToServer("debugjobassignment", true); AssignRelayToServer("givetalent", true); AssignRelayToServer("unlocktalents", true); @@ -2232,6 +2235,30 @@ namespace Barotrauma })); #if DEBUG + commands.Add(new Command("listspamfilters", "Lists filters that are in the global spam filter.", (string[] args) => + { + if (!SpamServerFilters.GlobalSpamFilter.TryUnwrap(out var filter)) + { + ThrowError("Global spam list is not initialized."); + return; + } + + if (!filter.Filters.Any()) + { + NewMessage("Global spam list is empty.", GUIStyle.Green); + return; + } + + StringBuilder sb = new(); + + foreach (var f in filter.Filters) + { + sb.AppendLine(f.ToString()); + } + + NewMessage(sb.ToString(), GUIStyle.Green); + })); + commands.Add(new Command("setplanthealth", "setplanthealth [value]: Sets the health of the selected plant in sub editor.", (string[] args) => { if (1 > args.Length || Screen.Selected != GameMain.SubEditorScreen) { return; } @@ -3092,7 +3119,7 @@ namespace Barotrauma int i = 0; foreach (LocationConnection connection in campaign.Map.CurrentLocation.Connections) { - NewMessage(" " + i + ". " + connection.OtherLocation(campaign.Map.CurrentLocation).Name, Color.White); + NewMessage(" " + i + ". " + connection.OtherLocation(campaign.Map.CurrentLocation).DisplayName, Color.White); i++; } ShowQuestionPrompt("Select a destination (0 - " + (campaign.Map.CurrentLocation.Connections.Count - 1) + "):", (string selectedDestination) => diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs index 45893dc8b..efbdb6712 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs @@ -374,13 +374,12 @@ namespace Barotrauma btn.RectTransform.MinSize = new Point(0, (int)(btn.TextBlock.Rect.Height * 1.2f)); } - textContent.RectTransform.MinSize = new Point(0, textContent.Children.Sum(c => c.Rect.Height) + GUI.IntScale(16)); + textContent.RectTransform.MinSize = new Point(0, textContent.Children.Sum(c => c.Rect.Height + textContent.AbsoluteSpacing) + GUI.IntScale(16)); content.RectTransform.MinSize = new Point(0, content.Children.Sum(c => c.Rect.Height)); // Recalculate the text size as it is scaled up and no longer matching the text height due to the textContent's minSize increasing textBlock.CalculateHeightFromText(); textBlock.TextAlignment = Alignment.TopLeft; - //content.RectTransform.MinSize = new Point(0, textContent.Rect.Height); return buttons; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/EventObjectiveAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/EventObjectiveAction.cs index 2686bd1af..3c81ad491 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/EventObjectiveAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/EventObjectiveAction.cs @@ -16,6 +16,11 @@ partial class EventObjectiveAction : EventAction int width = 450, int height = 80) { + if (Type == SegmentActionType.AddIfNotFound) + { + if (ObjectiveManager.IsSegmentActive(Identifier)) { return; } + } + ObjectiveManager.Segment? segment = null; // Only need to create the segment when it's being triggered (otherwise the tutorial already has the segment instance) if (Type == SegmentActionType.Trigger) @@ -24,7 +29,8 @@ partial class EventObjectiveAction : EventAction new ObjectiveManager.Segment.Text(TextTag, width, height, Anchor.Center), new ObjectiveManager.Segment.Video(videoFile, TextTag, width, height)); } - else if (Type == SegmentActionType.Add) + else if (Type == SegmentActionType.Add || + Type == SegmentActionType.AddIfNotFound) { segment = ObjectiveManager.Segment.CreateObjectiveSegment(Identifier, !ObjectiveTag.IsEmpty ? ObjectiveTag : Identifier); } @@ -33,10 +39,12 @@ partial class EventObjectiveAction : EventAction segment.CanBeCompleted = CanBeCompleted; segment.ParentId = ParentObjectiveId; } + switch (Type) { case SegmentActionType.Trigger: case SegmentActionType.Add: + case SegmentActionType.AddIfNotFound: ObjectiveManager.TriggerSegment(segment); break; case SegmentActionType.Complete: diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialHighlightAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/HighlightAction.cs similarity index 69% rename from Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialHighlightAction.cs rename to Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/HighlightAction.cs index 732c1a480..cce9ce464 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialHighlightAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/HighlightAction.cs @@ -1,22 +1,19 @@ +#nullable enable using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Linq; namespace Barotrauma; -partial class TutorialHighlightAction : EventAction +partial class HighlightAction : EventAction { - private static readonly Color highlightColor = Color.Orange; - - partial void UpdateProjSpecific() + partial void SetHighlightProjSpecific(Entity entity, IEnumerable? targetCharacters) { - if (GameMain.GameSession?.GameMode is not TutorialMode) { return; } - foreach (var target in ParentEvent.GetTargets(TargetTag)) + if (targetCharacters != null && !targetCharacters.Contains(Character.Controlled)) { - SetHighlight(target); + return; } - } - private void SetHighlight(Entity entity) - { if (entity is Item i) { SetItemHighlight(i); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs index 27007040d..d5687b38a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs @@ -47,7 +47,7 @@ namespace Barotrauma } float theoreticalMaxMonsterStrength = 10000; - float relativeMaxMonsterStrength = theoreticalMaxMonsterStrength * (GameMain.GameSession?.LevelData?.Difficulty ?? 0f) / 100; + float relativeMaxMonsterStrength = theoreticalMaxMonsterStrength * (GameMain.GameSession?.Level?.Difficulty ?? 0f) / 100; float absoluteMonsterStrength = monsterStrength / theoreticalMaxMonsterStrength; float relativeMonsterStrength = monsterStrength / relativeMaxMonsterStrength; @@ -581,7 +581,14 @@ namespace Barotrauma StatusEffect effect = StatusEffect.Load(subElement, $"EventManager.ClientRead ({eventIdentifier})"); foreach (Entity target in targets) { - effect.Apply(effect.type, 1.0f, target, target as ISerializableEntity); + if (target is Item item) + { + effect.Apply(effect.type, 1.0f, item, item.AllPropertyObjects); + } + else + { + effect.Apply(effect.type, 1.0f, target, target as ISerializableEntity); + } } } break; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs index 1a9941f42..3e9fdd7cb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs @@ -51,7 +51,8 @@ namespace Barotrauma if (requiredDeliveryAmount == 0) { requiredDeliveryAmount = items.Count; } if (requiredDeliveryAmount > items.Count) { - DebugConsole.AddWarning($"Error in mission \"{Prefab.Identifier}\". Required delivery amount is {requiredDeliveryAmount} but there's only {items.Count} items to deliver."); + DebugConsole.AddWarning($"Error in mission \"{Prefab.Identifier}\". Required delivery amount is {requiredDeliveryAmount} but there's only {items.Count} items to deliver.", + contentPackage: Prefab.ContentPackage); requiredDeliveryAmount = items.Count; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs index fdf6ccfa8..c244b7a22 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs @@ -126,7 +126,13 @@ namespace Barotrauma void GiveMissionExperience(CharacterInfo info) { if (info == null) { return; } - var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f); + var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f, info.Character); + //check if anyone else in the crew has talents that could give a bonus to this one + foreach (var c in crew) + { + if (c == info.Character) { continue; } + c.CheckTalents(AbilityEffectType.OnAllyGainMissionExperience, experienceGainMultiplierIndividual); + } info.Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual); info.GiveExperience((int)(experienceGain * experienceGainMultiplierIndividual.Value)); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs index c04c21183..374e92d5e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs @@ -247,30 +247,33 @@ namespace Barotrauma UpdateCrew(); } + public void UpdateHireables() + { + UpdateHireables(campaign?.CurrentLocation); + } + private void UpdateHireables(Location location) { - if (hireableList != null) + if (hireableList == null) { return; } + hireableList.Content.Children.ToList().ForEach(c => hireableList.RemoveChild(c)); + var hireableCharacters = location.GetHireableCharacters(); + if (hireableCharacters.None()) { - hireableList.Content.Children.ToList().ForEach(c => hireableList.RemoveChild(c)); - var hireableCharacters = location.GetHireableCharacters(); - if (hireableCharacters.None()) + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), hireableList.Content.RectTransform), TextManager.Get("HireUnavailable"), textAlignment: Alignment.Center) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), hireableList.Content.RectTransform), TextManager.Get("HireUnavailable"), textAlignment: Alignment.Center) - { - CanBeFocused = false - }; - } - else - { - foreach (CharacterInfo c in hireableCharacters) - { - if (c == null) { continue; } - CreateCharacterFrame(c, hireableList); - } - } - sortingDropDown.SelectItem(SortingMethod.JobAsc); - hireableList.UpdateScrollBarSize(); + CanBeFocused = false + }; } + else + { + foreach (CharacterInfo c in hireableCharacters) + { + if (c == null) { continue; } + CreateCharacterFrame(c, hireableList); + } + } + sortingDropDown.SelectItem(SortingMethod.JobAsc); + hireableList.UpdateScrollBarSize(); } public void SetHireables(Location location, List availableHires) @@ -434,7 +437,7 @@ namespace Barotrauma if (listBox != crewList) { new GUITextBlock(new RectTransform(new Vector2(width, 1.0f), mainGroup.RectTransform), - TextManager.FormatCurrency(characterInfo.Salary), + TextManager.FormatCurrency(HireManager.GetSalaryFor(characterInfo)), textAlignment: Alignment.Center) { CanBeFocused = false @@ -692,11 +695,8 @@ namespace Barotrauma private void SetTotalHireCost() { if (pendingList == null || totalBlock == null || validateHiresButton == null) { return; } - int total = 0; - pendingList.Content.Children.ForEach(c => - { - total += ((InfoSkill)c.UserData).CharacterInfo.Salary; - }); + var infos = pendingList.Content.Children.Select(static c => ((InfoSkill)c.UserData).CharacterInfo).ToArray(); + int total = HireManager.GetSalaryFor(infos); totalBlock.Text = TextManager.FormatCurrency(total); bool enoughMoney = campaign == null || campaign.CanAfford(total); totalBlock.TextColor = enoughMoney ? Color.White : Color.Red; @@ -718,14 +718,14 @@ namespace Barotrauma if (nonDuplicateHires.None()) { return false; } - int total = nonDuplicateHires.Aggregate(0, (total, info) => total + info.Salary); + int total = HireManager.GetSalaryFor(nonDuplicateHires); if (!campaign.CanAfford(total)) { return false; } bool atLeastOneHired = false; foreach (CharacterInfo ci in nonDuplicateHires) { - if (campaign.TryHireCharacter(campaign.Map.CurrentLocation, ci)) + if (campaign.TryHireCharacter(campaign.Map.CurrentLocation, ci, Character.Controlled)) { atLeastOneHired = true; } @@ -741,7 +741,7 @@ namespace Barotrauma SelectCharacter(null, null, null); var dialog = new GUIMessageBox( TextManager.Get("newcrewmembers"), - TextManager.GetWithVariable("crewhiredmessage", "[location]", campaignUI?.Campaign?.Map?.CurrentLocation?.Name), + TextManager.GetWithVariable("crewhiredmessage", "[location]", campaignUI?.Campaign?.Map?.CurrentLocation?.DisplayName), new LocalizedString[] { TextManager.Get("Ok") }); dialog.Buttons[0].OnClicked += dialog.Close; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index 3a0bff6d9..4a1bee7c4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -512,10 +512,18 @@ namespace Barotrauma soundStr += " (stopped)"; clr *= 0.5f; } - else if (playingSoundChannel.Muffled) + else { - soundStr += " (muffled)"; - clr = Color.Lerp(clr, Color.LightGray, 0.5f); + if (playingSoundChannel.Muffled) + { + soundStr += " (muffled)"; + clr = Color.Lerp(clr, Color.LightGray, 0.5f); + } + if (playingSoundChannel.FadingOutAndDisposing) + { + soundStr += ". Fading out..."; + clr = Color.Lerp(clr, Color.Black, 0.15f); + } } } @@ -2163,10 +2171,10 @@ namespace Barotrauma }; } - public static GUIMessageBox AskForConfirmation(LocalizedString header, LocalizedString body, Action onConfirm, Action onDeny = null) + public static GUIMessageBox AskForConfirmation(LocalizedString header, LocalizedString body, Action onConfirm, Action onDeny = null, Vector2? relativeSize = null, Point? minSize = null) { LocalizedString[] buttons = { TextManager.Get("Ok"), TextManager.Get("Cancel") }; - GUIMessageBox msgBox = new GUIMessageBox(header, body, buttons, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175)); + GUIMessageBox msgBox = new GUIMessageBox(header, body, buttons, relativeSize: relativeSize ?? new Vector2(0.2f, 0.175f), minSize: minSize ?? new Point(300, 175)); // Cancel button msgBox.Buttons[1].OnClicked = delegate diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index e3ea42eaa..762c4053c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -775,23 +775,30 @@ namespace Barotrauma toolTipBlock.UserData = toolTip; } - toolTipBlock.RectTransform.AbsoluteOffset = - RectTransform.CalculateAnchorPoint(anchor, targetElement) + - RectTransform.CalculatePivotOffset(pivot, toolTipBlock.RectTransform.NonScaledSize); + CalculateOffset(); if (toolTipBlock.Rect.Right > GameMain.GraphicsWidth - 10) { - toolTipBlock.RectTransform.AbsoluteOffset -= new Point(toolTipBlock.Rect.Width + targetElement.Width, 0); + anchor = RectTransform.MoveAnchorLeft(anchor); + pivot = (Pivot)RectTransform.MoveAnchorRight((Anchor)pivot); + CalculateOffset(); } if (toolTipBlock.Rect.Bottom > GameMain.GraphicsHeight - 10) { - toolTipBlock.RectTransform.AbsoluteOffset -= new Point( - 0, - toolTipBlock.Rect.Bottom - (GameMain.GraphicsHeight - 10)); + anchor = RectTransform.MoveAnchorTop(anchor); + pivot = (Pivot)RectTransform.MoveAnchorBottom((Anchor)pivot); + CalculateOffset(); } toolTipBlock.SetTextPos(); toolTipBlock.DrawManually(spriteBatch); + + void CalculateOffset() + { + toolTipBlock.RectTransform.AbsoluteOffset = + RectTransform.CalculateAnchorPoint(anchor, targetElement) + + RectTransform.CalculatePivotOffset(pivot, toolTipBlock.RectTransform.NonScaledSize); + } } #endregion diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs index c5e6fda31..de557c274 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; using Barotrauma.Extensions; +using Microsoft.Xna.Framework.Input; namespace Barotrauma { @@ -81,6 +82,8 @@ namespace Barotrauma public bool FlashOnAutoCloseCondition { get; set; } + public Action OnEnterPressed { get; set; } + public Type MessageBoxType => type; public static GUIComponent VisibleBox => MessageBoxes.LastOrDefault(); @@ -89,6 +92,10 @@ namespace Barotrauma : this(headerText, text, new LocalizedString[] { "OK" }, relativeSize, minSize, type: type) { this.Buttons[0].OnClicked = Close; + OnEnterPressed = () => + { + Buttons[0].OnClicked(Buttons[0], Buttons[0].UserData); + }; } public GUIMessageBox(RichString headerText, RichString text, LocalizedString[] buttons, @@ -516,6 +523,11 @@ namespace Barotrauma protected override void Update(float deltaTime) { + if (PlayerInput.KeyHit(Keys.Enter)) + { + OnEnterPressed?.Invoke(); + } + if (Draggable) { GUIComponent parent = GUI.MouseOn?.Parent?.Parent; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs index 9edfae736..488f9a83d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs @@ -18,6 +18,20 @@ namespace Barotrauma public GUIButton PlusButton { get; private set; } public GUIButton MinusButton { get; private set; } + private void UpdatePlusMinusButtonVisibility() + { + if (ForceShowPlusMinusButtons + || inputType == NumberType.Int + || (inputType == NumberType.Float && MinValueFloat > float.MinValue && MaxValueFloat < float.MaxValue)) + { + ShowPlusMinusButtons(); + } + else + { + HidePlusMinusButtons(); + } + } + private NumberType inputType; public NumberType InputType { @@ -26,15 +40,7 @@ namespace Barotrauma { if (inputType == value) { return; } inputType = value; - if (inputType == NumberType.Int || - (inputType == NumberType.Float && MinValueFloat > float.MinValue && MaxValueFloat < float.MaxValue)) - { - ShowPlusMinusButtons(); - } - else - { - HidePlusMinusButtons(); - } + UpdatePlusMinusButtonVisibility(); } } @@ -46,15 +52,7 @@ namespace Barotrauma { minValueFloat = value; ClampFloatValue(); - if (inputType == NumberType.Int || - (inputType == NumberType.Float && MinValueFloat > float.MinValue && MaxValueFloat < float.MaxValue)) - { - ShowPlusMinusButtons(); - } - else - { - HidePlusMinusButtons(); - } + UpdatePlusMinusButtonVisibility(); } } public float? MaxValueFloat @@ -64,15 +62,7 @@ namespace Barotrauma { maxValueFloat = value; ClampFloatValue(); - if (inputType == NumberType.Int || - (inputType == NumberType.Float && MinValueFloat > float.MinValue && MaxValueFloat < float.MaxValue)) - { - ShowPlusMinusButtons(); - } - else - { - HidePlusMinusButtons(); - } + UpdatePlusMinusButtonVisibility(); } } @@ -96,6 +86,19 @@ namespace Barotrauma } } + private bool forceShowPlusMinusButtons; + + public bool ForceShowPlusMinusButtons + { + get { return forceShowPlusMinusButtons; } + set + { + if (forceShowPlusMinusButtons == value) { return; } + forceShowPlusMinusButtons = value; + UpdatePlusMinusButtonVisibility(); + } + } + private int decimalsToDisplay = 1; public int DecimalsToDisplay { @@ -184,7 +187,7 @@ namespace Barotrauma /// public bool WrapAround; - public float valueStep; + public float ValueStep; private float pressedTimer; private readonly float pressedDelay = 0.5f; @@ -339,12 +342,12 @@ namespace Barotrauma { if (inputType == NumberType.Int) { - IntValue -= valueStep > 0 ? (int)valueStep : 1; + IntValue -= ValueStep > 0 ? (int)ValueStep : 1; ClampIntValue(); } else if (maxValueFloat.HasValue && minValueFloat.HasValue) { - FloatValue -= valueStep > 0 ? valueStep : Round(); + FloatValue -= ValueStep > 0 ? ValueStep : Round(); ClampFloatValue(); } } @@ -353,12 +356,12 @@ namespace Barotrauma { if (inputType == NumberType.Int) { - IntValue += valueStep > 0 ? (int)valueStep : 1; + IntValue += ValueStep > 0 ? (int)ValueStep : 1; ClampIntValue(); } else if (inputType == NumberType.Float) { - FloatValue += valueStep > 0 ? valueStep : Round(); + FloatValue += ValueStep > 0 ? ValueStep : Round(); ClampFloatValue(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs index 8d77932bc..d1d4a470f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs @@ -313,13 +313,18 @@ namespace Barotrauma public class GUIColor : GUISelector { - public GUIColor(string identifier) : base(identifier) { } + private readonly Color fallbackColor; + + public GUIColor(string identifier, Color fallbackColor) : base(identifier) + { + this.fallbackColor = fallbackColor; + } public Color Value { get { - return Prefabs.ActivePrefab.Color; + return Prefabs?.ActivePrefab?.Color ?? fallbackColor; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs index 05d7b129c..f6e5612d9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs @@ -75,72 +75,72 @@ namespace Barotrauma /// /// General green color used for elements whose colors are set from code /// - public readonly static GUIColor Green = new GUIColor("Green"); + public readonly static GUIColor Green = new GUIColor("Green", new Color(154, 213, 163, 255)); /// /// General red color used for elements whose colors are set from code /// - public readonly static GUIColor Orange = new GUIColor("Orange"); + public readonly static GUIColor Orange = new GUIColor("Orange", new Color(243, 162, 50, 255)); /// /// General red color used for elements whose colors are set from code /// - public readonly static GUIColor Red = new GUIColor("Red"); + public readonly static GUIColor Red = new GUIColor("Red", new Color(245, 105, 105, 255)); /// /// General blue color used for elements whose colors are set from code /// - public readonly static GUIColor Blue = new GUIColor("Blue"); + public readonly static GUIColor Blue = new GUIColor("Blue", new Color(126, 211, 224, 255)); /// /// General yellow color used for elements whose colors are set from code /// - public readonly static GUIColor Yellow = new GUIColor("Yellow"); + public readonly static GUIColor Yellow = new GUIColor("Yellow", new Color(255, 255, 0, 255)); /// /// Color to display the name of modded servers in the server list. /// - public readonly static GUIColor ModdedServerColor = new GUIColor("ModdedServerColor"); + public readonly static GUIColor ModdedServerColor = new GUIColor("ModdedServerColor", new Color(154, 185, 160, 255)); - public readonly static GUIColor ColorInventoryEmpty = new GUIColor("ColorInventoryEmpty"); - public readonly static GUIColor ColorInventoryHalf = new GUIColor("ColorInventoryHalf"); - public readonly static GUIColor ColorInventoryFull = new GUIColor("ColorInventoryFull"); - public readonly static GUIColor ColorInventoryBackground = new GUIColor("ColorInventoryBackground"); - public readonly static GUIColor ColorInventoryEmptyOverlay = new GUIColor("ColorInventoryEmptyOverlay"); + public readonly static GUIColor ColorInventoryEmpty = new GUIColor("ColorInventoryEmpty", new Color(245, 105, 105, 255)); + public readonly static GUIColor ColorInventoryHalf = new GUIColor("ColorInventoryHalf", new Color(243, 162, 50, 255)); + public readonly static GUIColor ColorInventoryFull = new GUIColor("ColorInventoryFull", new Color(96, 222, 146, 255)); + public readonly static GUIColor ColorInventoryBackground = new GUIColor("ColorInventoryBackground", new Color(56, 56, 56, 255)); + public readonly static GUIColor ColorInventoryEmptyOverlay = new GUIColor("ColorInventoryEmptyOverlay", new Color(125, 125, 125, 255)); - public readonly static GUIColor TextColorNormal = new GUIColor("TextColorNormal"); - public readonly static GUIColor TextColorBright = new GUIColor("TextColorBright"); - public readonly static GUIColor TextColorDark = new GUIColor("TextColorDark"); - public readonly static GUIColor TextColorDim = new GUIColor("TextColorDim"); + public readonly static GUIColor TextColorNormal = new GUIColor("TextColorNormal", new Color(228, 217, 167, 255)); + public readonly static GUIColor TextColorBright = new GUIColor("TextColorBright", new Color(255, 255, 255, 255)); + public readonly static GUIColor TextColorDark = new GUIColor("TextColorDark", new Color(0, 0, 0, 230)); + public readonly static GUIColor TextColorDim = new GUIColor("TextColorDim", new Color(153, 153, 153, 153)); - public readonly static GUIColor ItemQualityColorPoor = new GUIColor("ItemQualityColorPoor"); - public readonly static GUIColor ItemQualityColorNormal = new GUIColor("ItemQualityColorNormal"); - public readonly static GUIColor ItemQualityColorGood = new GUIColor("ItemQualityColorGood"); - public readonly static GUIColor ItemQualityColorExcellent = new GUIColor("ItemQualityColorExcellent"); - public readonly static GUIColor ItemQualityColorMasterwork = new GUIColor("ItemQualityColorMasterwork"); + public readonly static GUIColor ItemQualityColorPoor = new GUIColor("ItemQualityColorPoor", new Color(128, 128, 128, 255)); + public readonly static GUIColor ItemQualityColorNormal = new GUIColor("ItemQualityColorNormal", new Color(255, 255, 255, 255)); + public readonly static GUIColor ItemQualityColorGood = new GUIColor("ItemQualityColorGood", new Color(144, 238, 144, 255)); + public readonly static GUIColor ItemQualityColorExcellent = new GUIColor("ItemQualityColorExcellent", new Color(173, 216, 230, 255)); + public readonly static GUIColor ItemQualityColorMasterwork = new GUIColor("ItemQualityColorMasterwork", new Color(147, 112, 219, 255)); - public readonly static GUIColor ColorReputationVeryLow = new GUIColor("ColorReputationVeryLow"); - public readonly static GUIColor ColorReputationLow = new GUIColor("ColorReputationLow"); - public readonly static GUIColor ColorReputationNeutral = new GUIColor("ColorReputationNeutral"); - public readonly static GUIColor ColorReputationHigh = new GUIColor("ColorReputationHigh"); - public readonly static GUIColor ColorReputationVeryHigh = new GUIColor("ColorReputationVeryHigh"); + public readonly static GUIColor ColorReputationVeryLow = new GUIColor("ColorReputationVeryLow", new Color(192, 60, 60, 255)); + public readonly static GUIColor ColorReputationLow = new GUIColor("ColorReputationLow", new Color(203, 145, 23, 255)); + public readonly static GUIColor ColorReputationNeutral = new GUIColor("ColorReputationNeutral", new Color(228, 217, 167, 255)); + public readonly static GUIColor ColorReputationHigh = new GUIColor("ColorReputationHigh", new Color(51, 152, 64, 255)); + public readonly static GUIColor ColorReputationVeryHigh = new GUIColor("ColorReputationVeryHigh", new Color(71, 160, 164, 255)); // Inventory - public readonly static GUIColor EquipmentSlotIconColor = new GUIColor("EquipmentSlotIconColor"); + public readonly static GUIColor EquipmentSlotIconColor = new GUIColor("EquipmentSlotIconColor", new Color(99, 70, 64, 255)); // Health HUD - public readonly static GUIColor BuffColorLow = new GUIColor("BuffColorLow"); - public readonly static GUIColor BuffColorMedium = new GUIColor("BuffColorMedium"); - public readonly static GUIColor BuffColorHigh = new GUIColor("BuffColorHigh"); + public readonly static GUIColor BuffColorLow = new GUIColor("BuffColorLow", new Color(66, 170, 73, 255)); + public readonly static GUIColor BuffColorMedium = new GUIColor("BuffColorMedium", new Color(110, 168, 118, 255)); + public readonly static GUIColor BuffColorHigh = new GUIColor("BuffColorHigh", new Color(154, 213, 163, 255)); - public readonly static GUIColor DebuffColorLow = new GUIColor("DebuffColorLow"); - public readonly static GUIColor DebuffColorMedium = new GUIColor("DebuffColorMedium"); - public readonly static GUIColor DebuffColorHigh = new GUIColor("DebuffColorHigh"); + public readonly static GUIColor DebuffColorLow = new GUIColor("DebuffColorLow", new Color(243, 162, 50, 255)); + public readonly static GUIColor DebuffColorMedium = new GUIColor("DebuffColorMedium", new Color(155, 55, 55, 255)); + public readonly static GUIColor DebuffColorHigh = new GUIColor("DebuffColorHigh", new Color(228, 27, 27, 255)); - public readonly static GUIColor HealthBarColorLow = new GUIColor("HealthBarColorLow"); - public readonly static GUIColor HealthBarColorMedium = new GUIColor("HealthBarColorMedium"); - public readonly static GUIColor HealthBarColorHigh = new GUIColor("HealthBarColorHigh"); - public readonly static GUIColor HealthBarColorPoisoned = new GUIColor("HealthBarColorPoisoned"); + public readonly static GUIColor HealthBarColorLow = new GUIColor("HealthBarColorLow", new Color(255, 0, 0, 255)); + public readonly static GUIColor HealthBarColorMedium = new GUIColor("HealthBarColorMedium", new Color(255, 165, 0, 255)); + public readonly static GUIColor HealthBarColorHigh = new GUIColor("HealthBarColorHigh", new Color(78, 114, 88)); + public readonly static GUIColor HealthBarColorPoisoned = new GUIColor("HealthBarColorPoisoned", new Color(100, 150, 0, 255)); private readonly static Point defaultItemFrameMargin = new Point(50, 56); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs index 168f53a7e..c9b9d0626 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs @@ -461,14 +461,16 @@ namespace Barotrauma } private ImmutableArray cachedCaretPositions = ImmutableArray.Empty; - + //which text were the cached caret positions calculated for? + private string cachedCaretPositionsText; public ImmutableArray GetAllCaretPositions() { - if (cachedCaretPositions.Any()) + string textDrawn = Censor ? CensoredText : Text.SanitizedValue; + if (cachedCaretPositions.Any() && + textDrawn == cachedCaretPositionsText) { return cachedCaretPositions; } - string textDrawn = Censor ? CensoredText : Text.SanitizedValue; float w = Wrap ? (Rect.Width - Padding.X - Padding.Z) / TextScale : float.PositiveInfinity; @@ -482,6 +484,7 @@ namespace Barotrauma .Select(p => p - new Vector2(alignmentXDiff, 0)) .Select(p => p * TextScale + TextPos - Origin * TextScale) .ToImmutableArray(); + cachedCaretPositionsText = textDrawn; return cachedCaretPositions; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index 55fc849e9..600d92bb5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -353,6 +353,10 @@ namespace Barotrauma { CaretIndex = Math.Clamp(CaretIndex, 0, textBlock.Text.Length); var caretPositions = textBlock.GetAllCaretPositions(); + if (CaretIndex >= caretPositions.Length) + { + throw new Exception($"Caret index was outside the bounds of the calculated caret positions. Index: {CaretIndex}, caret positions: {caretPositions.Length}, text: {textBlock.Text}"); + } caretPos = caretPositions[CaretIndex]; caretPosDirty = false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs index e7902d5ca..06a9b05a4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs @@ -784,11 +784,95 @@ namespace Barotrauma #region Static methods public static Pivot MatchPivotToAnchor(Anchor anchor) { - if (!Enum.TryParse(anchor.ToString(), out Pivot pivot)) + return (Pivot)anchor; + } + public static Anchor MatchAnchorToPivot(Pivot pivot) + { + return (Anchor)pivot; + } + + /// + /// Moves the anchor to the left, keeping the vertical position unchanged (e.g. CenterRight -> CenterLeft) + /// + public static Anchor MoveAnchorLeft(Anchor anchor) + { + switch (anchor) { - throw new Exception($"[RectTransform] Cannot match pivot to anchor {anchor}"); + case Anchor.TopCenter: + case Anchor.TopRight: + return Anchor.TopLeft; + case Anchor.Center: + case Anchor.CenterRight: + return Anchor.CenterLeft; + case Anchor.BottomCenter: + case Anchor.BottomRight: + return Anchor.BottomLeft; + default: + return anchor; + } + } + + /// + /// Moves the anchor to the right, keeping the vertical position unchanged (e.g. CenterLeft -> CenterRight) + /// + public static Anchor MoveAnchorRight(Anchor anchor) + { + switch (anchor) + { + case Anchor.TopCenter: + case Anchor.TopLeft: + return Anchor.TopRight; + case Anchor.Center: + case Anchor.CenterLeft: + return Anchor.CenterRight; + case Anchor.BottomCenter: + case Anchor.BottomLeft: + return Anchor.BottomRight; + default: + return anchor; + } + } + + /// + /// Moves the anchor to the top, keeping the horizontal position unchanged (e.g. BottomCenter -> TopCenter) + /// + public static Anchor MoveAnchorTop(Anchor anchor) + { + switch (anchor) + { + case Anchor.CenterLeft: + case Anchor.BottomLeft: + return Anchor.TopLeft; + case Anchor.Center: + case Anchor.BottomCenter: + return Anchor.TopCenter; + case Anchor.CenterRight: + case Anchor.BottomRight: + return Anchor.TopRight; + default: + return anchor; + } + } + + /// + /// Moves the anchor to the bottom, keeping the horizontal position unchanged (e.g. TopCenter -> BottomCenter) + /// + public static Anchor MoveAnchorBottom(Anchor anchor) + { + switch (anchor) + { + case Anchor.CenterLeft: + case Anchor.TopLeft: + return Anchor.BottomLeft; + case Anchor.Center: + case Anchor.TopCenter: + return Anchor.BottomCenter; + case Anchor.CenterRight: + case Anchor.TopRight: + return Anchor.BottomRight; + default: + return anchor; } - return pivot; } /// @@ -811,11 +895,11 @@ namespace Barotrauma } } - public static Point CalculatePivotOffset(Pivot pivot, Point size) + public static Point CalculatePivotOffset(Pivot anchor, Point size) { int width = size.X; int height = size.Y; - switch (pivot) + switch (anchor) { case Pivot.TopLeft: return Point.Zero; @@ -836,7 +920,7 @@ namespace Barotrauma case Pivot.BottomRight: return new Point(-width, -height); default: - throw new NotImplementedException(pivot.ToString()); + throw new NotImplementedException(anchor.ToString()); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index 34b935f84..5264b13b5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -40,6 +40,8 @@ namespace Barotrauma private readonly List itemsToSell = new List(); private readonly List itemsToSellFromSub = new List(); + private GUIMessageBox deliveryPrompt; + private StoreTab activeTab = StoreTab.Buy; private MapEntityCategory? selectedItemCategory; private bool suppressBuySell; @@ -341,9 +343,9 @@ namespace Barotrauma }; var panelMaxWidth = (int)(GUI.xScale * (GUI.HorizontalAspectRatio < 1.4f ? 650 : 560)); - var storeContent = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 1.0f), campaignUI.GetTabContainer(CampaignMode.InteractionType.Store).RectTransform) + var storeContent = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 1.0f), campaignUI.GetTabContainer(CampaignMode.InteractionType.Store).RectTransform, Anchor.BottomLeft) { - MaxSize = new Point(panelMaxWidth, campaignUI.GetTabContainer(CampaignMode.InteractionType.Store).Rect.Height) + MaxSize = new Point(panelMaxWidth, campaignUI.GetTabContainer(CampaignMode.InteractionType.Store).Rect.Height - HUDLayoutSettings.ButtonAreaTop.Bottom) }) { Stretch = true, @@ -583,9 +585,9 @@ namespace Barotrauma // Shopping Crate ------------------------------------------------------------------------------------------------------------------------------------------ - var shoppingCrateContent = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 1.0f), campaignUI.GetTabContainer(CampaignMode.InteractionType.Store).RectTransform, anchor: Anchor.TopRight) + var shoppingCrateContent = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 1.0f), campaignUI.GetTabContainer(CampaignMode.InteractionType.Store).RectTransform, anchor: Anchor.BottomRight) { - MaxSize = new Point(panelMaxWidth, campaignUI.GetTabContainer(CampaignMode.InteractionType.Store).Rect.Height) + MaxSize = new Point(panelMaxWidth, campaignUI.GetTabContainer(CampaignMode.InteractionType.Store).Rect.Height - HUDLayoutSettings.ButtonAreaTop.Bottom) }) { Stretch = true, @@ -922,15 +924,12 @@ namespace Barotrauma { if (itemPrefab.CanBeBoughtFrom(ActiveStore, out PriceInfo priceInfo) && itemPrefab.CanCharacterBuy()) { - bool isDailySpecial = ActiveStore.DailySpecials.Contains(itemPrefab); var itemFrame = isDailySpecial ? storeDailySpecialsGroup.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab == itemPrefab) : storeBuyList.Content.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab == itemPrefab); - if (CargoManager.GetPurchasedItem(ActiveStore, itemPrefab) is { } purchasedItem) - { - quantity = Math.Max(quantity - purchasedItem.Quantity, 0); - } + + quantity = Math.Max(quantity - CargoManager.GetPurchasedItemCount(ActiveStore, itemPrefab), 0); if (CargoManager.GetBuyCrateItem(ActiveStore, itemPrefab) is { } buyCrateItem) { quantity = Math.Max(quantity - buyCrateItem.Quantity, 0); @@ -1245,9 +1244,9 @@ namespace Barotrauma int totalPrice = 0; if (ActiveStore != null) { - foreach (PurchasedItem item in items) + foreach (PurchasedItem item in items.ToList()) { - if (!(item.ItemPrefab.GetPriceInfo(ActiveStore) is { } priceInfo)) { continue; } + if (item.ItemPrefab.GetPriceInfo(ActiveStore) is not { } priceInfo) { continue; } GUINumberInput numInput = null; if (!(listBox.Content.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab.Identifier == item.ItemPrefab.Identifier) is { } itemFrame)) { @@ -1749,7 +1748,7 @@ namespace Barotrauma } // Add items already purchased - CargoManager?.GetPurchasedItems(ActiveStore).ForEach(pi => AddNonEmptyOwnedItems(pi)); + CargoManager?.GetPurchasedItems(ActiveStore).Where(pi => !pi.DeliverImmediately).ForEach(pi => AddNonEmptyOwnedItems(pi)); ownedItemsUpdateTimer = 0.0f; @@ -1959,14 +1958,13 @@ namespace Barotrauma } catch (NotImplementedException e) { - DebugConsole.LogError($"Error getting item availability: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}"); + DebugConsole.LogError($"Error getting item availability: Unknown store tab type. {e.StackTrace.CleanupStackTrace()}"); } if (list != null && list.Find(i => i.ItemPrefab == itemPrefab) is PurchasedItem item) { if (mode == StoreTab.Buy) { - var purchasedItem = CargoManager.GetPurchasedItem(ActiveStore, item.ItemPrefab); - if (purchasedItem != null) { return Math.Max(item.Quantity - purchasedItem.Quantity, 0); } + return Math.Max(item.Quantity - CargoManager.GetPurchasedItemCount(ActiveStore, item.ItemPrefab), 0); } return item.Quantity; } @@ -2093,16 +2091,57 @@ namespace Barotrauma } itemsToRemove.ForEach(i => itemsToPurchase.Remove(i)); if (itemsToPurchase.None() || Balance < totalPrice) { return false; } - CargoManager.PurchaseItems(ActiveStore.Identifier, itemsToPurchase, true); - GameMain.Client?.SendCampaignState(); - var dialog = new GUIMessageBox( - TextManager.Get("newsupplies"), - TextManager.GetWithVariable("suppliespurchasedmessage", "[location]", campaignUI?.Campaign?.Map?.CurrentLocation?.Name), - new LocalizedString[] { TextManager.Get("Ok") }); - dialog.Buttons[0].OnClicked += dialog.Close; + + if (CampaignMode.AllowImmediateItemDelivery()) + { + deliveryPrompt = new GUIMessageBox( + TextManager.Get("newsupplies"), + TextManager.Get("suppliespurchased.deliverymethod"), + new LocalizedString[] + { + TextManager.Get("suppliespurchased.deliverymethod.deliverimmediately"), + TextManager.Get("suppliespurchased.deliverymethod.delivertosub") + }); + deliveryPrompt.Buttons[0].OnClicked = (btn, userdata) => + { + ConfirmPurchase(deliverImmediately: true); + deliveryPrompt.Close(); + return true; + }; + deliveryPrompt.Buttons[1].OnClicked = (btn, userdata) => + { + ConfirmPurchase(deliverImmediately: false); + deliveryPrompt.Close(); + return true; + }; + } + else + { + ConfirmPurchase(deliverImmediately: false); + } + + void ConfirmPurchase(bool deliverImmediately) + { + itemsToPurchase.ForEach(it => it.DeliverImmediately = deliverImmediately); + CargoManager.PurchaseItems(ActiveStore.Identifier, itemsToPurchase, removeFromCrate: true); + GameMain.Client?.SendCampaignState(); + if (!deliverImmediately) + { + var dialog = new GUIMessageBox( + TextManager.Get("newsupplies"), + TextManager.GetWithVariable("suppliespurchasedmessage", "[location]", campaignUI?.Campaign?.Map?.CurrentLocation?.DisplayName)); + dialog.Buttons[0].OnClicked += dialog.Close; + } + } return false; } + public void OnDeselected() + { + deliveryPrompt?.Close(); + deliveryPrompt = null; + } + private bool SellItems() { if (!HasActiveTabPermissions()) { return false; } @@ -2118,7 +2157,7 @@ namespace Barotrauma } catch (NotImplementedException e) { - DebugConsole.LogError($"Error confirming the store transaction: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}"); + DebugConsole.LogError($"Error confirming the store transaction: Unknown store tab type. {e.StackTrace.CleanupStackTrace()}"); return false; } var itemsToRemove = new List(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs index 64f9c9ac9..325b3d3f5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -130,7 +130,7 @@ namespace Barotrauma }; content = new GUILayoutGroup(new RectTransform(new Point(background.Rect.Width - HUDLayoutSettings.Padding * 4, background.Rect.Height - HUDLayoutSettings.Padding * 4), background.RectTransform, Anchor.Center)) { AbsoluteSpacing = (int)(HUDLayoutSettings.Padding * 1.5f) }; - GUITextBlock header = new GUITextBlock(new RectTransform(new Vector2(1f, 0.0f), content.RectTransform), transferService ? TextManager.Get("switchsubmarineheader") : TextManager.GetWithVariable("outpostshipyard", "[location]", GameMain.GameSession.Map.CurrentLocation.Name), font: GUIStyle.LargeFont); + GUITextBlock header = new GUITextBlock(new RectTransform(new Vector2(1f, 0.0f), content.RectTransform), transferService ? TextManager.Get("switchsubmarineheader") : TextManager.GetWithVariable("outpostshipyard", "[location]", GameMain.GameSession.Map.CurrentLocation.DisplayName), font: GUIStyle.LargeFont); header.CalculateHeightFromText(0, true); playerBalanceElement = CampaignUI.AddBalanceElement(header, new Vector2(1.0f, 1.5f)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 24362e63c..4e78f979b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -165,6 +165,11 @@ namespace Barotrauma public TabMenu() { if (!initialized) { Initialize(); } + if (Level.Loaded == null) + { + //make sure we're not trying to view e.g. mission or reputation info if the tab menu is opened in the test mode + SelectedTab = InfoFrameTab.Crew; + } CreateInfoFrame(SelectedTab); SelectInfoFrameTab(SelectedTab); } @@ -303,7 +308,7 @@ namespace Barotrauma { var missionBtn = createTabButton(InfoFrameTab.Mission, "mission"); eventLogNotification = GameSession.CreateNotificationIcon(missionBtn); - eventLogNotification.Visible = GameMain.GameSession.EventManager?.EventLog?.UnreadEntries ?? false; + eventLogNotification.Visible = GameMain.GameSession?.EventManager?.EventLog?.UnreadEntries ?? false; if (eventLogNotification.Visible) { eventLogNotification.Pulsate(Vector2.One, Vector2.One * 2, 1.0f); @@ -1508,7 +1513,7 @@ namespace Barotrauma portraitImage.RectTransform.NonScaledSize = new Point(Math.Min((int)(portraitImage.Rect.Size.Y * portraitAspectRatio), portraitImage.Rect.Width), portraitImage.Rect.Size.Y); } - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), locationInfoContainer.RectTransform), location.Name, font: GUIStyle.LargeFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), locationInfoContainer.RectTransform), location.DisplayName, font: GUIStyle.LargeFont); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), locationInfoContainer.RectTransform), location.Type.Name, font: GUIStyle.SubHeadingFont); if (location.Faction?.Prefab != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs index 90c860332..1e8b12e76 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs @@ -163,7 +163,7 @@ namespace Barotrauma else if (Tile) { Vector2 startPos = new Vector2(rect.X, rect.Y); - Sprite.DrawTiled(spriteBatch, startPos, new Vector2(rect.Width, rect.Height), color, startOffset: uvOffset); + Sprite.DrawTiled(spriteBatch, startPos, new Vector2(rect.Width, rect.Height), color: color, startOffset: uvOffset); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index cff4b0a2f..3a8e2cb68 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -1110,7 +1110,7 @@ namespace Barotrauma public static UpgradeFrame CreateUpgradeFrame(UpgradePrefab prefab, UpgradeCategory category, CampaignMode campaign, RectTransform rectTransform, bool addBuyButton = true) { - int price = prefab.Price.GetBuyPrice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation, characterList); + int price = prefab.Price.GetBuyPrice(prefab, campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation, characterList); return CreateUpgradeEntry(rectTransform, prefab.Sprite, prefab.Name, prefab.Description, price, new CategoryData(category, prefab), addBuyButton, upgradePrefab: prefab, currentLevel: campaign.UpgradeManager.GetUpgradeLevel(prefab, category)); } @@ -1267,7 +1267,7 @@ namespace Barotrauma { LocalizedString promptBody = TextManager.GetWithVariables("Upgrades.PurchasePromptBody", ("[upgradename]", prefab.Name), - ("[amount]", prefab.Price.GetBuyPrice(Campaign.UpgradeManager.GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation, characterList).ToString())); + ("[amount]", prefab.Price.GetBuyPrice(prefab, Campaign.UpgradeManager.GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation, characterList).ToString())); currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), promptBody, () => { if (GameMain.NetworkMember != null) @@ -1682,7 +1682,7 @@ namespace Barotrauma GUITextBlock priceLabel = (GUITextBlock)buttonParent.FindChild(UpgradeStoreUserData.PriceLabel, recursive: true); priceLabel.Visible = true; - int price = prefab.Price.GetBuyPrice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation, characterList); + int price = prefab.Price.GetBuyPrice(prefab, campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation, characterList); if (!WaitForServerUpdate) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 4f4e5a284..50dbc4178 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -123,6 +123,10 @@ namespace Barotrauma private Viewport defaultViewport; + /// + /// NOTE: Use very carefully. You need to ensure that you ALWAYS unsubscribe from this when you no longer need the subscriber! + /// If you're subscribing to this from something else than a singleton or something that there's only ever one instance of, you're probably in dangerous territory. + /// public event Action ResolutionChanged; private bool exiting; @@ -404,7 +408,7 @@ namespace Barotrauma //do this here because we need it for the loading screen WaterRenderer.Instance = new WaterRenderer(base.GraphicsDevice); - Quad.Init(GraphicsDevice); + GraphicsQuad.Init(GraphicsDevice); loadingScreenOpen = true; TitleScreen = new LoadingScreen(GraphicsDevice) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs index 4655e13d5..0d1df98e3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs @@ -147,7 +147,7 @@ namespace Barotrauma } catch (NotImplementedException e) { - DebugConsole.LogError($"Error selling items: uknown store tab type \"{sellingMode}\".\n{e.StackTrace.CleanupStackTrace()}"); + DebugConsole.LogError($"Error selling items: unknown store tab type \"{sellingMode}\".\n{e.StackTrace.CleanupStackTrace()}"); return; } bool canAddToRemoveQueue = campaign.IsSinglePlayer && Entity.Spawner != null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index 28556fd9d..feb6eb7d9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -72,6 +72,9 @@ namespace Barotrauma case InteractionType.MedicalClinic: CampaignUI.MedicalClinic?.OnDeselected(); break; + case InteractionType.Store: + CampaignUI.Store?.OnDeselected(); + break; } } @@ -121,6 +124,16 @@ namespace Barotrauma { return AllowedToManageCampaign(ClientPermissions.ManageMoney); } + + public static bool AllowImmediateItemDelivery() + { + if (GameMain.Client == null) { return true; } + return + GameMain.Client.ServerSettings.AllowImmediateItemDelivery || + GameMain.Client.HasPermission(ClientPermissions.ManageCampaign) || + GameMain.Client.IsServerOwner; + } + protected GUIButton CreateEndRoundButton() { int buttonWidth = (int)(450 * GUI.xScale * (GUI.IsUltrawide ? 3.0f : 1.0f)); @@ -182,12 +195,12 @@ namespace Barotrauma if (Level.Loaded.EndOutpost == null || !Level.Loaded.EndOutpost.DockedTo.Contains(leavingSub)) { string textTag = availableTransition == TransitionType.ProgressToNextLocation ? "EnterLocation" : "EnterEmptyLocation"; - buttonText = TextManager.GetWithVariable(textTag, "[locationname]", Level.Loaded.EndLocation?.Name ?? "[ERROR]"); + buttonText = TextManager.GetWithVariable(textTag, "[locationname]", Level.Loaded.EndLocation?.DisplayName ?? "[ERROR]"); allowEndingRound = !ForceMapUI && !ShowCampaignUI; } break; case TransitionType.LeaveLocation: - buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); + buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.DisplayName ?? "[ERROR]"); allowEndingRound = !ForceMapUI && !ShowCampaignUI; break; case TransitionType.ReturnToPreviousLocation: @@ -195,7 +208,7 @@ namespace Barotrauma if (Level.Loaded.StartOutpost == null || !Level.Loaded.StartOutpost.DockedTo.Contains(leavingSub)) { string textTag = availableTransition == TransitionType.ReturnToPreviousLocation ? "EnterLocation" : "EnterEmptyLocation"; - buttonText = TextManager.GetWithVariable(textTag, "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); + buttonText = TextManager.GetWithVariable(textTag, "[locationname]", Level.Loaded.StartLocation?.DisplayName ?? "[ERROR]"); allowEndingRound = !ForceMapUI && !ShowCampaignUI; } break; @@ -211,7 +224,7 @@ namespace Barotrauma endRoundButton.Color = GUIStyle.Red * 0.7f; endRoundButton.HoverColor = GUIStyle.Red; } - buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); + buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.DisplayName ?? "[ERROR]"); allowEndingRound = !ForceMapUI && !ShowCampaignUI; } else diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index a3be9801c..ce6972067 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -535,7 +535,7 @@ namespace Barotrauma bool refreshCampaignUI = false; - if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaignID != campaign.CampaignID) + if (GameMain.GameSession?.GameMode is not MultiPlayerCampaign campaign || campaignID != campaign.CampaignID) { string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer); @@ -614,7 +614,7 @@ namespace Barotrauma { if (availableMission.ConnectionIndex < 0 || availableMission.ConnectionIndex >= campaign.Map.CurrentLocation.Connections.Count) { - DebugConsole.ThrowError($"Error when receiving campaign data from the server: connection index for mission \"{availableMission.Identifier}\" out of range (index: {availableMission.ConnectionIndex}, current location: {campaign.Map.CurrentLocation.Name}, connections: {campaign.Map.CurrentLocation.Connections.Count})."); + DebugConsole.ThrowError($"Error when receiving campaign data from the server: connection index for mission \"{availableMission.Identifier}\" out of range (index: {availableMission.ConnectionIndex}, current location: {campaign.Map.CurrentLocation.DisplayName}, connections: {campaign.Map.CurrentLocation.Connections.Count})."); continue; } LocationConnection connection = campaign.Map.CurrentLocation.Connections[availableMission.ConnectionIndex]; @@ -647,7 +647,15 @@ namespace Barotrauma { if (ownedSubIndex >= GameMain.Client.ServerSubmarines.Count) { - string errorMsg = $"Error in {nameof(MultiPlayerCampaign.ClientRead)}. Owned submarine index was out of bounds. Index: {ownedSubIndex}, submarines: {string.Join(", ", GameMain.Client.ServerSubmarines.Select(s => s.Name))}"; + string errorMsg; + if (GameMain.Client.ServerSubmarines.None()) + { + errorMsg = $"Error in {nameof(MultiPlayerCampaign.ClientRead)}. Owned submarine index was out of bounds (list of server submarines is empty)."; + } + else + { + errorMsg = $"Error in {nameof(MultiPlayerCampaign.ClientRead)}. Owned submarine index was out of bounds. Index: {ownedSubIndex}, submarines: {string.Join(", ", GameMain.Client.ServerSubmarines.Select(s => s.Name))}"; + } DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce( "MultiPlayerCampaign.ClientRead.OwnerSubIndexOutOfBounds" + ownedSubIndex, @@ -822,11 +830,12 @@ namespace Barotrauma UInt16 id = msg.ReadUInt16(); bool hasCharacterData = msg.ReadBoolean(); CharacterInfo myCharacterInfo = null; + bool waitForModsDownloaded = Screen.Selected is ModDownloadScreen; if (hasCharacterData) { - myCharacterInfo = CharacterInfo.ClientRead(CharacterPrefab.HumanSpeciesName, msg); + myCharacterInfo = CharacterInfo.ClientRead(CharacterPrefab.HumanSpeciesName, msg, requireJobPrefabFound: !waitForModsDownloaded); } - if (ShouldApply(NetFlags.CharacterInfo, id, requireUpToDateSave: true)) + if (!waitForModsDownloaded && ShouldApply(NetFlags.CharacterInfo, id, requireUpToDateSave: true)) { if (myCharacterInfo != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index fa274ad8f..cc4695375 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -548,9 +548,12 @@ namespace Barotrauma } else { - //wasn't initially docked (sub doesn't have a docking port?) - // -> choose a destination when the sub is far enough from the start outpost - if (!Submarine.MainSub.AtStartExit && !Level.Loaded.StartOutpost.ExitPoints.Any()) + //force the map to open if the sub is somehow not at the start of the outpost level + //UNLESS the level has specific exit points, in that case the sub needs to get to those + if (!Submarine.MainSub.AtStartExit && + /*there should normally always be a start outpost in outpost levels, + * but that might not always be the case e.g. mods or outdated saves (see #13042)*/ + Level.Loaded.StartOutpost is not { ExitPoints.Count: > 0 }) { ForceMapUI = true; CampaignUI.SelectTab(InteractionType.Map); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs index 9c42d6beb..7ce88ca26 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs @@ -48,6 +48,8 @@ namespace Barotrauma private GUIImage eventLogNotification; + private Point prevTopLeftButtonsResolution; + private void CreateTopLeftButtons() { if (topLeftButtonGroup != null) @@ -61,10 +63,6 @@ namespace Barotrauma AbsoluteSpacing = HUDLayoutSettings.Padding, CanBeFocused = false }; - topLeftButtonGroup.RectTransform.ParentChanged += (_) => - { - GameMain.Instance.ResolutionChanged -= CreateTopLeftButtons; - }; int buttonHeight = GUI.IntScale(40); Vector2 buttonSpriteSize = GUIStyle.GetComponentStyle("CrewListToggleButton").GetDefaultSprite().size; int buttonWidth = (int)((buttonHeight / buttonSpriteSize.Y) * buttonSpriteSize.X); @@ -98,8 +96,6 @@ namespace Barotrauma talentPointNotification = CreateNotificationIcon(tabMenuButton); eventLogNotification = CreateNotificationIcon(tabMenuButton); - GameMain.Instance.ResolutionChanged += CreateTopLeftButtons; - respawnInfoFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 1.0f), parent: topLeftButtonGroup.RectTransform) { MaxSize = new Point(HUDLayoutSettings.ButtonAreaTop.Width / 3, int.MaxValue) }, style: null) { @@ -121,6 +117,7 @@ namespace Barotrauma return true; } }; + prevTopLeftButtonsResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); } public void AddToGUIUpdateList() @@ -133,7 +130,8 @@ namespace Barotrauma if ((GameMode is not CampaignMode campaign || (!campaign.ForceMapUI && !campaign.ShowCampaignUI)) && !CoroutineManager.IsCoroutineRunning("LevelTransition") && !CoroutineManager.IsCoroutineRunning("SubmarineTransition")) { - if (topLeftButtonGroup == null) + if (topLeftButtonGroup == null || + prevTopLeftButtonsResolution.X != GameMain.GraphicsWidth || prevTopLeftButtonsResolution.Y != GameMain.GraphicsHeight) { CreateTopLeftButtons(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ObjectiveManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ObjectiveManager.cs index 0a70bc1d9..33fc06086 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ObjectiveManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ObjectiveManager.cs @@ -139,6 +139,11 @@ static class ObjectiveManager VideoPlayer.AddToGUIUpdateList(order: 100); } + public static bool IsSegmentActive(Identifier segmentId) + { + return activeObjectives.Any(o => o.Id == segmentId); + } + public static void TriggerSegment(Segment segment, bool connectObjective = false) { if (segment.SegmentType != SegmentType.InfoBox) @@ -361,9 +366,18 @@ static class ObjectiveManager activeObjectives.IndexOf(parentSegment) + activeObjectives.Count(s => s.ParentId == segment.ParentId); if (objectiveGroup.RectTransform.GetChildIndex(frameRt) != childIndex) { - frameRt.RepositionChildInHierarchy(childIndex); - activeObjectives.Remove(segment); - activeObjectives.Insert(childIndex, segment); + if (childIndex < 0 || childIndex >= frameRt.Parent.CountChildren) + { + DebugConsole.ThrowError( + $"Error in {nameof(ObjectiveManager.AddToObjectiveList)}. " + + $"Failed to reposition an objective in the list. Text \"{segment.ObjectiveText}\", parentId: {segment.ParentId}, childIndex: {childIndex}"); + } + else + { + frameRt.RepositionChildInHierarchy(childIndex); + activeObjectives.Remove(segment); + activeObjectives.Insert(childIndex, segment); + } } } frameRt.AbsoluteOffset = GetObjectiveHiddenPosition(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index 33f02d0fe..068477a41 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -564,7 +564,7 @@ namespace Barotrauma private LocalizedString GetHeaderText(bool gameOver, CampaignMode.TransitionType transitionType) { - string locationName = Submarine.MainSub is { AtEndExit: true } ? endLocation?.Name : startLocation?.Name; + LocalizedString locationName = Submarine.MainSub is { AtEndExit: true } ? endLocation?.DisplayName : startLocation?.DisplayName; string textTag; if (gameOver) @@ -576,23 +576,23 @@ namespace Barotrauma switch (transitionType) { case CampaignMode.TransitionType.LeaveLocation: - locationName = startLocation?.Name; + locationName = startLocation?.DisplayName; textTag = "RoundSummaryLeaving"; break; case CampaignMode.TransitionType.ProgressToNextLocation: - locationName = endLocation?.Name; + locationName = endLocation?.DisplayName; textTag = "RoundSummaryProgress"; break; case CampaignMode.TransitionType.ProgressToNextEmptyLocation: - locationName = endLocation?.Name; + locationName = endLocation?.DisplayName; textTag = "RoundSummaryProgressToEmptyLocation"; break; case CampaignMode.TransitionType.ReturnToPreviousLocation: - locationName = startLocation?.Name; + locationName = startLocation?.DisplayName; textTag = "RoundSummaryReturn"; break; case CampaignMode.TransitionType.ReturnToPreviousEmptyLocation: - locationName = startLocation?.Name; + locationName = startLocation?.DisplayName; textTag = "RoundSummaryReturnToEmptyLocation"; break; default: @@ -603,14 +603,14 @@ namespace Barotrauma if (startLocation?.Biome != null && startLocation.Biome.IsEndBiome) { - locationName ??= startLocation.Name; + locationName ??= startLocation.DisplayName; } if (textTag == null) { return ""; } if (locationName == null) { - DebugConsole.ThrowError($"Error while creating round summary: could not determine destination location. Start location: {startLocation?.Name ?? "null"}, end location: {endLocation?.Name ?? "null"}"); + DebugConsole.ThrowError($"Error while creating round summary: could not determine destination location. Start location: {startLocation?.DisplayName ?? "null"}, end location: {endLocation?.DisplayName ?? "null"}"); locationName = "[UNKNOWN]"; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 54b27c3e0..705b59f8f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -568,7 +568,7 @@ namespace Barotrauma itemContainer.KeepOpenWhenEquippedBy(character) && !DraggingItems.Contains(item) && character.CanAccessInventory(itemContainer.Inventory) && - !highlightedSubInventorySlots.Any(s => s.Inventory == itemContainer.Inventory)) + !highlightedSubInventorySlots.Any(s => s.Inventory == itemContainer.Inventory && s.SlotIndex == i)) { ShowSubInventory(new SlotReference(this, visualSlots[i], i, false, itemContainer.Inventory), deltaTime, cam, hideSubInventories, true); } @@ -709,11 +709,11 @@ namespace Barotrauma private void ShowSubInventory(SlotReference slotRef, float deltaTime, Camera cam, List hideSubInventories, bool isEquippedSubInventory) { Rectangle hoverArea = GetSubInventoryHoverArea(slotRef); - if (isEquippedSubInventory) + if (isEquippedSubInventory && slotRef.Inventory is not ItemInventory { Container.MovableFrame: true, Container.KeepOpenWhenEquipped: true }) { foreach (SlotReference highlightedSubInventorySlot in highlightedSubInventorySlots) { - if (highlightedSubInventorySlot == slotRef) continue; + if (highlightedSubInventorySlot == slotRef) { continue; } if (hoverArea.Intersects(GetSubInventoryHoverArea(highlightedSubInventorySlot))) { return; // If an equipped one intersects with a currently active hover one, do not open @@ -818,7 +818,8 @@ namespace Barotrauma if (selectedContainer != null && selectedContainer.Inventory != null && - !selectedContainer.Inventory.Locked && + !selectedContainer.Inventory.Locked && + selectedContainer.DrawInventory && allowInventorySwap) { //player has selected the inventory of another item -> attempt to move the item there @@ -841,6 +842,7 @@ namespace Barotrauma } else if (character.HeldItems.FirstOrDefault(i => i.OwnInventory != null && + i.OwnInventory.Container.DrawInventory && (i.OwnInventory.CanBePut(item) || ((i.OwnInventory.Capacity == 1 || i.OwnInventory.Container.HasSubContainers) && i.OwnInventory.AllowSwappingContainedItems && i.OwnInventory.Container.CanBeContained(item)))) is { } equippedContainer) { if (allowEquip) @@ -1027,7 +1029,7 @@ namespace Barotrauma //order by the condition of the contained item to prefer putting into the item with the emptiest ammo/battery/tank foreach (Item heldItem in character.HeldItems.OrderByDescending(heldItem => GetContainPriority(item, heldItem))) { - if (heldItem.OwnInventory == null) { continue; } + if (heldItem.OwnInventory == null || !heldItem.OwnInventory.Container.DrawInventory) { continue; } //don't allow swapping if we're moving items into an item with 1 slot holding a stack of items //(in that case, the quick action should just fill up the stack) bool disallowSwapping = diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs index 590e908f9..7d5981608 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs @@ -54,7 +54,9 @@ namespace Barotrauma.Items.Components { if (deconstructor.InputContainer.Inventory.AllItems.Count() == 2) { - if (!deconstructor.InputContainer.Inventory.AllItems.All(it => it.Prefab == item.Prefab)) + var otherGeneticMaterial = + deconstructor.InputContainer.Inventory.AllItems.FirstOrDefault(it => it != item && it.Prefab == item.Prefab)?.GetComponent(); + if (otherGeneticMaterial == null) { buttonText = TextManager.Get("researchstation.combine"); infoText = TextManager.Get("researchstation.combine.infotext"); @@ -62,7 +64,7 @@ namespace Barotrauma.Items.Components else { buttonText = TextManager.Get("researchstation.refine"); - int taintedProbability = (int)(GetTaintedProbabilityOnRefine(Character.Controlled) * 100); + int taintedProbability = (int)(GetTaintedProbabilityOnRefine(otherGeneticMaterial, Character.Controlled) * 100); infoText = TextManager.GetWithVariable("researchstation.refine.infotext", "[taintedprobability]", taintedProbability.ToString()); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index 32dca442a..e9f8847e5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -260,6 +260,12 @@ namespace Barotrauma.Items.Components if (!hasSoundsOfType[(int)type]) { return; } if (GameMain.Client?.MidRoundSyncing ?? false) { return; } + //above the top boundary of the level (in an inactive respawn shuttle?) + if (item.Submarine != null && Level.Loaded != null && item.Submarine.WorldPosition.Y > Level.Loaded.Size.Y) + { + return; + } + if (loopingSound != null) { if (Vector3.DistanceSquared(GameMain.SoundManager.ListenerPosition, new Vector3(item.WorldPosition, 0.0f)) > loopingSound.Range * loopingSound.Range || @@ -388,12 +394,9 @@ namespace Barotrauma.Items.Components } } - public void StopSounds(ActionType type) + public void StopLoopingSound() { if (loopingSound == null) { return; } - - if (loopingSound.Type != type) { return; } - if (loopingSoundChannel != null) { loopingSoundChannel.FadeOutAndDispose(); @@ -402,6 +405,12 @@ namespace Barotrauma.Items.Components } } + public void StopSounds(ActionType type) + { + if (loopingSound == null || loopingSound.Type != type) { return; } + StopLoopingSound(); + } + private float GetSoundVolume(ItemSound sound) { if (sound == null) { return 0.0f; } @@ -497,7 +506,8 @@ namespace Barotrauma.Items.Components case "guiframe": if (subElement.GetAttribute("rect") != null) { - DebugConsole.ThrowError($"Error in item config \"{item.ConfigFilePath}\" - GUIFrame defined as rect, use RectTransform instead."); + DebugConsole.ThrowError($"Error in item config \"{item.ConfigFilePath}\" - GUIFrame defined as rect, use RectTransform instead.", + contentPackage: subElement.ContentPackage); break; } GuiFrameSource = subElement; @@ -516,7 +526,8 @@ namespace Barotrauma.Items.Components if (filePath.IsNullOrEmpty()) { DebugConsole.ThrowError( - $"Error when instantiating item \"{item.Name}\" - sound with no file path set"); + $"Error when instantiating item \"{item.Name}\" - sound with no file path set", + contentPackage: subElement.ContentPackage); break; } @@ -528,7 +539,8 @@ namespace Barotrauma.Items.Components } catch (Exception e) { - DebugConsole.ThrowError($"Invalid sound type \"{typeStr}\" in item \"{item.Prefab.Identifier}\"!", e); + DebugConsole.ThrowError($"Invalid sound type \"{typeStr}\" in item \"{item.Prefab.Identifier}\"!", e, + contentPackage: subElement.ContentPackage); break; } @@ -758,6 +770,8 @@ namespace Barotrauma.Items.Components } } + public virtual void OnPlayerSkillsChanged() { } + public virtual void AddTooltipInfo(ref LocalizedString name, ref LocalizedString description) { } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index 830703d01..21b3c9c76 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -158,7 +158,8 @@ namespace Barotrauma.Items.Components IndicatorStyle = GUIStyle.GetComponentStyle("ContainedStateIndicator." + ContainedStateIndicatorStyle); if (ContainedStateIndicator != null || ContainedStateIndicatorEmpty != null) { - DebugConsole.AddWarning($"Item \"{item.Name}\" defines both a contained state indicator style and a custom indicator sprite. Will use the custom sprite..."); + DebugConsole.AddWarning($"Item \"{item.Name}\" defines both a contained state indicator style and a custom indicator sprite. Will use the custom sprite...", + contentPackage: item.Prefab.ContentPackage); } } if (GuiFrame == null) @@ -345,9 +346,9 @@ namespace Barotrauma.Items.Components public bool KeepOpenWhenEquippedBy(Character character) { - if (!character.CanAccessInventory(Inventory) || - !KeepOpenWhenEquipped || - !character.HasEquippedItem(Item)) + if (!KeepOpenWhenEquipped || + !character.HasEquippedItem(Item) || + !character.CanAccessInventory(Inventory)) { return false; } @@ -570,11 +571,13 @@ namespace Barotrauma.Items.Components { spriteRotation = contained.Rotation; } - if ((item.body != null && item.body.Dir == -1) || item.FlippedX) + bool flipX = (item.body != null && item.body.Dir == -1) || item.FlippedX; + if (flipX) { spriteEffects |= MathUtils.NearlyEqual(spriteRotation % 180, 90.0f) ? SpriteEffects.FlipVertically : SpriteEffects.FlipHorizontally; } - if (item.FlippedY) + bool flipY = item.FlippedY; + if (flipY) { spriteEffects |= MathUtils.NearlyEqual(spriteRotation % 180, 90.0f) ? SpriteEffects.FlipHorizontally : SpriteEffects.FlipVertically; } @@ -588,6 +591,7 @@ 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); foreach (ItemContainer ic in contained.Item.GetComponents()) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs index a6ff486bb..7f81d3a3c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs @@ -227,7 +227,7 @@ namespace Barotrauma.Items.Components switch (text) { case "[CurrentLocationName]": - SetDisplayText(Level.Loaded?.StartLocation?.Name ?? string.Empty); + SetDisplayText(Level.Loaded?.StartLocation?.DisplayName.Value ?? string.Empty); break; case "[CurrentBiomeName]": SetDisplayText(Level.Loaded?.LevelData?.Biome?.DisplayName.Value ?? string.Empty); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs index 540bc1759..9e4c1b164 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -67,7 +67,7 @@ namespace Barotrauma.Items.Components Light.Position = item.Position; } PhysicsBody body = Light.ParentBody; - if (body != null) + if (body != null && body.Enabled) { Light.Rotation = body.Dir > 0.0f ? body.DrawRotation : body.DrawRotation - MathHelper.Pi; Light.LightSpriteEffect = (body.Dir > 0.0f) ? SpriteEffects.None : SpriteEffects.FlipVertically; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs index 8c3d920c6..24fbcfb36 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs @@ -66,7 +66,11 @@ namespace Barotrauma.Items.Components new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), sliderArea.RectTransform, Anchor.TopCenter), "", textColor: GUIStyle.TextColorNormal, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center) { AutoScaleHorizontal = true, - TextGetter = () => { return TextManager.AddPunctuation(':', powerLabel, (int)(targetForce) + " %"); } + TextGetter = () => + { + return TextManager.AddPunctuation(':', powerLabel, + TextManager.GetWithVariable("percentageformat", "[value]", ((int)MathF.Round(targetForce)).ToString())); + } }; forceSlider = new GUIScrollBar(new RectTransform(new Vector2(0.95f, 0.45f), sliderArea.RectTransform, Anchor.Center), barSize: 0.1f, style: "DeviceSlider") { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index c86d674e5..96f708e8a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; namespace Barotrauma.Items.Components @@ -393,6 +394,8 @@ namespace Barotrauma.Items.Components partial void SelectProjSpecific(Character character) { + if (character != Character.Controlled) { return; } + var nonItems = itemList.Content.Children.Where(c => c.UserData is not FabricationRecipe).ToList(); nonItems.ForEach(i => itemList.Content.RemoveChild(i)); @@ -784,6 +787,7 @@ namespace Barotrauma.Items.Components private void HideEmptyItemListCategories() { + bool visibleElementsChanged = false; //go through the elements backwards, and disable the labels ("insufficient skills to fabricate", "recipe required...") if there's no items below them bool recipeVisible = false; foreach (GUIComponent child in itemList.Content.Children.Reverse()) @@ -792,7 +796,11 @@ namespace Barotrauma.Items.Components { if (child.Enabled) { - child.Visible = recipeVisible; + if (child.Visible != recipeVisible) + { + child.Visible = recipeVisible; + visibleElementsChanged = true; + } } recipeVisible = false; } @@ -802,8 +810,11 @@ namespace Barotrauma.Items.Components } } - itemList.UpdateScrollBarSize(); - itemList.BarScroll = 0.0f; + if (visibleElementsChanged) + { + itemList.UpdateScrollBarSize(); + itemList.BarScroll = 0.0f; + } } public bool ClearFilter() @@ -815,11 +826,22 @@ namespace Barotrauma.Items.Components return true; } + private readonly record struct SelectedRecipe(Character User, FabricationRecipe SelectedItem, Option OverrideRequiredTime); + private Option LastSelectedRecipe = Option.None; + private bool SelectItem(Character user, FabricationRecipe selectedItem, float? overrideRequiredTime = null) { this.selectedItem = selectedItem; displayingForCharacter = user; + var selectedRecipe = new SelectedRecipe(user, selectedItem, overrideRequiredTime is null ? Option.None : Option.Some(overrideRequiredTime.Value)); + LastSelectedRecipe = Option.Some(selectedRecipe); + CreateSelectedItemUI(selectedRecipe); + return true; + } + private void CreateSelectedItemUI(SelectedRecipe recipe) + { + var (user, selectedItem, overrideRequiredTime) = recipe; int max = Math.Max(selectedItem.TargetItem.GetMaxStackSize(outputContainer.Inventory) / selectedItem.Amount, 1); if (amountInput != null) @@ -843,8 +865,10 @@ namespace Barotrauma.Items.Components LocalizedString itemName = GetRecipeNameAndAmount(selectedItem); LocalizedString name = itemName; - float quality = selectedItem.Quality ?? GetFabricatedItemQuality(selectedItem, user); - if (quality > 0) + QualityResult result = GetFabricatedItemQuality(selectedItem, user); + + float quality = selectedItem.Quality ?? result.Quality; + if (quality > 0 || result.HasRandomQualityRollChance) { name = TextManager.GetWithVariable("itemname.quality" + (int)quality, "[itemname]", itemName + '\n') .Fallback(TextManager.GetWithVariable("itemname.quality3", "[itemname]", itemName + '\n')); @@ -855,6 +879,49 @@ namespace Barotrauma.Items.Components { AutoScaleHorizontal = true }; + + if (result.HasRandomQualityRollChance) + { + var iconLayout = new GUIFrame(new RectTransform(new Vector2(0.4f, 1f), selectedItemFrame.RectTransform, anchor: Anchor.TopRight), style: null); + var icon = GameSession.CreateNotificationIcon(iconLayout, offset: true); + + float percentage1 = result.TotalPlusOnePercentage; + float percentage2 = result.TotalPlusTwoPercentage; + + string chance1text = percentage1.ToString("F1", CultureInfo.InvariantCulture); + string chance2text = percentage2.ToString("F1", CultureInfo.InvariantCulture); + + int quality1 = Math.Clamp(result.Quality + 1, min: 0, max: 3); + int quality2 = Math.Clamp(result.Quality + 2, min: 0, max: 3); + + LocalizedString quality1Text = TextManager.Get($"quality{quality1}"); + LocalizedString quality2Text = TextManager.Get($"quality{quality2}"); + + string localizationTag = percentage2 > 0f && percentage1 > 0 && quality1 != quality2 ? "meetsbonusrequirementtwice" : "meetsbonusrequirement"; + + var variables = new (string Key, LocalizedString Value)[] + { + ("[chance]", chance1text), ("[quality]", quality1Text), + ("[chance2]", chance2text), ("[quality2]", quality2Text) + }; + + if (MathUtils.NearlyEqual(percentage1, 0)) + { + variables = new[] { ("[chance]", chance2text), ("[quality]", quality2Text) }; + } + + if (quality1 == quality2) + { + LocalizedString rawPercentage = result.PlusOnePercentage.ToString("F1", CultureInfo.InvariantCulture); + variables = new[] { ("[chance]", rawPercentage), ("[quality]", quality1Text) }; + } + + LocalizedString qualityTooltip = TextManager.GetWithVariables(localizationTag, variables); + + icon.ToolTip = RichString.Rich(qualityTooltip); + icon.Visible = icon.CanBeFocused = true; + } + nameBlock.Padding = new Vector4(0, nameBlock.Padding.Y, GUI.IntScale(5), nameBlock.Padding.W); if (nameBlock.TextScale < 0.7f) { @@ -865,15 +932,15 @@ namespace Barotrauma.Items.Components nameBlock.Wrap = true; nameBlock.SetTextPos(); nameBlock.RectTransform.MinSize = new Point(0, (int)(nameBlock.TextSize.Y * nameBlock.TextScale)); - } - + } + if (!selectedItem.TargetItem.Description.IsNullOrEmpty()) { var description = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), selectedItem.TargetItem.Description, font: GUIStyle.SmallFont, wrap: true); description.Padding = new Vector4(0, description.Padding.Y, description.Padding.Z, description.Padding.W); - + while (description.Rect.Height + nameBlock.Rect.Height > paddedFrame.Rect.Height) { var lines = description.WrappedText.Split('\n'); @@ -884,13 +951,13 @@ namespace Barotrauma.Items.Components description.ToolTip = selectedItem.TargetItem.Description; } } - + IEnumerable inadequateSkills = Enumerable.Empty(); if (user != null) { inadequateSkills = selectedItem.RequiredSkills.Where(skill => user.GetSkillLevel(skill.Identifier) < Math.Round(skill.Level * SkillRequirementMultiplier)); } - + if (selectedItem.RequiredSkills.Any()) { LocalizedString text = ""; @@ -911,9 +978,10 @@ namespace Barotrauma.Items.Components float degreeOfSuccess = user == null ? 0.0f : FabricationDegreeOfSuccess(user, selectedItem.RequiredSkills); if (degreeOfSuccess > 0.5f) { degreeOfSuccess = 1.0f; } - float requiredTime = overrideRequiredTime ?? - (user == null ? selectedItem.RequiredTime : GetRequiredTime(selectedItem, user)); - + float requiredTime = overrideRequiredTime.TryUnwrap(out var time) + ? time + : (user == null ? selectedItem.RequiredTime : GetRequiredTime(selectedItem, user)); + if ((int)requiredTime > 0) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform), @@ -936,7 +1004,6 @@ namespace Barotrauma.Items.Components font: GUIStyle.SmallFont); } - return true; } public void HighlightRecipe(string identifier, Color color) @@ -1046,6 +1113,15 @@ namespace Barotrauma.Items.Components } } + public override void OnPlayerSkillsChanged() + => RefreshSelectedItem(); + + public void RefreshSelectedItem() + { + if (!LastSelectedRecipe.TryUnwrap(out var lastSelected)) { return; } + CreateSelectedItemUI(lastSelected); + } + partial void UpdateRequiredTimeProjSpecific() { if (requiredTimeBlock == null) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index e06de7a92..1e636ec15 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -66,7 +66,15 @@ namespace Barotrauma.Items.Components private float prevPassivePingRadius; private Vector2 center; - private float displayScale; + + /// + /// Current scale of the display, taking zoom into account. In other words, the scaling factor of world coordinates to coordinates on the display. + /// + public float DisplayScale + { + get; + private set; + } = 1.0f; private const float DisruptionUpdateInterval = 0.2f; private float disruptionUpdateTimer; @@ -751,9 +759,9 @@ namespace Barotrauma.Items.Components { var activePing = activePings[pingIndex]; float pingRadius = DisplayRadius * activePing.State / zoom; - if (disruptionUpdateTimer <= 0.0f) { UpdateDisruptions(transducerCenter, pingRadius / displayScale); } + if (disruptionUpdateTimer <= 0.0f) { UpdateDisruptions(transducerCenter, pingRadius / DisplayScale); } Ping(transducerCenter, transducerCenter, - pingRadius, activePing.PrevPingRadius, displayScale, range / zoom, passive: false, pingStrength: 2.0f); + pingRadius, activePing.PrevPingRadius, DisplayScale, range / zoom, passive: false, pingStrength: 2.0f); activePing.PrevPingRadius = pingRadius; } if (disruptionUpdateTimer <= 0.0f) @@ -770,7 +778,7 @@ namespace Barotrauma.Items.Components if (c.Params.HideInSonar) { continue; } if (!c.IsUnconscious && c.Params.DistantSonarRange > 0.0f && - ((c.WorldPosition - transducerCenter) * displayScale).LengthSquared() > DisplayRadius * DisplayRadius) + ((c.WorldPosition - transducerCenter) * DisplayScale).LengthSquared() > DisplayRadius * DisplayRadius) { Vector2 targetVector = c.WorldPosition - transducerCenter; if (targetVector.LengthSquared() > MathUtils.Pow2(c.Params.DistantSonarRange)) { continue; } @@ -818,7 +826,7 @@ namespace Barotrauma.Items.Components if (dist > prevPassivePingRadius * Range && dist <= passivePingRadius * Range && Rand.Int(sonarBlips.Count) < 500) { Ping(t.WorldPosition, transducerCenter, - t.SoundRange * displayScale, 0, displayScale, range, + t.SoundRange * DisplayScale, 0, DisplayScale, range, passive: true, pingStrength: 0.5f, needsToBeInSector: t); if (t.IsWithinSector(transducerCenter)) { @@ -857,7 +865,7 @@ namespace Barotrauma.Items.Components displayBorderSize = 0.2f; center = rect.Center.ToVector2(); DisplayRadius = (rect.Width / 2.0f) * (1.0f - displayBorderSize); - displayScale = DisplayRadius / range * zoom; + DisplayScale = DisplayRadius / range * zoom; screenBackground?.Draw(spriteBatch, center, 0.0f, rect.Width / screenBackground.size.X); @@ -972,7 +980,7 @@ namespace Barotrauma.Items.Components aiTarget.SonarIconIdentifier, aiTarget, aiTarget.WorldPosition, transducerCenter, - displayScale, center, DisplayRadius * 0.975f); + DisplayScale, center, DisplayRadius * 0.975f); } } @@ -983,21 +991,21 @@ namespace Barotrauma.Items.Components if (Level.Loaded.StartLocation?.Type is { ShowSonarMarker: true }) { DrawMarker(spriteBatch, - Level.Loaded.StartLocation.Name, + Level.Loaded.StartLocation.DisplayName.Value, (Level.Loaded.StartOutpost != null ? "outpost" : "location").ToIdentifier(), "startlocation", Level.Loaded.StartExitPosition, transducerCenter, - displayScale, center, DisplayRadius); + DisplayScale, center, DisplayRadius); } if (Level.Loaded is { EndLocation.Type.ShowSonarMarker: true, Type: LevelData.LevelType.LocationConnection }) { DrawMarker(spriteBatch, - Level.Loaded.EndLocation.Name, + Level.Loaded.EndLocation.DisplayName.Value, (Level.Loaded.EndOutpost != null ? "outpost" : "location").ToIdentifier(), "endlocation", Level.Loaded.EndExitPosition, transducerCenter, - displayScale, center, DisplayRadius); + DisplayScale, center, DisplayRadius); } for (int i = 0; i < Level.Loaded.Caves.Count; i++) @@ -1009,7 +1017,7 @@ namespace Barotrauma.Items.Components "cave".ToIdentifier(), "cave" + i, cave.StartPos.ToVector2(), transducerCenter, - displayScale, center, DisplayRadius); + DisplayScale, center, DisplayRadius); } } @@ -1026,7 +1034,7 @@ namespace Barotrauma.Items.Components mission.SonarIconIdentifier, "mission" + missionIndex + ":" + i, position, transducerCenter, - displayScale, center, DisplayRadius * 0.95f); + DisplayScale, center, DisplayRadius * 0.95f); } i++; } @@ -1059,7 +1067,7 @@ namespace Barotrauma.Items.Components DrawMarker(spriteBatch, i.Name, "mineral".ToIdentifier(), "mineralcluster" + i, c.center, transducerCenter, - displayScale, center, DisplayRadius * 0.95f, + DisplayScale, center, DisplayRadius * 0.95f, onlyShowTextOnMouseOver: true); } } @@ -1088,19 +1096,19 @@ namespace Barotrauma.Items.Components (sub.Info.HasTag(SubmarineTag.Shuttle) ? "shuttle" : "submarine").ToIdentifier(), sub, sub.WorldPosition, transducerCenter, - displayScale, center, DisplayRadius * 0.95f); + DisplayScale, center, DisplayRadius * 0.95f); } if (GameMain.DebugDraw) { var steering = item.GetComponent(); - steering?.DebugDrawHUD(spriteBatch, transducerCenter, displayScale, DisplayRadius, center); + steering?.DebugDrawHUD(spriteBatch, transducerCenter, DisplayScale, DisplayRadius, center); } } private void DrawOwnSubmarineBorders(SpriteBatch spriteBatch, Vector2 transducerCenter, float signalStrength) { - float simScale = displayScale * Physics.DisplayToSimRation * zoom; + float simScale = DisplayScale * Physics.DisplayToSimRation; foreach (Submarine submarine in Submarine.Loaded) { @@ -1167,7 +1175,7 @@ namespace Barotrauma.Items.Components private void DrawDockingPorts(SpriteBatch spriteBatch, Vector2 transducerCenter, float signalStrength) { - float scale = displayScale * zoom; + float scale = DisplayScale; Steering steering = item.GetComponent(); if (steering != null && steering.DockingModeEnabled && steering.ActiveDockingSource != null) @@ -1219,7 +1227,7 @@ namespace Barotrauma.Items.Components private void DrawDockingIndicator(SpriteBatch spriteBatch, Steering steering, ref Vector2 transducerCenter) { - float scale = displayScale * zoom; + float scale = DisplayScale; Vector2 worldFocusPos = (steering.ActiveDockingSource.Item.WorldPosition + steering.DockingTarget.Item.WorldPosition) / 2.0f; worldFocusPos.X = steering.DockingTarget.Item.WorldPosition.X; @@ -1591,7 +1599,7 @@ namespace Barotrauma.Items.Components { lineStep /= zoom; zStep /= zoom; - range *= displayScale; + range *= DisplayScale; float length = (point1 - point2).Length(); Vector2 lineDir = (point2 - point1) / length; for (float x = 0; x < length; x += lineStep * Rand.Range(0.8f, 1.2f)) @@ -1602,12 +1610,12 @@ namespace Barotrauma.Items.Components //ignore if outside the display Vector2 transducerDiff = point - transducerPos; - Vector2 transducerDisplayDiff = transducerDiff * displayScale; + Vector2 transducerDisplayDiff = transducerDiff * DisplayScale / zoom; if (transducerDisplayDiff.LengthSquared() > DisplayRadius * DisplayRadius) { continue; } //ignore if the point is not within the ping Vector2 pointDiff = point - pingSource; - Vector2 displayPointDiff = pointDiff * displayScale; + Vector2 displayPointDiff = pointDiff * DisplayScale / zoom; float displayPointDistSqr = displayPointDiff.LengthSquared(); if (displayPointDistSqr < prevPingRadius * prevPingRadius || displayPointDistSqr > pingRadius * pingRadius) { continue; } @@ -1628,9 +1636,9 @@ namespace Barotrauma.Items.Components float displayPointDist = (float)Math.Sqrt(displayPointDistSqr); float alpha = pingStrength * Rand.Range(1.5f, 2.0f); - for (float z = 0; z < DisplayRadius - transducerDist * displayScale; z += zStep) + for (float z = 0; z < DisplayRadius - transducerDist * DisplayScale; z += zStep) { - Vector2 pos = point + Rand.Vector(150.0f / zoom) + pingDirection * z / displayScale; + Vector2 pos = point + Rand.Vector(150.0f / zoom) + pingDirection * z / DisplayScale; float fadeTimer = alpha * (1.0f - displayPointDist / range); if (needsToBeInSector != null) @@ -1697,7 +1705,7 @@ namespace Barotrauma.Items.Components private bool CheckBlipVisibility(SonarBlip blip, Vector2 transducerPos) { - Vector2 pos = (blip.Position - transducerPos) * displayScale * zoom; + Vector2 pos = (blip.Position - transducerPos) * DisplayScale; pos.Y = -pos.Y; float posDistSqr = pos.LengthSquared(); @@ -1731,7 +1739,7 @@ namespace Barotrauma.Items.Components } if (currentPingIndex != -1 && activePings[currentPingIndex].IsDirectional) { - var pos = (resourcePos - transducerPos) * displayScale * zoom; + var pos = (resourcePos - transducerPos) * DisplayScale; pos.Y = -pos.Y; var length = pos.Length(); var dir = pos / length; @@ -1749,7 +1757,7 @@ namespace Barotrauma.Items.Components float distort = 1.0f - item.Condition / item.MaxCondition; - Vector2 pos = (blip.Position - transducerPos) * displayScale * zoom; + Vector2 pos = (blip.Position - transducerPos) * DisplayScale; pos.Y = -pos.Y; if (Rand.Range(0.5f, 2.0f) < distort) { pos.X = -pos.X; } @@ -1825,14 +1833,13 @@ namespace Barotrauma.Items.Components Vector2 position = worldPosition - transducerPosition; - position *= zoom; position *= scale; position.Y = -position.Y; float textAlpha = MathHelper.Clamp(1.5f - dist / 50000.0f, 0.5f, 1.0f); Vector2 dir = Vector2.Normalize(position); - Vector2 markerPos = (linearDist * zoom * scale > radius) ? dir * radius : position; + Vector2 markerPos = (linearDist * scale > radius) ? dir * radius : position; markerPos += center; markerPos.X = (int)markerPos.X; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index fd5be2193..90d3f85ed 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -216,7 +216,7 @@ namespace Barotrauma.Items.Components } }; levelStartTickBox = new GUITickBox(new RectTransform(new Vector2(1, 0.333f), paddedAutoPilotControls.RectTransform, Anchor.Center), - GameMain.GameSession?.StartLocation == null ? "" : ToolBox.LimitString(GameMain.GameSession.StartLocation.Name, GUIStyle.SmallFont, textLimit), + GameMain.GameSession?.StartLocation == null ? "" : ToolBox.LimitString(GameMain.GameSession.StartLocation.DisplayName, GUIStyle.SmallFont, textLimit), font: GUIStyle.SmallFont, style: "GUIRadioButton") { Enabled = autoPilot, @@ -243,7 +243,7 @@ namespace Barotrauma.Items.Components }; levelEndTickBox = new GUITickBox(new RectTransform(new Vector2(1, 0.333f), paddedAutoPilotControls.RectTransform, Anchor.BottomCenter), - (GameMain.GameSession?.EndLocation == null || Level.IsLoadedOutpost) ? "" : ToolBox.LimitString(GameMain.GameSession.EndLocation.Name, GUIStyle.SmallFont, textLimit), + (GameMain.GameSession?.EndLocation == null || Level.IsLoadedOutpost) ? "" : ToolBox.LimitString(GameMain.GameSession.EndLocation.DisplayName, GUIStyle.SmallFont, textLimit), font: GUIStyle.SmallFont, style: "GUIRadioButton") { Enabled = autoPilot, @@ -389,7 +389,7 @@ namespace Barotrauma.Items.Components if (!ObjectiveManager.AllActiveObjectivesCompleted()) { exitOutpostPrompt = new GUIMessageBox("", - TextManager.GetWithVariable("CampaignExitTutorialOutpostPrompt", "[locationname]", campaign.Map.CurrentLocation.Name), + TextManager.GetWithVariable("CampaignExitTutorialOutpostPrompt", "[locationname]", campaign.Map.CurrentLocation.DisplayName), new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); exitOutpostPrompt.Buttons[0].OnClicked += (_, _) => { @@ -509,9 +509,9 @@ namespace Barotrauma.Items.Components noPowerTip = TextManager.Get("SteeringNoPowerTip"); autoPilotMaintainPosTip = TextManager.Get("SteeringAutoPilotMaintainPosTip"); autoPilotLevelStartTip = TextManager.GetWithVariable("SteeringAutoPilotLocationTip", "[locationname]", - GameMain.GameSession?.StartLocation == null ? "Start" : GameMain.GameSession.StartLocation.Name); + GameMain.GameSession?.StartLocation == null ? "Start" : GameMain.GameSession.StartLocation.DisplayName); autoPilotLevelEndTip = TextManager.GetWithVariable("SteeringAutoPilotLocationTip", "[locationname]", - GameMain.GameSession?.EndLocation == null ? "End" : GameMain.GameSession.EndLocation.Name); + GameMain.GameSession?.EndLocation == null ? "End" : GameMain.GameSession.EndLocation.DisplayName); } protected override void OnResolutionChanged() @@ -589,7 +589,8 @@ namespace Barotrauma.Items.Components Sonar sonar = item.GetComponent(); if (sonar != null && controlledSub != null) { - Vector2 displayPosToMaintain = ((posToMaintain.Value - controlledSub.WorldPosition)) / sonar.Range * sonar.DisplayRadius * sonar.Zoom; + Vector2 displayPosToMaintain = ((posToMaintain.Value - controlledSub.WorldPosition)) * sonar.DisplayScale; + displayPosToMaintain.Y = -displayPosToMaintain.Y; displayPosToMaintain = displayPosToMaintain.ClampLength(velRect.Width / 2); displayPosToMaintain = steerArea.Rect.Center.ToVector2() + displayPosToMaintain; @@ -670,14 +671,14 @@ namespace Barotrauma.Items.Components pos2.Y = -pos2.Y; pos2 += center; - GUI.DrawLine(spriteBatch, - pos1, + GUI.DrawLine(spriteBatch, + pos1, pos2, GUIStyle.Red * 0.6f, width: 3); if (obstacle.Intersection.HasValue) { - Vector2 intersectionPos = (obstacle.Intersection.Value - transducerCenter) *displayScale; + Vector2 intersectionPos = (obstacle.Intersection.Value - transducerCenter) * displayScale; intersectionPos.Y = -intersectionPos.Y; intersectionPos += center; GUI.DrawRectangle(spriteBatch, intersectionPos - Vector2.One * 2, Vector2.One * 4, GUIStyle.Red); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs index 8f58322ae..1b76a65ef 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs @@ -151,7 +151,7 @@ namespace Barotrauma.Items.Components GUI.DrawLine(spriteBatch, new Vector2(debugRayStartPos.X, -debugRayStartPos.Y), new Vector2(debugRayEndPos.X, -debugRayEndPos.Y), - Color.Yellow); + Color.Yellow, width: 3f); } } #endif diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs index c821da993..dc5b84592 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs @@ -95,7 +95,7 @@ namespace Barotrauma.Items.Components MaxValueFloat = numberInputMax, FloatValue = Math.Clamp(floatSignal, numberInputMin, numberInputMax), DecimalsToDisplay = ciElement.NumberInputDecimalPlaces, - valueStep = numberInputStep, + ValueStep = numberInputStep, OnValueChanged = (ni) => { if (GameMain.Client == null) @@ -121,7 +121,7 @@ namespace Barotrauma.Items.Components MinValueInt = numberInputMin, MaxValueInt = numberInputMax, IntValue = Math.Clamp(intSignal, numberInputMin, numberInputMax), - valueStep = numberInputStep, + ValueStep = numberInputStep, OnValueChanged = (ni) => { if (GameMain.Client == null) @@ -137,7 +137,8 @@ namespace Barotrauma.Items.Components } else { - DebugConsole.LogError($"Error creating a CustomInterface component: unexpected NumberType \"{(ciElement.NumberType.HasValue ? ciElement.NumberType.Value.ToString() : "none")}\""); + DebugConsole.LogError($"Error creating a CustomInterface component: unexpected NumberType \"{(ciElement.NumberType.HasValue ? ciElement.NumberType.Value.ToString() : "none")}\"", + contentPackage: item.Prefab.ContentPackage); } if (numberInput != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs index 698db9c7c..a2f10cce8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs @@ -349,8 +349,8 @@ namespace Barotrauma.Items.Components } GUI.DrawString(spriteBatch, hudPos, texts[0].Value, textColors[0] * alpha, Color.Black * 0.7f * alpha, 2, GUIStyle.SubHeadingFont, ForceUpperCase.No); - hudPos.X += 5.0f; - hudPos.Y += 24.0f * GameSettings.CurrentConfig.Graphics.TextScale; + hudPos.X += 5.0f * GUI.Scale; + hudPos.Y += GUIStyle.SubHeadingFont.MeasureString(texts[0].Value).Y; hudPos.X = (int)hudPos.X; hudPos.Y = (int)hudPos.Y; @@ -358,7 +358,7 @@ namespace Barotrauma.Items.Components for (int i = 1; i < texts.Count; i++) { GUI.DrawString(spriteBatch, hudPos, texts[i], textColors[i] * alpha, Color.Black * 0.7f * alpha, 2, GUIStyle.SmallFont); - hudPos.Y += (int)(18.0f * GameSettings.CurrentConfig.Graphics.TextScale); + hudPos.Y += (int)(GUIStyle.SubHeadingFont.MeasureString(texts[i].Value).Y); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index a4903a351..c43909cc7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -16,6 +16,8 @@ namespace Barotrauma.Items.Components private GUIProgressBar powerIndicator; + private Vector2? debugDrawTargetPos; + public int UIElementHeight { get @@ -422,9 +424,23 @@ namespace Barotrauma.Items.Components if (GameMain.DebugDraw) { Vector2 firingPos = GetRelativeFiringPosition(); + Vector2 endPos = firingPos + 3500 * GetBarrelDir(); firingPos.Y = -firingPos.Y; + endPos.Y = -endPos.Y; GUI.DrawLine(spriteBatch, firingPos - Vector2.UnitX * 5, firingPos + Vector2.UnitX * 5, Color.Red); GUI.DrawLine(spriteBatch, firingPos - Vector2.UnitY * 5, firingPos + Vector2.UnitY * 5, Color.Red); + + if (debugDrawTargetPos.HasValue) + { + Vector2 targetPos = debugDrawTargetPos.Value; + targetPos.Y = -targetPos.Y; + GUI.DrawLine(spriteBatch, targetPos - Vector2.UnitX * 5, targetPos + Vector2.UnitX * 5, Color.Magenta, width: 5); + GUI.DrawLine(spriteBatch, targetPos - Vector2.UnitY * 5, targetPos + Vector2.UnitY * 5, Color.Magenta, width: 5); + + GUI.DrawLine(spriteBatch, firingPos, targetPos, Color.Magenta, width: 2); + + } + GUI.DrawLine(spriteBatch, firingPos, endPos, Color.LightGray, width: 2); } if (!editing || GUI.DisableHUD || !item.IsSelected) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index ecb897421..1846a30b6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -258,7 +258,7 @@ namespace Barotrauma else { LocalizedString description = item.Description; - if (item.HasTag(Tags.IdCard) || item.HasTag(Tags.DespawnContainer)) + if (item.HasTag(Tags.IdCardTag) || item.HasTag(Tags.DespawnContainer)) { string[] readTags = item.Tags.Split(','); string idName = null; @@ -730,7 +730,7 @@ namespace Barotrauma DraggingInventory = null; subInventory.savedPosition = PlayerInput.MousePosition.ToPoint(); } - else + else if (DraggingInventory == subInventory) { subInventory.savedPosition = PlayerInput.MousePosition.ToPoint(); } @@ -901,7 +901,7 @@ namespace Barotrauma if (IsOnInventorySlot(Character.Controlled.SelectedCharacter.Inventory)) { return true; } } - bool IsOnInventorySlot(Inventory inventory) + static bool IsOnInventorySlot(Inventory inventory) { for (var i = 0; i < inventory.visualSlots.Length; i++) { @@ -1107,7 +1107,7 @@ namespace Barotrauma if (container.MovableFrame && !IsInventoryHoverAvailable(Owner as Character, container)) { - if (positionUpdateQueued) // Wait a frame before updating the positioning of the container after a resolution change to have everything working + if (container.Inventory.positionUpdateQueued) // Wait a frame before updating the positioning of the container after a resolution change to have everything working { int height = (int)(movableFrameRectHeight * UIScale); CreateSlots(); @@ -1116,7 +1116,7 @@ namespace Barotrauma draggableIndicatorOffset = DraggableIndicator.size * draggableIndicatorScale / 2f; draggableIndicatorOffset += new Vector2(height / 2f - draggableIndicatorOffset.Y); container.Inventory.originalPos = container.Inventory.savedPosition = container.Inventory.movableFrameRect.Center; - positionUpdateQueued = false; + container.Inventory.positionUpdateQueued = false; } if (container.Inventory.movableFrameRect.Size == Point.Zero || GUI.HasSizeChanged(prevScreenResolution, prevUIScale, prevHUDScale)) @@ -1127,11 +1127,20 @@ namespace Barotrauma prevScreenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); prevUIScale = UIScale; prevHUDScale = GUI.Scale; - positionUpdateQueued = true; + container.Inventory.positionUpdateQueued = true; } else { - GUI.DrawRectangle(spriteBatch, container.Inventory.movableFrameRect, movableFrameRectColor, true); + Color color = movableFrameRectColor; + if (DraggingInventory != null && DraggingInventory != container.Inventory) + { + color *= 0.7f; + } + else if (container.Inventory.movableFrameRect.Contains(PlayerInput.MousePosition)) + { + color = Color.Lerp(color, PlayerInput.PrimaryMouseButtonHeld() ? Color.Black : Color.White, 0.25f); + } + GUI.DrawRectangle(spriteBatch, container.Inventory.movableFrameRect, color, true); DraggableIndicator.Draw(spriteBatch, container.Inventory.movableFrameRect.Location.ToVector2() + draggableIndicatorOffset, 0, draggableIndicatorScale); } } @@ -1269,12 +1278,20 @@ namespace Barotrauma 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; + allowCombine = false; } int itemCount = 0; foreach (Item item in DraggingItems) { - bool success = selectedInventory.TryPutItem(item, slotIndex, allowSwapping: !anySuccess, allowCombine, Character.Controlled); + if (selectedInventory.GetItemAt(slotIndex)?.OwnInventory?.Container is { } container && + container.Inventory.CanBePut(item)) + { + if (!container.AllowDragAndDrop || !container.DrawInventory) + { + allowCombine = false; + } + } + bool success = selectedInventory.TryPutItem(item, slotIndex, allowSwapping: !anySuccess, allowCombine, Character.Controlled); if (success) { anySuccess = true; @@ -1380,18 +1397,17 @@ namespace Barotrauma protected static Rectangle GetSubInventoryHoverArea(SlotReference subSlot) { - Rectangle hoverArea; - if ((Screen.Selected != GameMain.SubEditorScreen || GameMain.SubEditorScreen.DrawCharacterInventory) && - (!subSlot.Inventory.Movable() || - (Character.Controlled?.Inventory == subSlot.ParentInventory && !Character.Controlled.HasEquippedItem(subSlot.Item)) || - (subSlot.ParentInventory is CharacterInventory characterInventory && characterInventory.CurrentLayout != CharacterInventory.Layout.Default))) + if (Character.Controlled == null) { - //slot not visible as a separate, movable panel -> just use the area of the slot directly - hoverArea = subSlot.Slot.Rect; - hoverArea.Location += subSlot.Slot.DrawOffset.ToPoint(); - hoverArea = Rectangle.Union(hoverArea, subSlot.Slot.EquipButtonRect); + return Rectangle.Empty; } - else + + Rectangle hoverArea; + bool isMovable = subSlot.Inventory.Movable() && !subSlot.ParentInventory.IsInventoryHoverAvailable(Character.Controlled, subSlot.Item?.GetComponent()); + bool unEquipped = Character.Controlled.Inventory == subSlot.ParentInventory && !Character.Controlled.HasEquippedItem(subSlot.Item); + bool isDefaultLayout = subSlot.ParentInventory is not CharacterInventory characterInventory || characterInventory.CurrentLayout == CharacterInventory.Layout.Default; + bool subEditorCharacterInventoryHidden = Screen.Selected == GameMain.SubEditorScreen && !GameMain.SubEditorScreen.DrawCharacterInventory; + if (subEditorCharacterInventoryHidden || (isMovable && !unEquipped && isDefaultLayout)) { hoverArea = subSlot.Inventory.BackgroundFrame; hoverArea.Location += subSlot.Slot.DrawOffset.ToPoint(); @@ -1400,6 +1416,13 @@ namespace Barotrauma hoverArea = Rectangle.Union(hoverArea, subSlot.Inventory.movableFrameRect); } } + else + { + //slot not visible as a separate, movable panel -> just use the area of the slot directly + hoverArea = subSlot.Slot.Rect; + hoverArea.Location += subSlot.Slot.DrawOffset.ToPoint(); + hoverArea = Rectangle.Union(hoverArea, subSlot.Slot.EquipButtonRect); + } if (subSlot.Inventory?.visualSlots != null) { @@ -1584,11 +1607,16 @@ namespace Barotrauma if (DraggingItems.Any() && inventory != null && slotIndex > -1 && slotIndex < inventory.visualSlots.Length) { + var itemInSlot = inventory.slots[slotIndex].FirstOrDefault(); if (inventory.CanBePutInSlot(DraggingItems.First(), slotIndex)) { canBePut = true; } - else if (inventory.slots[slotIndex].FirstOrDefault()?.OwnInventory?.CanBePut(DraggingItems.First()) ?? false) + else if + (itemInSlot?.OwnInventory != null && + itemInSlot.OwnInventory.CanBePut(DraggingItems.First()) && + itemInSlot.OwnInventory.Container.AllowDragAndDrop && + itemInSlot.OwnInventory.Container.DrawInventory) { canBePut = true; } @@ -1922,9 +1950,25 @@ namespace Barotrauma foreach (UInt16 id in receivedItemIDs[i]) { if (Entity.FindEntityByID(id) is not Item item || slots[i].Contains(item)) { continue; } + + if (Owner is Item thisItem && thisItem.Container == item) + { + //if this item is inside the item we're trying to contain inside it, we need to drop it (both items can't be inside each other!) + //can happen when a player swaps the items to be "the other way around", and we receive a message about the contained item + //before the message about the "parent item" being placed in some other inventory (like the player's inventory) + thisItem.Drop(null); + } + if (!TryPutItem(item, i, false, false, null, false)) { - ForceToSlot(item, i); + try + { + ForceToSlot(item, i); + } + catch (InvalidOperationException e) + { + DebugConsole.AddSafeError(e.Message + "\n" + e.StackTrace.CleanupStackTrace()); + } } for (int j = 0; j < capacity; j++) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 0f42a313c..1633cf7bc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -132,9 +132,9 @@ namespace Barotrauma return GetDrawDepth(SpriteDepth + DrawDepthOffset, Sprite); } - public Color GetSpriteColor(bool withHighlight = false) + public Color GetSpriteColor(Color? defaultColor = null, bool withHighlight = false) { - Color color = spriteColor; + Color color = defaultColor ?? spriteColor; if (Prefab.UseContainedSpriteColor && ownInventory != null) { foreach (Item item in ContainedItems) @@ -286,7 +286,8 @@ namespace Barotrauma { int padding = 100; - Vector2 min = new Vector2(-rect.Width / 2 - padding, -rect.Height / 2 - padding); + RectangleF boundingBox = GetTransformedQuad().BoundingAxisAlignedRectangle; + Vector2 min = new Vector2(-boundingBox.Width / 2 - padding, -boundingBox.Height / 2 - padding); Vector2 max = -min; foreach (IDrawableComponent drawable in drawableComponents) @@ -333,9 +334,7 @@ namespace Barotrauma else if (!ShowItems) { return; } } - Color color = - overrideColor ?? - (IsIncludedInSelection && editing ? GUIStyle.Blue : GetSpriteColor(withHighlight: true)); + Color color = GetSpriteColor(spriteColor); bool isWiringMode = editing && SubEditorScreen.TransparentWiringMode && SubEditorScreen.IsWiringMode() && !isWire && parentInventory == null; bool renderTransparent = isWiringMode && GetComponent() == null; @@ -388,9 +387,9 @@ namespace Barotrauma { if (Prefab.ResizeHorizontal || Prefab.ResizeVertical) { - Vector2 size = new Vector2(rect.Width, rect.Height); if (color.A > 0) { + Vector2 size = new Vector2(rect.Width, rect.Height); activeSprite.DrawTiled(spriteBatch, new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2)) + drawOffset, size, color: color, textureScale: Vector2.One * Scale, @@ -403,18 +402,7 @@ namespace Barotrauma textureScale: Vector2.One * Scale, depth: d); } - foreach (var decorativeSprite in Prefab.DecorativeSprites) - { - if (!spriteAnimState[decorativeSprite].IsActive) { continue; } - Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, flippedX && Prefab.CanSpriteFlipX ? RotationRad : -RotationRad) * Scale; - if (flippedX && Prefab.CanSpriteFlipX) { offset.X = -offset.X; } - if (flippedY && Prefab.CanSpriteFlipY) { offset.Y = -offset.Y; } - decorativeSprite.Sprite.DrawTiled(spriteBatch, - new Vector2(DrawPosition.X + offset.X - rect.Width / 2, -(DrawPosition.Y + offset.Y + rect.Height / 2)), - size, color: color, - textureScale: Vector2.One * Scale, - depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth), 0.999f)); - } + DrawDecorativeSprites(spriteBatch, DrawPosition, flippedX && Prefab.CanSpriteFlipX, flippedY && Prefab.CanSpriteFlipY, rotation: 0, depth); } } else @@ -434,19 +422,8 @@ namespace Barotrauma Prefab.InfectedSprite?.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, color, Prefab.InfectedSprite.Origin, RotationRad, Scale, activeSprite.effects, depth - 0.001f); Prefab.DamagedInfectedSprite?.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, Infector.HealthColor, Prefab.DamagedInfectedSprite.Origin, RotationRad, Scale, activeSprite.effects, depth - 0.002f); } - foreach (var decorativeSprite in Prefab.DecorativeSprites) - { - if (!spriteAnimState[decorativeSprite].IsActive) { continue; } - float rot = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor); - bool flipX = flippedX && Prefab.CanSpriteFlipX; - bool flipY = flippedY && Prefab.CanSpriteFlipY; - Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, flipX ^ flipY ? RotationRad : -RotationRad) * Scale; - if (flipX) { offset.X = -offset.X; } - if (flipY) { offset.Y = -offset.Y; } - decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), color, - RotationRad + rot, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, activeSprite.effects, - depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth), 0.999f)); - } + + DrawDecorativeSprites(spriteBatch, DrawPosition, flippedX && Prefab.CanSpriteFlipX, flippedY && Prefab.CanSpriteFlipY, -RotationRad, depth); } } else if (body.Enabled) @@ -490,21 +467,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); } - foreach (var decorativeSprite in Prefab.DecorativeSprites) - { - if (!spriteAnimState[decorativeSprite].IsActive) { continue; } - float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor); - Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, -RotationRad) * Scale; - if (flippedX && Prefab.CanSpriteFlipX) { offset.X = -offset.X; } - if (flippedY && Prefab.CanSpriteFlipY) { offset.Y = -offset.Y; } - var ca = MathF.Cos(-body.DrawRotation); - var sa = MathF.Sin(-body.DrawRotation); - Vector2 transformedOffset = new Vector2(ca * offset.X + sa * offset.Y, -sa * offset.X + ca * offset.Y); - - decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(body.DrawPosition.X + transformedOffset.X, -(body.DrawPosition.Y + transformedOffset.Y)), color, - -body.DrawRotation + rotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, activeSprite.effects, - depth: depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth)); - } + DrawDecorativeSprites(spriteBatch, body.DrawPosition, flipX: body.Dir < 0, flipY: false, rotation: body.Rotation, depth: depth); } foreach (var upgrade in Upgrades) @@ -522,7 +485,6 @@ namespace Barotrauma rotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, activeSprite.effects, depth: depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth)); } - } activeSprite.effects = oldEffects; @@ -567,8 +529,14 @@ namespace Barotrauma Vector2 drawPos = new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2)); Vector2 drawSize = new Vector2(MathF.Ceiling(rect.Width + Math.Abs(drawPos.X - (int)drawPos.X)), MathF.Ceiling(rect.Height + Math.Abs(drawPos.Y - (int)drawPos.Y))); drawPos = new Vector2(MathF.Floor(drawPos.X), MathF.Floor(drawPos.Y)); - GUI.DrawRectangle(spriteBatch, drawPos, drawSize, - Color.White, false, 0, thickness: Math.Max(1, (int)(2 / Screen.Selected.Cam.Zoom))); + GUI.DrawRectangle(sb: spriteBatch, + center: drawPos + drawSize * 0.5f, + width: drawSize.X, + height: drawSize.Y, + rotation: RotationRad, + clr: Color.White, + depth: 0, + thickness: 2f / Screen.Selected.Cam.Zoom); foreach (Rectangle t in Prefab.Triggers) { @@ -618,6 +586,62 @@ namespace Barotrauma } return origin; } + + Color GetSpriteColor(Color defaultColor) + { + return + overrideColor ?? + (IsIncludedInSelection && editing ? GUIStyle.Blue : this.GetSpriteColor(defaultColor: defaultColor, withHighlight: true)); + } + } + + public void DrawDecorativeSprites(SpriteBatch spriteBatch, Vector2 drawPos, bool flipX, bool flipY, float rotation, float depth) + { + foreach (var decorativeSprite in Prefab.DecorativeSprites) + { + Color decorativeSpriteColor = GetSpriteColor(decorativeSprite.Color).Multiply(GetSpriteColor(spriteColor)); + if (!spriteAnimState[decorativeSprite].IsActive) { continue; } + + Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, + flipX ^ flipY ? -rotation : rotation) * Scale; + + if (ResizeHorizontal || ResizeVertical) + { + decorativeSprite.Sprite.DrawTiled(spriteBatch, + new Vector2(DrawPosition.X + offset.X - rect.Width / 2, -(DrawPosition.Y + offset.Y + rect.Height / 2)), + new Vector2(rect.Width, rect.Height), color: decorativeSpriteColor, + textureScale: Vector2.One * Scale, + depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth), 0.999f)); + } + else + { + float spriteRotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor); + + Vector2 origin = decorativeSprite.Sprite.Origin; + SpriteEffects spriteEffects = SpriteEffects.None; + if (flipX && Prefab.CanSpriteFlipX) + { + offset.X = -offset.X; + origin.X = -origin.X + decorativeSprite.Sprite.size.X; + spriteEffects = SpriteEffects.FlipHorizontally; + } + if (flipY && Prefab.CanSpriteFlipY) + { + offset.Y = -offset.Y; + origin.Y = -origin.Y + decorativeSprite.Sprite.size.Y; + spriteEffects |= SpriteEffects.FlipVertically; + } + if (body != null) + { + var ca = MathF.Cos(-body.DrawRotation); + var sa = MathF.Sin(-body.DrawRotation); + offset = new Vector2(ca * offset.X + sa * offset.Y, -sa * offset.X + ca * offset.Y); + } + decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(drawPos.X + offset.X, -(drawPos.Y + offset.Y)), decorativeSpriteColor, origin, + -rotation + spriteRotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, spriteEffects, + depth: depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth)); + } + } } partial void OnCollisionProjSpecific(float impact) @@ -795,6 +819,19 @@ namespace Barotrauma } } + public override bool IsMouseOn(Vector2 position) + { + Vector2 rectSize = rect.Size.ToVector2(); + + Vector2 bodyPos = WorldPosition; + + Vector2 transformedMousePos = MathUtils.RotatePointAroundTarget(position, bodyPos, RotationRad); + + return + Math.Abs(transformedMousePos.X - bodyPos.X) < rectSize.X / 2.0f && + Math.Abs(transformedMousePos.Y - bodyPos.Y) < rectSize.Y / 2.0f; + } + public GUIComponent CreateEditingHUD(bool inGame = false) { activeEditors.Clear(); @@ -852,7 +889,12 @@ namespace Barotrauma CanBeFocused = true }; - new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityX"), style: "GUIButtonSmall") + GUINumberInput rotationField = + itemEditor.Fields.TryGetValue("Rotation".ToIdentifier(), out var rotationFieldComponents) + ? rotationFieldComponents.OfType().FirstOrDefault() + : null; + + var mirrorX = new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityX"), style: "GUIButtonSmall") { ToolTip = TextManager.Get("MirrorEntityXToolTip"), Enabled = Prefab.CanFlipX, @@ -863,10 +905,13 @@ namespace Barotrauma me.FlipX(relativeToSub: false); } if (!SelectedList.Contains(this)) { FlipX(relativeToSub: false); } + ColorFlipButton(button, FlippedX); + if (rotationField != null) { rotationField.FloatValue = Rotation; } return true; } }; - new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityY"), style: "GUIButtonSmall") + ColorFlipButton(mirrorX, FlippedX); + var mirrorY = new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityY"), style: "GUIButtonSmall") { ToolTip = TextManager.Get("MirrorEntityYToolTip"), Enabled = Prefab.CanFlipY, @@ -877,9 +922,12 @@ namespace Barotrauma me.FlipY(relativeToSub: false); } if (!SelectedList.Contains(this)) { FlipY(relativeToSub: false); } + ColorFlipButton(button, FlippedY); + if (rotationField != null) { rotationField.FloatValue = Rotation; } return true; } }; + ColorFlipButton(mirrorY, FlippedY); if (Sprite != null) { var reloadTextureButton = new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("ReloadSprite"), style: "GUIButtonSmall"); @@ -1540,6 +1588,23 @@ namespace Barotrauma RemoveFromDroppedStack(allowClientExecute: true); } break; + case EventType.SetHighlight: + bool isTargetedForThisClient = msg.ReadBoolean(); + if (isTargetedForThisClient) + { + bool highlight = msg.ReadBoolean(); + ExternalHighlight = highlight; + if (highlight) + { + Color highlightColor = msg.ReadColorR8G8B8A8(); + HighlightColor = highlightColor; + } + else + { + HighlightColor = null; + } + } + break; default: throw new Exception($"Malformed incoming item event: unsupported event type {eventType}"); } @@ -1940,5 +2005,13 @@ namespace Barotrauma Inventory.DraggingSlot = null; } } + + public void OnPlayerSkillsChanged() + { + foreach (ItemComponent ic in components) + { + ic.OnPlayerSkillsChanged(); + } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs index 49c3eb0e3..7a7701b9e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs @@ -321,10 +321,8 @@ namespace Barotrauma } else { - if (ResizeHorizontal) - placeSize.X = Math.Max(position.X - placePosition.X, Size.X); - if (ResizeVertical) - placeSize.Y = Math.Max(placePosition.Y - position.Y, Size.Y); + if (ResizeHorizontal) { placeSize.X = Math.Max(position.X - placePosition.X, Size.X); } + if (ResizeVertical) { placeSize.Y = Math.Max(placePosition.Y - position.Y, Size.Y); } if (PlayerInput.PrimaryMouseButtonReleased()) { @@ -369,15 +367,31 @@ namespace Barotrauma } } - public override void DrawPlacing(SpriteBatch spriteBatch, Rectangle placeRect, float scale = 1.0f, SpriteEffects spriteEffects = SpriteEffects.None) + public override void DrawPlacing(SpriteBatch spriteBatch, Rectangle placeRect, float scale = 1.0f, float rotation = 0.0f, SpriteEffects spriteEffects = SpriteEffects.None) { if (!ResizeHorizontal && !ResizeVertical) { - Sprite.Draw(spriteBatch, new Vector2(placeRect.Center.X, -(placeRect.Y - placeRect.Height / 2)), SpriteColor * 0.8f, scale: scale); + sprite.Draw( + spriteBatch: spriteBatch, + pos: new Vector2(placeRect.Center.X, + -(placeRect.Y - placeRect.Height / 2)), + color: SpriteColor * 0.8f, + scale: scale, + rotate: rotation, + spriteEffect: spriteEffects ^ sprite.effects); } else { - Sprite.DrawTiled(spriteBatch, new Vector2(placeRect.X, -placeRect.Y), placeRect.Size.ToVector2(), SpriteColor * 0.8f); + Vector2 position = placeRect.Location.ToVector2(); + Vector2 placeSize = placeRect.Size.ToVector2(); + sprite?.DrawTiled( + spriteBatch: spriteBatch, + position: new Vector2(position.X, -position.Y), + targetSize: placeSize, + rotation: rotation, + textureScale: Vector2.One * scale, + color: SpriteColor * 0.8f, + spriteEffects: spriteEffects ^ sprite.effects); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index 25f13500c..b4786982f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -277,12 +277,21 @@ namespace Barotrauma Rectangle drawRect = Submarine == null ? rect : new Rectangle((int)(Submarine.DrawPosition.X + rect.X), (int)(Submarine.DrawPosition.Y + rect.Y), rect.Width, rect.Height); - if ((IsSelected || IsHighlighted) && editing) + if (editing) { + if (IsSelected || IsHighlighted) + { + GUI.DrawRectangle(spriteBatch, + new Vector2(drawRect.X, -drawRect.Y), + new Vector2(rect.Width, rect.Height), + (IsHighlighted ? Color.LightBlue * 0.8f : GUIStyle.Red * 0.5f) * alpha, false, 0, (int)Math.Max(5.0f / Screen.Selected.Cam.Zoom, 1.0f)); + } + + float waterHeight = WaterVolume / rect.Width; GUI.DrawRectangle(spriteBatch, - new Vector2(drawRect.X, -drawRect.Y), - new Vector2(rect.Width, rect.Height), - (IsHighlighted ? Color.LightBlue * 0.8f : GUIStyle.Red * 0.5f) * alpha, false, 0, (int)Math.Max(5.0f / Screen.Selected.Cam.Zoom, 1.0f)); + new Vector2(drawRect.X, -drawRect.Y + drawRect.Height - waterHeight), + new Vector2(drawRect.Width, waterHeight), + Color.Blue * 0.25f, isFilled: true); } GUI.DrawRectangle(spriteBatch, @@ -746,7 +755,7 @@ namespace Barotrauma var newFire = i < FireSources.Count ? FireSources[i] : - new FireSource(Submarine == null ? pos : pos + Submarine.Position, null, true); + new FireSource(Submarine == null ? pos : pos + Submarine.Position, sourceCharacter: null, isNetworkMessage: true); newFire.Position = pos; newFire.Size = new Vector2(size, newFire.Size.Y); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs index 797801633..52ba077ff 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs @@ -86,7 +86,7 @@ namespace Barotrauma MathUtils.RoundTowardsClosest(center.Y, Submarine.GridSize.Y) - center.Y - Submarine.GridSize.Y / 2); MapEntity.SelectedList.Clear(); - assemblyEntities.ForEach(e => MapEntity.AddSelection(e)); + entities.ForEach(e => MapEntity.AddSelection(e)); foreach (MapEntity mapEntity in assemblyEntities) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreatureManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreatureManager.cs index 7dc0b6ad4..1bdf40355 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreatureManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreatureManager.cs @@ -43,7 +43,7 @@ namespace Barotrauma { mainElement = mainElement.FirstElement(); prefabs.Clear(); - DebugConsole.NewMessage($"Overriding all background creatures with '{configPath}'", Color.Yellow); + DebugConsole.NewMessage($"Overriding all background creatures with '{configPath}'", Color.MediumPurple); } else if (prefabs.Any()) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs index 38299827b..5fe2aeca2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs @@ -1,10 +1,9 @@ using Barotrauma.Networking; +using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; -using System.Linq; using System.Collections.Generic; -using FarseerPhysics; namespace Barotrauma { @@ -53,7 +52,7 @@ namespace Barotrauma foreach (InterestingPosition pos in PositionsOfInterest) { Color color = Color.Yellow; - if (pos.PositionType == PositionType.Cave) + if (pos.PositionType == PositionType.Cave || pos.PositionType == PositionType.AbyssCave) { color = Color.DarkOrange; } @@ -61,6 +60,10 @@ namespace Barotrauma { color = Color.LightGray; } + if (!pos.IsValid) + { + color = Color.Red; + } GUI.DrawRectangle(spriteBatch, new Vector2(pos.Position.X - 15.0f, -pos.Position.Y - 15.0f), new Vector2(30.0f, 30.0f), color, true); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs index 745af094f..91a25af00 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs @@ -162,6 +162,8 @@ namespace Barotrauma CanBeVisible = Sprite != null || Prefab.DeformableSprite != null || + ParticleEmitters is { Length: > 0 } || + (GameMain.DebugDraw && Triggers is { Count: > 0 }) || Prefab.OverrideProperties.Any(p => p != null && (p.Sprites.Any() || p.DeformableSprite != null)); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs index 4ec014826..80027c367 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -23,7 +23,14 @@ namespace Barotrauma private Rectangle currentGridIndices; public bool ForceRefreshVisibleObjects; - + + partial void RemoveProjSpecific() + { + visibleObjectsBack.Clear(); + visibleObjectsMid.Clear(); + visibleObjectsFront.Clear(); + } + partial void UpdateProjSpecific(float deltaTime) { foreach (LevelObject obj in visibleObjectsBack) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs index 82ad83280..bb2024258 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs @@ -133,6 +133,8 @@ namespace Barotrauma.Lights public Rectangle BoundingBox { get; private set; } + public bool IsInvalid { get; private set; } + public ConvexHull(Rectangle rect, bool isHorizontal, MapEntity parent) { shadowEffect ??= new BasicEffect(GameMain.Instance.GraphicsDevice) @@ -481,15 +483,34 @@ namespace Barotrauma.Lights for (int i = 0; i < 4; i++) { vertices[i].WorldPos = vertices[i].Pos; + ValidateVertex(vertices[i].WorldPos, "vertices[i].Pos"); segments[i].Start.WorldPos = segments[i].Start.Pos; + ValidateVertex(segments[i].Start.WorldPos, "segments[i].Start.Pos"); segments[i].End.WorldPos = segments[i].End.Pos; + ValidateVertex(segments[i].End.WorldPos, "segments[i].End.Pos"); } if (ParentEntity == null || ParentEntity.Submarine == null) { return; } for (int i = 0; i < 4; i++) { vertices[i].WorldPos += ParentEntity.Submarine.DrawPosition; + ValidateVertex(vertices[i].WorldPos, "vertices[i].WorldPos"); segments[i].Start.WorldPos += ParentEntity.Submarine.DrawPosition; + ValidateVertex(segments[i].Start.WorldPos, "segments[i].Start.WorldPos"); segments[i].End.WorldPos += ParentEntity.Submarine.DrawPosition; + ValidateVertex(segments[i].End.WorldPos, "segments[i].End.WorldPos"); + } + + void ValidateVertex(Vector2 vertex, string debugName) + { + if (!MathUtils.IsValid(vertex)) + { + IsInvalid = true; + string errorMsg = $"Invalid vertex on convex hull ({debugName}: {vertex}, parent entity: {ParentEntity?.ToString() ?? "null"})."; +#if DEBUG + DebugConsole.ThrowError(errorMsg); +#endif + GameAnalyticsManager.AddErrorEventOnce("ConvexHull.RefreshWorldPositions:InvalidVertex", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index c04db86d6..4de4c7810 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -234,6 +234,15 @@ namespace Barotrauma.Lights } } + public void DebugDrawVertices(SpriteBatch spriteBatch) + { + foreach (LightSource light in lights) + { + if (!light.Enabled) { continue; } + light.DebugDrawVertices(spriteBatch); + } + } + public void RenderLightMap(GraphicsDevice graphics, SpriteBatch spriteBatch, Camera cam, RenderTarget2D backgroundObstructor = null) { if (!LightingEnabled) { return; } @@ -269,6 +278,9 @@ namespace Barotrauma.Lights light.Position = pos; } + //above the top boundary of the level (in an inactive respawn shuttle?) + if (Level.Loaded != null && light.WorldPosition.Y > Level.Loaded.Size.Y) { continue; } + float range = light.LightSourceParams.TextureRange; if (light.LightSprite != null) { @@ -801,6 +813,8 @@ namespace Barotrauma.Lights public void ClearLights() { + activeLights.Clear(); + activeLightsWithLightVolume.Clear(); lights.Clear(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs index 7606b40c6..5e9a1f267 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs @@ -37,9 +37,9 @@ namespace Barotrauma.Lights TextureRange = range; if (OverrideLightTexture != null) { - TextureRange += Math.Max( - Math.Abs(OverrideLightTexture.RelativeOrigin.X - 0.5f) * OverrideLightTexture.size.X, - Math.Abs(OverrideLightTexture.RelativeOrigin.Y - 0.5f) * OverrideLightTexture.size.Y); + TextureRange *= 1.0f + Math.Max( + Math.Abs(OverrideLightTexture.RelativeOrigin.X - 0.5f), + Math.Abs(OverrideLightTexture.RelativeOrigin.Y - 0.5f)); } } } @@ -238,7 +238,11 @@ namespace Barotrauma.Lights private bool needsRecalculationWhenUpToDate; public bool NeedsRecalculation { - get { return needsRecalculation; } + get + { + if (ParentBody?.UserData is Item it && it.Prefab.Identifier == "flashlight") { return true; } + return needsRecalculation; + } set { if (!needsRecalculation && value) @@ -708,6 +712,7 @@ namespace Barotrauma.Lights { foreach (ConvexHull hull in chList.List) { + if (hull.IsInvalid) { continue; } if (!chList.IsHidden.Contains(hull)) { //find convexhull segments that are close enough and facing towards the light source @@ -735,6 +740,7 @@ namespace Barotrauma.Lights GameMain.LightManager.AddRayCastTask(this, drawPos, rotation); } + const float MinPointDistance = 6; public void RayCastTask(Vector2 drawPos, float rotation) { @@ -877,12 +883,11 @@ namespace Barotrauma.Lights } } - const float MinPointDistance = 6; - //remove points that are very close to each other - for (int i = 0; i < points.Count; i++) + //+= 2 because the points are added in pairs above, i.e. 0 and 1 belong to the same segment + for (int i = 0; i < points.Count; i += 2) { - for (int j = Math.Min(i + 4, points.Count - 1); j > i; j--) + for (int j = Math.Min(i + 2, points.Count - 1); j > i; j--) { if (Math.Abs(points[i].WorldPos.X - points[j].WorldPos.X) < MinPointDistance && Math.Abs(points[i].WorldPos.Y - points[j].WorldPos.Y) < MinPointDistance) @@ -892,14 +897,14 @@ namespace Barotrauma.Lights } } - var compareCCW = new CompareSegmentPointCW(drawPos); try { - points.Sort(compareCCW); + var compareCW = new CompareSegmentPointCW(drawPos); + points.Sort(compareCW); } catch (Exception e) { - StringBuilder sb = new StringBuilder("Constructing light volumes failed! Light pos: " + drawPos + ", Hull verts:\n"); + StringBuilder sb = new StringBuilder($"Constructing light volumes failed ({nameof(CompareSegmentPointCW)})! Light pos: {drawPos}, Hull verts:\n"); foreach (SegmentPoint sp in points) { sb.AppendLine(sp.Pos.ToString()); @@ -914,7 +919,11 @@ namespace Barotrauma.Lights verts.Clear(); foreach (SegmentPoint p in points) { - Vector2 dir = Vector2.Normalize(p.WorldPos - drawPos); + Vector2 diff = p.WorldPos - drawPos; + float dist = diff.Length(); + //light source exactly at the segment point, don't cast a shadow (normalizing the vector would lead to NaN) + if (dist <= 0.0001f) { continue; } + Vector2 dir = diff / dist; Vector2 dirNormal = new Vector2(-dir.Y, dir.X) * MinPointDistance; //do two slightly offset raycasts to hit the segment itself and whatever's behind it @@ -940,9 +949,36 @@ namespace Barotrauma.Lights { //the raycasts landed on different segments //we definitely want to generate new geometry here - verts.Add(isPoint1 ? p.WorldPos : intersection1.pos); - verts.Add(isPoint2 ? p.WorldPos : intersection2.pos); - markAsVisible = true; + if (isPoint1) + { + TryAddPoints(intersection2.pos, p.WorldPos, drawPos, verts); + markAsVisible = true; + } + else if (isPoint2) + { + TryAddPoints(intersection1.pos, p.WorldPos, drawPos, verts); + markAsVisible = true; + } + else + { + //didn't hit either point, completely obstructed + verts.Add(intersection1.pos); + verts.Add(intersection2.pos); + } + static void TryAddPoints(Vector2 intersection, Vector2 point, Vector2 refPos, List verts) + { + //* 0.8f because we don't care about obstacles that are very close (intersecting walls), + //only about obstacles that are clearly between the point and the refPos + bool intersectionCloserThanPoint = Vector2.DistanceSquared(intersection, refPos) < Vector2.DistanceSquared(point, refPos) * 0.8f; + //if the raycast hit a segment that's closer than the point we're aiming towards, + //it means we didn't hit a segment behind the point, but something that's obstructing it + //= we don't want to add vertex at that obstructed point, it could make the light go through obstacles + if (!intersectionCloserThanPoint) + { + verts.Add(point); + } + verts.Add(intersection); + } } if (markAsVisible) { @@ -959,15 +995,32 @@ namespace Barotrauma.Lights //remove points that are very close to each other for (int i = 0; i < verts.Count - 1; i++) { - for (int j = Math.Min(i + 4, verts.Count - 1); j > i; j--) + for (int j = verts.Count - 1; j > i; j--) { - if (Math.Abs(verts[i].X - verts[j].X) < 6 && - Math.Abs(verts[i].Y - verts[j].Y) < 6) + if (Math.Abs(verts[i].X - verts[j].X) < MinPointDistance && + Math.Abs(verts[i].Y - verts[j].Y) < MinPointDistance) { verts.RemoveAt(j); } } } + + try + { + var compareCW = new CompareCW(drawPos); + verts.Sort(compareCW); + } + catch (Exception e) + { + StringBuilder sb = new StringBuilder($"Constructing light volumes failed ({nameof(CompareSegmentPointCW)})! Light pos: {drawPos}, verts:\n"); + foreach (Vector2 v in verts) + { + sb.AppendLine(v.ToString()); + } + DebugConsole.ThrowError(sb.ToString(), e); + } + + calculatedDrawPos = drawPos; state = LightVertexState.PendingVertexRecalculation; } @@ -1114,7 +1167,7 @@ namespace Barotrauma.Lights //add the normals together and use some magic numbers to create //a somewhat useful/good-looking blur - float blurDistance = 40.0f; + float blurDistance = 25.0f; Vector2 nDiff = nDiff1 * blurDistance; if (MathUtils.GetLineIntersection(vertex + (nDiff1 * blurDistance), nextVertex + (nDiff1 * blurDistance), vertex + (nDiff2 * blurDistance), prevVertex + (nDiff2 * blurDistance), true, out Vector2 intersection)) { @@ -1230,7 +1283,8 @@ namespace Barotrauma.Lights /// public void DrawSprite(SpriteBatch spriteBatch, Camera cam) { - if (GameMain.DebugDraw) + //uncomment if you want to visualize the bounds of the light volume + /*if (GameMain.DebugDraw) { Vector2 drawPos = position; if (ParentSub != null) @@ -1269,7 +1323,7 @@ namespace Barotrauma.Lights { GUI.DrawLine(spriteBatch, boundaryCorners[i].Pos, boundaryCorners[(i + 1) % 4].Pos, Color.White, 0, 3); } - } + }*/ if (DeformableLightSprite != null) { @@ -1318,7 +1372,7 @@ namespace Barotrauma.Lights if (LightTextureTargetSize != Vector2.Zero) { - LightSprite.DrawTiled(spriteBatch, drawPos, LightTextureTargetSize, color, startOffset: LightTextureOffset, textureScale: LightTextureScale); + LightSprite.DrawTiled(spriteBatch, drawPos, LightTextureTargetSize, color: color, startOffset: LightTextureOffset, textureScale: LightTextureScale); } else { @@ -1367,6 +1421,38 @@ namespace Barotrauma.Lights } } + public void DebugDrawVertices(SpriteBatch spriteBatch) + { + if (Range < 1.0f || Color.A < 1 || CurrentBrightness <= 0.0f) { return; } + + //commented out because this is mostly just useful in very specific situations, otherwise it just makes debugdraw very messy + //(you may also need to add a condition here that only draws this for the specific light you're interested in) + if (GameMain.DebugDraw && vertices != null) + { + if (ParentBody?.UserData is Item it && it.Prefab.Identifier == "flashlight") + + for (int i = 1; i < vertices.Length - 1; i += 2) + { + Vector2 vert1 = new Vector2(vertices[i].Position.X, vertices[i].Position.Y); + int nextIndex = (i + 2) % vertices.Length; + //the first vertex is the one at the position of the light source, skip that one + //(we just want to draw lines between the vertices at the circumference of the light volume) + if (nextIndex == 0) { nextIndex++; } + Vector2 vert2 = new Vector2(vertices[nextIndex].Position.X, vertices[nextIndex].Position.Y); + if (ParentSub != null) + { + vert1 += ParentSub.DrawPosition; + vert2 += ParentSub.DrawPosition; + } + vert1.Y = -vert1.Y; + vert2.Y = -vert2.Y; + + var randomColor = ToolBox.GradientLerp(i / (float)vertices.Length, Color.Magenta, Color.Blue, Color.Yellow, Color.Green, Color.Cyan, Color.Red, Color.Purple, Color.Yellow); + GUI.DrawLine(spriteBatch, vert1, vert2, randomColor * 0.8f, width: 2); + } + } + } + public void DrawLightVolume(SpriteBatch spriteBatch, BasicEffect lightEffect, Matrix transform, bool allowRecalculation, ref int recalculationCount) { if (Range < 1.0f || Color.A < 1 || CurrentBrightness <= 0.0f) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index 783c6d66a..3b847fece 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -291,14 +291,14 @@ namespace Barotrauma private readonly List mapNotifications = new List(); - partial void ChangeLocationTypeProjSpecific(Location location, string prevName, LocationTypeChange change) + partial void ChangeLocationTypeProjSpecific(Location location, LocalizedString prevName, LocationTypeChange change) { var messages = change.GetMessages(location.Faction); if (!messages.Any()) { return; } string msg = messages.GetRandom(Rand.RandSync.Unsynced) .Replace("[previousname]", $"‖color:gui.yellow‖{prevName}‖end‖") - .Replace("[name]", $"‖color:gui.yellow‖{location.Name}‖end‖"); + .Replace("[name]", $"‖color:gui.yellow‖{location.DisplayName}‖end‖"); location.LastTypeChangeMessage = msg; mapNotifications.Add(new MapNotification(msg, GUIStyle.SubHeadingFont, mapNotifications, location)); @@ -377,7 +377,7 @@ namespace Barotrauma bool showReputation = hudVisibility > 0.0f && location.Type.HasOutpost && location.Reputation != null; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), location.Name, font: GUIStyle.LargeFont) { Padding = Vector4.Zero }; + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), location.DisplayName, font: GUIStyle.LargeFont) { Padding = Vector4.Zero }; if (!location.Type.Name.IsNullOrEmpty()) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), location.Type.Name, font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero }; @@ -1080,6 +1080,7 @@ namespace Barotrauma } float dist = Vector2.Distance(start, end); var connectionSprite = connection.Passed ? generationParams.PassedConnectionSprite : generationParams.ConnectionSprite; + if (connectionSprite?.Texture == null) { continue; } Color segmentColor = connectionColor; int segmentWidth = width; @@ -1092,9 +1093,6 @@ namespace Barotrauma segmentWidth /= 2; segmentColor = connection.Passed ? generationParams.ConnectionColor : generationParams.UnvisitedConnectionColor; } - else - { - } } spriteBatch.Draw(connectionSprite.Texture, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Radiation.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Radiation.cs index 2ed19962f..5f8fdb8ef 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Radiation.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Radiation.cs @@ -29,7 +29,7 @@ namespace Barotrauma Vector2 spriteScale = new Vector2(zoom); - uiSprite.Sprite.DrawTiled(spriteBatch, topLeft, size, Params.RadiationAreaColor, Vector2.Zero, textureScale: spriteScale); + uiSprite.Sprite.DrawTiled(spriteBatch, topLeft, size, color: Params.RadiationAreaColor, startOffset: Vector2.Zero, textureScale: spriteScale); Vector2 topRight = topLeft + Vector2.UnitX * size.X; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index 7fefcda46..4e97d4929 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Barotrauma.Lights; +using Microsoft.Xna.Framework; namespace Barotrauma { @@ -65,9 +66,7 @@ namespace Barotrauma disableSelect = value; if (disableSelect) { - startMovingPos = Vector2.Zero; - selectionSize = Vector2.Zero; - selectionPos = Vector2.Zero; + StopSelection(); } } } @@ -163,21 +162,9 @@ namespace Barotrauma { if (SelectedAny) { - if (SelectedList.Any(static t => t is Item it && it.GetComponent() is not null)) - { - GUI.AskForConfirmation(SubEditorScreen.CircuitBoxDeletionWarningHeader, SubEditorScreen.CircuitBoxDeletionWarningBody, onConfirm: Delete); - } - else - { - Delete(); - } - - void Delete() - { - SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List(SelectedList), true)); - SelectedList.ForEach(static e => { if (!e.Removed) { e.Remove(); } }); - SelectedList.Clear(); - } + SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List(SelectedList), true)); + SelectedList.ForEachMod(static e => { if (!e.Removed) { e.Remove(); } }); + SelectedList.Clear(); } } @@ -494,6 +481,13 @@ namespace Barotrauma } } + public static void StopSelection() + { + startMovingPos = Vector2.Zero; + selectionSize = Vector2.Zero; + selectionPos = Vector2.Zero; + } + public static Vector2 GetNudgeAmount(bool doHold = true) { Vector2 nudgeAmount = Vector2.Zero; @@ -792,12 +786,16 @@ namespace Barotrauma foreach (MapEntity e in SelectedList) { SpriteEffects spriteEffects = SpriteEffects.None; + float spriteRotation = 0.0f; + float rectangleRotation = 0.0f; switch (e) { case Item item: { if (item.FlippedX && item.Prefab.CanSpriteFlipX) { spriteEffects ^= SpriteEffects.FlipHorizontally; } - if (item.flippedY && item.Prefab.CanSpriteFlipY) { spriteEffects ^= SpriteEffects.FlipVertically; } + if (item.FlippedY && item.Prefab.CanSpriteFlipY) { spriteEffects ^= SpriteEffects.FlipVertically; } + spriteRotation = MathHelper.ToRadians(item.Rotation); + rectangleRotation = spriteRotation; var wire = item.GetComponent(); if (wire != null && wire.Item.body != null && !wire.Item.body.Enabled) { @@ -809,7 +807,10 @@ namespace Barotrauma case Structure structure: { if (structure.FlippedX && structure.Prefab.CanSpriteFlipX) { spriteEffects ^= SpriteEffects.FlipHorizontally; } - if (structure.flippedY && structure.Prefab.CanSpriteFlipY) { spriteEffects ^= SpriteEffects.FlipVertically; } + if (structure.FlippedY && structure.Prefab.CanSpriteFlipY) { spriteEffects ^= SpriteEffects.FlipVertically; } + spriteRotation = MathHelper.ToRadians(structure.Rotation); + rectangleRotation = spriteRotation; + if (structure.FlippedX != structure.FlippedY) { rectangleRotation = -rectangleRotation; } break; } case WayPoint wayPoint: @@ -831,11 +832,12 @@ namespace Barotrauma } } e.Prefab?.DrawPlacing(spriteBatch, - new Rectangle(e.WorldRect.Location + new Point((int)moveAmount.X, (int)-moveAmount.Y), e.WorldRect.Size), e.Scale, spriteEffects); + new Rectangle(e.WorldRect.Location + new Point((int)moveAmount.X, (int)-moveAmount.Y), e.WorldRect.Size), e.Scale, spriteRotation, spriteEffects: spriteEffects); GUI.DrawRectangle(spriteBatch, - new Vector2(e.WorldRect.X, -e.WorldRect.Y) + moveAmount, - new Vector2(e.rect.Width, e.rect.Height), - Color.White, false, 0, (int)Math.Max(3.0f / GameScreen.Selected.Cam.Zoom, 2.0f)); + center: e.WorldRect.Center.ToVector2().FlipY() + moveAmount + new Vector2(0f, e.WorldRect.Height), + width: e.WorldRect.Width, height: e.WorldRect.Height, + rotation: rectangleRotation, clr: Color.White, + depth: 0f, thickness: Math.Max(3.0f / GameScreen.Selected.Cam.Zoom, 2.0f)); } //stop dragging the "selection rectangle" @@ -877,6 +879,23 @@ namespace Barotrauma spriteBatch.DrawLine(corners[3] + offset, corners[0] - offset, color, thickness); } + protected static void ColorFlipButton(GUIButton btn, bool flip) + { + var color = flip ? GUIStyle.Green : Color.White; + var hsv = ToolBox.RGBToHSV(color); + + // Boost saturation and reduce value a bit because our default colors are too muted for this button's style + var hsvBase = hsv; + hsvBase.Y *= 4f; + hsvBase.Z *= 0.8f; + btn.Color = ToolBox.HSVToRGB(hsvBase.X, hsvBase.Y, hsvBase.Z); + btn.SelectedColor = ToolBox.HSVToRGB(hsvBase.X, hsvBase.Y, hsvBase.Z); + + var hsvHover = hsv; + hsvHover.Z *= 1.2f; + btn.HoverColor = ToolBox.HSVToRGB(hsvHover.X, hsvHover.Y, hsvHover.Z); + } + public static List FilteredSelectedList { get; private set; } = new List(); public static void UpdateEditor(Camera cam, float deltaTime) @@ -1105,6 +1124,25 @@ namespace Barotrauma public virtual void DrawEditing(SpriteBatch spriteBatch, Camera cam) { } + private float RotationRad + => MathHelper.ToRadians( + this switch + { + Structure s => s.Rotation, + Item it => it.Rotation, + _ => 0.0f + }); + + private Vector2 GetEditingHandlePos(int x, int y, Camera cam) + { + Vector2 handleDiff = new Vector2(x * (rect.Width * 0.5f), y * (rect.Height * 0.5f)); + var rotation = -RotationRad; + handleDiff = MathUtils.RotatePoint(handleDiff, rotation); + if (FlippedX) { handleDiff = handleDiff.FlipX(); } + if (FlippedY) { handleDiff = handleDiff.FlipY(); } + return cam.WorldToScreen(Position + handleDiff); + } + float ResizeHandleSize => 10 * GUI.Scale; float ResizeHandleHighlightDistance => 8 * GUI.Scale; @@ -1119,9 +1157,10 @@ namespace Barotrauma { for (int y = startY; y < 2; y += 2) { - Vector2 handlePos = cam.WorldToScreen(Position + new Vector2(x * (rect.Width * 0.5f), y * (rect.Height * 0.5f))); + Vector2 handlePos = GetEditingHandlePos(x, y, cam); bool highlighted = Vector2.DistanceSquared(PlayerInput.MousePosition, handlePos) < ResizeHandleHighlightDistance * ResizeHandleHighlightDistance; + if (highlighted && PlayerInput.PrimaryMouseButtonDown()) { selectionPos = Vector2.Zero; @@ -1138,44 +1177,83 @@ namespace Barotrauma { if (prevRect == null) { - prevRect = new Rectangle(Rect.Location, Rect.Size); + prevRect = Rect; } - Vector2 placePosition = new Vector2(rect.X, rect.Y); - Vector2 placeSize = new Vector2(rect.Width, rect.Height); + Vector2 placePosition = prevRect.Value.Location.ToVector2(); + Vector2 placeSize = prevRect.Value.Size.ToVector2(); - Vector2 mousePos = Submarine.MouseToWorldGrid(cam, Submarine.MainSub, round: true); - - if (PlayerInput.IsShiftDown()) + static Vector2 flipThenRotate(Vector2 point, Vector2 center, float angle, bool flipX, bool flipY) { - mousePos = cam.ScreenToWorld(PlayerInput.MousePosition); + if (flipX) { point = (point - center).FlipX() + center; } + if (flipY) { point = (point - center).FlipY() + center; } + + point = MathUtils.RotatePointAroundTarget(point, center, angle); + + return point; + } + + static Vector2 rotateThenFlip(Vector2 point, Vector2 center, float angle, bool flipX, bool flipY) + { + point = MathUtils.RotatePointAroundTarget(point, center, angle); + + if (flipX) { point = (point - center).FlipX() + center; } + if (flipY) { point = (point - center).FlipY() + center; } + + return point; + } + + Vector2 mousePos = cam.ScreenToWorld(PlayerInput.MousePosition); + Vector2 prevPos = placePosition; + Vector2 prevOppositeCorner = prevPos + placeSize.FlipY(); + Vector2 prevCenter = placePosition + placeSize.FlipY() * 0.5f; + mousePos = flipThenRotate(mousePos, prevCenter, RotationRad, FlippedX, FlippedY); + + if (!PlayerInput.IsShiftDown()) + { + mousePos = Submarine.VectorToWorldGrid(mousePos, Submarine.MainSub, round: true); } if (resizeDirX > 0) { - mousePos.X = Math.Max(mousePos.X, rect.X + Submarine.GridSize.X); + mousePos.X = Math.Max(mousePos.X, prevRect.Value.X + Submarine.GridSize.X); placeSize.X = mousePos.X - placePosition.X; } else if (resizeDirX < 0) { - mousePos.X = Math.Min(mousePos.X, rect.Right - Submarine.GridSize.X); + mousePos.X = Math.Min(mousePos.X, prevRect.Value.Right - Submarine.GridSize.X); placeSize.X = MathF.Round((placePosition.X + placeSize.X) - mousePos.X); placePosition.X = MathF.Round(mousePos.X); } if (resizeDirY < 0) { - mousePos.Y = Math.Min(mousePos.Y, rect.Y - Submarine.GridSize.Y); + mousePos.Y = Math.Min(mousePos.Y, prevRect.Value.Y - Submarine.GridSize.Y); placeSize.Y = placePosition.Y - mousePos.Y; } else if (resizeDirY > 0) { - mousePos.Y = Math.Max(mousePos.Y, rect.Y - rect.Height + Submarine.GridSize.X); + mousePos.Y = Math.Max(mousePos.Y, prevRect.Value.Y - prevRect.Value.Height + Submarine.GridSize.Y); - placeSize.Y = mousePos.Y - (rect.Y - rect.Height); + placeSize.Y = mousePos.Y - (prevRect.Value.Y - prevRect.Value.Height); placePosition.Y = mousePos.Y; } + Vector2 newPos = placePosition; + Vector2 newOppositeCorner = placePosition + placeSize.FlipY(); + + Vector2 transformedCornerDiff = rotateThenFlip(newPos-prevPos, Vector2.Zero, -RotationRad, FlippedX, FlippedY); + Vector2 transformedOppositeCornerDiff = rotateThenFlip(newOppositeCorner-prevOppositeCorner, Vector2.Zero, -RotationRad, FlippedX, FlippedY); + + Vector2 newPosTransformed = rotateThenFlip(prevPos, prevCenter, -RotationRad, FlippedX, FlippedY) + + transformedCornerDiff; + Vector2 newOppositeTransformed = rotateThenFlip(prevOppositeCorner, prevCenter, -RotationRad, FlippedX, FlippedY) + + transformedOppositeCornerDiff; + Vector2 newTransformedCenter = (newPosTransformed + newOppositeTransformed) * 0.5f; + + var newDiff = (newOppositeCorner - newPos) * 0.5f; + placePosition = newTransformedCenter - newDiff; + if ((int)placePosition.X != rect.X || (int)placePosition.Y != rect.Y || (int)placeSize.X != rect.Width || (int)placeSize.Y != rect.Height) { Rect = new Rectangle((int)placePosition.X, (int)placePosition.Y, (int)placeSize.X, (int)placeSize.Y); @@ -1210,15 +1288,16 @@ namespace Barotrauma IsHighlighted = true; int startX = ResizeHorizontal ? -1 : 0; - int StartY = ResizeVertical ? -1 : 0; + int startY = ResizeVertical ? -1 : 0; for (int x = startX; x < 2; x += 2) { - for (int y = StartY; y < 2; y += 2) + for (int y = startY; y < 2; y += 2) { - Vector2 handlePos = cam.WorldToScreen(Position + new Vector2(x * (rect.Width * 0.5f), y * (rect.Height * 0.5f))); + Vector2 handlePos = GetEditingHandlePos(x, y, cam); bool highlighted = Vector2.DistanceSquared(PlayerInput.MousePosition, handlePos) < ResizeHandleHighlightDistance * ResizeHandleHighlightDistance; + var color = Color.White * (highlighted ? 1.0f : 0.6f); if (highlighted && !PlayerInput.PrimaryMouseButtonHeld()) { GUI.MouseCursor = CursorState.Hand; @@ -1226,7 +1305,7 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, handlePos - new Vector2(ResizeHandleSize / 2), new Vector2(ResizeHandleSize), - Color.White * (highlighted ? 1.0f : 0.6f), + color, true, 0, (int)Math.Max(1.5f / GameScreen.Selected.Cam.Zoom, 1.0f)); } @@ -1241,12 +1320,15 @@ namespace Barotrauma HashSet foundEntities = new HashSet(); Rectangle selectionRect = Submarine.AbsRect(pos, size); + Quad2D selectionQuad = Quad2D.FromSubmarineRectangle(selectionRect); foreach (MapEntity entity in MapEntityList) { if (!entity.SelectableInEditor) { continue; } - if (Submarine.RectsOverlap(selectionRect, entity.rect)) + Quad2D entityQuad = entity.GetTransformedQuad(); + + if (selectionQuad.Intersects(entityQuad)) { foundEntities.Add(entity); entity.IsIncludedInSelection = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs index d70dc4087..5007fbecc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs @@ -84,7 +84,7 @@ namespace Barotrauma } } - public virtual void DrawPlacing(SpriteBatch spriteBatch, Rectangle drawRect, float scale = 1.0f, SpriteEffects spriteEffects = SpriteEffects.None) + public virtual void DrawPlacing(SpriteBatch spriteBatch, Rectangle drawRect, float scale = 1.0f, float rotation = 0.0f, SpriteEffects spriteEffects = SpriteEffects.None) { if (Submarine.MainSub != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs index 18e2bef75..1b3c99d28 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs @@ -1,10 +1,9 @@ #nullable enable +using Barotrauma.Sounds; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Globalization; -using System.Xml.Linq; -using Barotrauma.Sounds; -using Microsoft.Xna.Framework; namespace Barotrauma { @@ -45,7 +44,8 @@ namespace Barotrauma } if (FrequencyMultiplierRange.Y > 4.0f) { - DebugConsole.ThrowError($"Loaded frequency range exceeds max value: {FrequencyMultiplierRange} (original string was \"{freqMultAttr}\")"); + DebugConsole.ThrowError($"Loaded frequency range exceeds max value: {FrequencyMultiplierRange} (original string was \"{freqMultAttr}\")", + contentPackage: element.ContentPackage); } IgnoreMuffling = element.GetAttributeBool("dontmuffle", false); } @@ -65,7 +65,8 @@ namespace Barotrauma if (filename is null) { string errorMsg = "Error when loading round sound (" + element + ") - file path not set"; - DebugConsole.ThrowError(errorMsg); + DebugConsole.ThrowError(errorMsg, + contentPackage: element.ContentPackage); GameAnalyticsManager.AddErrorEventOnce("RoundSound.LoadRoundSound:FilePathEmpty" + element.ToString(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); return null; } @@ -86,7 +87,8 @@ namespace Barotrauma catch (System.IO.FileNotFoundException e) { string errorMsg = "Failed to load sound file \"" + filename + "\" (file not found)."; - DebugConsole.ThrowError(errorMsg, e); + DebugConsole.ThrowError(errorMsg, e, + contentPackage: element.ContentPackage); if (!ContentPackageManager.ModsEnabled) { GameAnalyticsManager.AddErrorEventOnce("RoundSound.LoadRoundSound:FileNotFound" + filename, GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); @@ -96,7 +98,8 @@ namespace Barotrauma catch (System.IO.InvalidDataException e) { string errorMsg = "Failed to load sound file \"" + filename + "\" (invalid data)."; - DebugConsole.ThrowError(errorMsg, e); + DebugConsole.ThrowError(errorMsg, e, + contentPackage: element.ContentPackage); GameAnalyticsManager.AddErrorEventOnce("RoundSound.LoadRoundSound:InvalidData" + filename, GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); return null; } @@ -123,7 +126,8 @@ namespace Barotrauma catch (System.IO.FileNotFoundException e) { string errorMsg = "Failed to load sound file \"" + roundSound.Filename + "\"."; - DebugConsole.ThrowError(errorMsg, e); + DebugConsole.ThrowError(errorMsg, e, + contentPackage: roundSound.Sound?.XElement?.ContentPackage); GameAnalyticsManager.AddErrorEventOnce("RoundSound.LoadRoundSound:FileNotFound" + roundSound.Filename, GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index 3c28cbe17..4fe5ce8e0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -150,7 +150,8 @@ namespace Barotrauma Stretch = true, RelativeSpacing = 0.01f }; - new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityX"), style: "GUIButtonSmall") + + var mirrorX = new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityX"), style: "GUIButtonSmall") { ToolTip = TextManager.Get("MirrorEntityXToolTip"), OnClicked = (button, data) => @@ -160,10 +161,12 @@ namespace Barotrauma me.FlipX(relativeToSub: false); } if (!SelectedList.Contains(this)) { FlipX(relativeToSub: false); } + ColorFlipButton(button, FlippedX); return true; } }; - new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityY"), style: "GUIButtonSmall") + ColorFlipButton(mirrorX, FlippedX); + var mirrorY = new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityY"), style: "GUIButtonSmall") { ToolTip = TextManager.Get("MirrorEntityYToolTip"), OnClicked = (button, data) => @@ -173,9 +176,11 @@ namespace Barotrauma me.FlipY(relativeToSub: false); } if (!SelectedList.Contains(this)) { FlipY(relativeToSub: false); } + ColorFlipButton(button, FlippedY); return true; } }; + ColorFlipButton(mirrorY, FlippedY); new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("ReloadSprite"), style: "GUIButtonSmall") { OnClicked = (button, data) => @@ -231,11 +236,14 @@ namespace Barotrauma public override bool IsVisible(Rectangle worldView) { - Rectangle worldRect = WorldRect; + RectangleF worldRect = Quad2D.FromSubmarineRectangle(WorldRect).Rotated( + FlippedX != FlippedY + ? rotationRad + : -rotationRad).BoundingAxisAlignedRectangle; Vector2 worldPos = WorldPosition; - Vector2 min = new Vector2(worldRect.X, worldRect.Y - worldRect.Height); - Vector2 max = new Vector2(worldRect.Right, worldRect.Y); + Vector2 min = new Vector2(worldRect.X, worldRect.Y); + Vector2 max = new Vector2(worldRect.Right, worldRect.Y + worldRect.Height); foreach (DecorativeSprite decorativeSprite in Prefab.DecorativeSprites) { float scale = decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale; @@ -307,7 +315,12 @@ namespace Barotrauma Vector2 bodyPos = WorldPosition + BodyOffset * Scale; - GUI.DrawRectangle(spriteBatch, new Vector2(bodyPos.X, -bodyPos.Y), rectSize.X, rectSize.Y, BodyRotation, Color.White, + GUI.DrawRectangle(sb: spriteBatch, + center: new Vector2(bodyPos.X, -bodyPos.Y), + width: rectSize.X, + height: rectSize.Y, + rotation: BodyRotation, + clr: Color.White, thickness: Math.Max(1, (int)(2 / Screen.Selected.Cam.Zoom))); } @@ -357,8 +370,10 @@ namespace Barotrauma Prefab.BackgroundSprite.DrawTiled( spriteBatch, - new Vector2(rect.X + drawOffset.X, -(rect.Y + drawOffset.Y)), + new Vector2(rect.X + rect.Width / 2 + drawOffset.X, -(rect.Y - rect.Height / 2 + drawOffset.Y)), new Vector2(rect.Width, rect.Height), + rotation: rotationRad, + origin: rect.Size.ToVector2() * new Vector2(0.5f, 0.5f), color: Prefab.BackgroundSpriteColor, textureScale: TextureScale * Scale, startOffset: backGroundOffset, @@ -368,8 +383,10 @@ namespace Barotrauma { Prefab.BackgroundSprite.DrawTiled( spriteBatch, - new Vector2(rect.X + drawOffset.X, -(rect.Y + drawOffset.Y)) + dropShadowOffset, + new Vector2(rect.X + rect.Width / 2 + drawOffset.X, -(rect.Y - rect.Height / 2 + drawOffset.Y)) + dropShadowOffset, new Vector2(rect.Width, rect.Height), + rotation: rotationRad, + origin: rect.Size.ToVector2() * new Vector2(0.5f, 0.5f), color: Color.Black * 0.5f, textureScale: TextureScale * Scale, startOffset: backGroundOffset, @@ -385,6 +402,13 @@ namespace Barotrauma SpriteEffects oldEffects = Prefab.Sprite.effects; Prefab.Sprite.effects ^= SpriteEffects; + Vector2 advanceX = MathUtils.RotatedUnitXRadians(this.rotationRad).FlipY(); + Vector2 advanceY = advanceX.YX().FlipX(); + if (FlippedX != FlippedY) + { + advanceX = advanceX.FlipY(); + advanceY = advanceY.FlipX(); + } for (int i = 0; i < Sections.Length; i++) { Rectangle drawSection = Sections[i].rect; @@ -409,7 +433,7 @@ namespace Barotrauma drawSection = new Rectangle( drawSection.X, drawSection.Y, - Sections[Sections.Length -1 ].rect.Right - drawSection.X, + Sections[Sections.Length - 1].rect.Right - drawSection.X, drawSection.Y - (Sections[Sections.Length - 1].rect.Y - Sections[Sections.Length - 1].rect.Height)); i = Sections.Length; } @@ -424,10 +448,18 @@ namespace Barotrauma sectionOffset.X += MathUtils.PositiveModulo((int)-textureOffset.X, Prefab.Sprite.SourceRect.Width); sectionOffset.Y += MathUtils.PositiveModulo((int)-textureOffset.Y, Prefab.Sprite.SourceRect.Height); + Vector2 pos = new Vector2(drawSection.X, drawSection.Y); + pos -= rect.Location.ToVector2(); + pos = advanceX * pos.X + advanceY * pos.Y; + pos += rect.Location.ToVector2(); + pos = new Vector2(pos.X + rect.Width / 2 + drawOffset.X, -(pos.Y - rect.Height / 2 + drawOffset.Y)); + Prefab.Sprite.DrawTiled( spriteBatch, - new Vector2(drawSection.X + drawOffset.X, -(drawSection.Y + drawOffset.Y)), + pos, new Vector2(drawSection.Width, drawSection.Height), + rotation: rotationRad, + origin: rect.Size.ToVector2() * new Vector2(0.5f, 0.5f), color: color, startOffset: sectionOffset, depth: depth, @@ -437,7 +469,7 @@ namespace Barotrauma foreach (var decorativeSprite in Prefab.DecorativeSprites) { if (!spriteAnimState[decorativeSprite].IsActive) { continue; } - float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor); + float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor) + this.rotationRad; Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier) * Scale; decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), color, rotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, Prefab.Sprite.effects, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs index f031e7fab..587e21dab 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs @@ -94,19 +94,20 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, new Rectangle(newRect.X, -newRect.Y - GameMain.GraphicsHeight, newRect.Width, newRect.Height + GameMain.GraphicsHeight * 2), Color.White); } - public override void DrawPlacing(SpriteBatch spriteBatch, Rectangle placeRect, float scale = 1.0f, SpriteEffects spriteEffects = SpriteEffects.None) + public override void DrawPlacing(SpriteBatch spriteBatch, Rectangle placeRect, float scale = 1.0f, float rotation = 0.0f, SpriteEffects spriteEffects = SpriteEffects.None) { - SpriteEffects oldEffects = Sprite.effects; - Sprite.effects ^= spriteEffects; - + var position = placeRect.Location.ToVector2().FlipY(); + position += placeRect.Size.ToVector2() * 0.5f; + Sprite.DrawTiled( spriteBatch, - new Vector2(placeRect.X, -placeRect.Y), - new Vector2(placeRect.Width, placeRect.Height), - color: Color.White * 0.8f, - textureScale: TextureScale * scale); - - Sprite.effects = oldEffects; + position, + placeRect.Size.ToVector2(), + color: Color.White * 0.8f, + origin: placeRect.Size.ToVector2() * 0.5f, + rotation: rotation, + textureScale: TextureScale * scale, + spriteEffects: spriteEffects ^ Sprite.effects); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index 6d330e43e..3d9e53dc6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -25,7 +25,7 @@ namespace Barotrauma /// /// Margin applied around the view area when culling entities (i.e. entities that are this far outside the view are still considered visible) /// - private const int CullMargin = 500; + private const int CullMargin = 50; /// /// Update entity culling when any corner of the view has moved more than this /// @@ -713,18 +713,12 @@ namespace Barotrauma return GameMain.LightManager.Lights.Count(l => l.CastShadows && !l.IsBackground) - disabledItemLightCount; } - public static Vector2 MouseToWorldGrid(Camera cam, Submarine sub, bool round = false) + public static Vector2 MouseToWorldGrid(Camera cam, Submarine sub, Vector2? mousePos = null, bool round = false) { - Vector2 position = PlayerInput.MousePosition; + Vector2 position = mousePos ?? PlayerInput.MousePosition; position = cam.ScreenToWorld(position); - Vector2 worldGridPos = VectorToWorldGrid(position, round); - - if (sub != null) - { - worldGridPos.X += sub.Position.X % GridSize.X; - worldGridPos.Y += sub.Position.Y % GridSize.Y; - } + Vector2 worldGridPos = VectorToWorldGrid(position, sub, round); return worldGridPos; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs index e056cc997..24c77837e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs @@ -421,7 +421,7 @@ namespace Barotrauma float scale = element.GetAttributeFloat("scale", 1f); Color color = element.GetAttributeColor("spritecolor", Color.White); - float rotation = element.GetAttributeFloat("rotation", 0f); + float rotationRad = MathHelper.ToRadians(element.GetAttributeFloat("rotation", 0f)); MapEntityPrefab prefab; if (element.NameAsIdentifier() == "item" @@ -455,7 +455,7 @@ namespace Barotrauma ItemPrefab itemPrefab = prefab as ItemPrefab; if (itemPrefab != null) { - BakeItemComponents(itemPrefab, rect, color, scale, rotation, depth, out overrideSprite); + BakeItemComponents(itemPrefab, rect, color, scale, rotationRad, depth, out overrideSprite); } if (!overrideSprite) @@ -485,13 +485,15 @@ namespace Barotrauma MathUtils.PositiveModulo((int)-textureOffset.Y, prefab.Sprite.SourceRect.Height)); prefab.Sprite.DrawTiled( - spriteRecorder, - rect.Location.ToVector2() * new Vector2(1f, -1f), - rect.Size.ToVector2(), + spriteBatch: spriteRecorder, + position: new Vector2(rect.X + rect.Width / 2, -(rect.Y - rect.Height / 2)), + targetSize: rect.Size.ToVector2(), + origin: rect.Size.ToVector2() * new Vector2(0.5f, 0.5f), color: color, startOffset: backGroundOffset, textureScale: textureScale * scale, - depth: depth); + depth: depth, + rotation: rotationRad); } else if (itemPrefab != null) { @@ -552,7 +554,7 @@ namespace Barotrauma spritePos * new Vector2(1f, -1f), color, prefab.Sprite.Origin, - rotation, + rotationRad, scale, prefab.Sprite.effects, depth); @@ -564,7 +566,7 @@ namespace Barotrauma if (flippedX) { offset.X = -offset.X; } if (flippedY) { offset.Y = -offset.Y; } decorativeSprite.Sprite.Draw(spriteRecorder, new Vector2(spritePos.X + offset.X, -(spritePos.Y + offset.Y)), color, - MathHelper.ToRadians(rotation) + rot, decorativeSprite.GetScale(0f) * scale, prefab.Sprite.effects, + rotationRad + rot, decorativeSprite.GetScale(0f) * scale, prefab.Sprite.effects, depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - prefab.Sprite.Depth), 0.999f)); } } @@ -577,7 +579,7 @@ namespace Barotrauma private void BakeItemComponents( ItemPrefab prefab, Rectangle rect, Color color, - float scale, float rotation, float depth, + float scale, float rotationRad, float depth, out bool overrideSprite) { overrideSprite = false; @@ -607,7 +609,7 @@ namespace Barotrauma Vector2 relativeBarrelPos = barrelPos * prefab.Scale - new Vector2(rect.Width / 2, rect.Height / 2); var transformedBarrelPos = MathUtils.RotatePoint( relativeBarrelPos, - MathHelper.ToRadians(rotation)); + rotationRad); Vector2 drawPos = new Vector2(rect.X + rect.Width * relativeScale / 2 + transformedBarrelPos.X * relativeScale, rect.Y - rect.Height * relativeScale / 2 - transformedBarrelPos.Y * relativeScale); drawPos.Y = -drawPos.Y; @@ -615,13 +617,13 @@ namespace Barotrauma railSprite?.Draw(spriteRecorder, drawPos, color, - rotation + MathHelper.PiOver2, scale, + rotationRad, scale, SpriteEffects.None, depth + (railSprite.Depth - prefab.Sprite.Depth)); barrelSprite?.Draw(spriteRecorder, drawPos, color, - rotation + MathHelper.PiOver2, scale, + rotationRad, scale, SpriteEffects.None, depth + (barrelSprite.Depth - prefab.Sprite.Depth)); break; @@ -781,7 +783,7 @@ namespace Barotrauma previewFrame = null; } spriteRecorder?.Dispose(); spriteRecorder = null; - camera?.Dispose(); camera = null; + camera = null; isDisposed = true; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs index 6a5bfb3ee..318176714 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs @@ -122,12 +122,12 @@ namespace Barotrauma.Networking VoipSound = null; } - public void SetPermissions(ClientPermissions permissions, IEnumerable permittedConsoleCommands) + public void SetPermissions(ClientPermissions permissions, IEnumerable permittedConsoleCommands) { List permittedCommands = new List(); - foreach (string commandName in permittedConsoleCommands) + foreach (Identifier commandName in permittedConsoleCommands) { - var consoleCommand = DebugConsole.Commands.Find(c => c.names.Contains(commandName)); + var consoleCommand = DebugConsole.Commands.Find(c => c.Names.Contains(commandName)); if (consoleCommand != null) { permittedCommands.Add(consoleCommand); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 36740fb62..c6d338147 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -65,7 +65,7 @@ namespace Barotrauma.Networking public bool LateCampaignJoin = false; private ClientPermissions permissions = ClientPermissions.None; - private List permittedConsoleCommands = new List(); + private List permittedConsoleCommands = new List(); private bool connected; @@ -170,9 +170,9 @@ namespace Barotrauma.Networking internal readonly struct PermissionChangedEvent { public readonly ClientPermissions NewPermissions; - public readonly ImmutableArray NewPermittedConsoleCommands; + public readonly ImmutableArray NewPermittedConsoleCommands; - public PermissionChangedEvent(ClientPermissions newPermissions, IReadOnlyList newPermittedConsoleCommands) + public PermissionChangedEvent(ClientPermissions newPermissions, IReadOnlyList newPermittedConsoleCommands) { NewPermissions = newPermissions; NewPermittedConsoleCommands = newPermittedConsoleCommands.ToImmutableArray(); @@ -1211,11 +1211,11 @@ namespace Barotrauma.Networking targetClient?.SetPermissions(permissions, permittedCommands); if (clientId == SessionId) { - SetMyPermissions(permissions, permittedCommands.Select(command => command.names[0])); + SetMyPermissions(permissions, permittedCommands.Select(command => command.Names[0])); } } - private void SetMyPermissions(ClientPermissions newPermissions, IEnumerable permittedConsoleCommands) + private void SetMyPermissions(ClientPermissions newPermissions, IEnumerable permittedConsoleCommands) { if (!(this.permittedConsoleCommands.Any(c => !permittedConsoleCommands.Contains(c)) || permittedConsoleCommands.Any(c => !this.permittedConsoleCommands.Contains(c)))) @@ -1227,7 +1227,7 @@ namespace Barotrauma.Networking permissions.HasFlag(ClientPermissions.ManageRound) != newPermissions.HasFlag(ClientPermissions.ManageRound); permissions = newPermissions; - this.permittedConsoleCommands = new List(permittedConsoleCommands); + this.permittedConsoleCommands = permittedConsoleCommands.ToList(); //don't show the "permissions changed" popup if the client owns the server if (!IsServerOwner) { @@ -1265,10 +1265,10 @@ namespace Barotrauma.Networking var commandsLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), rightColumn.RectTransform), TextManager.Get("PermittedConsoleCommands"), wrap: true, font: GUIStyle.SubHeadingFont); var commandList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), rightColumn.RectTransform)); - foreach (string permittedCommand in permittedConsoleCommands) + foreach (Identifier permittedCommand in permittedConsoleCommands) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), commandList.Content.RectTransform, minSize: new Point(0, 15)), - permittedCommand, font: GUIStyle.SmallFont) + permittedCommand.Value, font: GUIStyle.SmallFont) { CanBeFocused = false }; @@ -1348,6 +1348,7 @@ namespace Barotrauma.Networking bool respawnAllowed = inc.ReadBoolean(); ServerSettings.AllowDisguises = inc.ReadBoolean(); ServerSettings.AllowRewiring = inc.ReadBoolean(); + ServerSettings.AllowImmediateItemDelivery = inc.ReadBoolean(); ServerSettings.AllowFriendlyFire = inc.ReadBoolean(); ServerSettings.LockAllDefaultWires = inc.ReadBoolean(); ServerSettings.AllowLinkingWifiToChat = inc.ReadBoolean(); @@ -2221,7 +2222,7 @@ namespace Barotrauma.Networking } outmsg.WriteByte((byte)MultiplayerPreferences.Instance.TeamPreference); - if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0) + if (GameMain.GameSession?.GameMode is not MultiPlayerCampaign campaign || campaign.LastSaveID == 0) { outmsg.WriteUInt16((UInt16)0); } @@ -2551,18 +2552,18 @@ namespace Barotrauma.Networking return permissions.HasFlag(permission); } - public bool HasConsoleCommandPermission(string commandName) + public bool HasConsoleCommandPermission(Identifier commandName) { if (!permissions.HasFlag(ClientPermissions.ConsoleCommands)) { return false; } - if (permittedConsoleCommands.Any(c => c.Equals(commandName, StringComparison.OrdinalIgnoreCase))) { return true; } + if (permittedConsoleCommands.Contains(commandName)) { return true; } //check aliases foreach (DebugConsole.Command command in DebugConsole.Commands) { - if (command.names.Contains(commandName)) + if (command.Names.Contains(commandName)) { - if (command.names.Intersect(permittedConsoleCommands).Any()) { return true; } + if (command.Names.Intersect(permittedConsoleCommands).Any()) { return true; } break; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs index f7c5d0a44..4ce4dd2e0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs @@ -75,6 +75,9 @@ namespace Barotrauma.Networking [Serialize("", IsPropertySaveable.Yes)] public LanguageIdentifier Language { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public string SelectedSub { get; set; } = string.Empty; + public Version GameVersion { get; set; } = new Version(0, 0, 0, 0); public Option Ping = Option.None(); @@ -104,6 +107,8 @@ namespace Barotrauma.Networking public ImmutableArray ContentPackages; + public int ContentPackageCount; + public bool IsModded => ContentPackages.Any(p => !GameMain.VanillaContent.NameMatches(p.Name)); public ServerInfo(Endpoint endpoint) @@ -176,33 +181,6 @@ namespace Barotrauma.Networking }; title.Text = ToolBox.LimitString(title.Text, title.Font, (int)(title.Rect.Width * 0.85f)); - bool isFavorite = serverListScreen.IsFavorite(this); - - static LocalizedString favoriteTickBoxToolTip(bool isFavorite) - => TextManager.Get(isFavorite ? "RemoveFromFavorites" : "AddToFavorites"); - - GUITickBox favoriteTickBox = new GUITickBox(new RectTransform(new Vector2(0.15f, 0.8f), title.RectTransform, Anchor.CenterRight), - "", null, "GUIServerListFavoriteTickBox") - { - UserData = this, - Selected = isFavorite, - ToolTip = favoriteTickBoxToolTip(isFavorite), - OnSelected = tickbox => - { - ServerInfo info = (ServerInfo)tickbox.UserData; - if (tickbox.Selected) - { - GameMain.ServerListScreen.AddToFavoriteServers(info); - } - else - { - GameMain.ServerListScreen.RemoveFromFavoriteServers(info); - } - tickbox.ToolTip = favoriteTickBoxToolTip(tickbox.Selected); - return true; - } - }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform), TextManager.AddPunctuation(':', TextManager.Get("ServerListVersion"), GameVersion == new Version(0, 0, 0, 0) ? TextManager.Get("Unknown") : GameVersion.ToString())) @@ -258,6 +236,59 @@ namespace Barotrauma.Networking { Stretch = true }; + + var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 0.25f), playStyleBanner.RectTransform, Anchor.BottomRight), + isHorizontal: true, childAnchor: Anchor.BottomRight); + + //shadow behind the buttons + new GUIFrame(new RectTransform(new Vector2(3.15f, 1.05f), buttonContainer.RectTransform, Anchor.BottomRight, scaleBasis: ScaleBasis.Smallest), style: null) + { + Color = Color.Black * 0.7f, + IgnoreLayoutGroups = true + }; + + bool isFavorite = serverListScreen.IsFavorite(this); + static LocalizedString favoriteTickBoxToolTip(bool isFavorite) + => TextManager.Get(isFavorite ? "RemoveFromFavorites" : "AddToFavorites"); + + GUITickBox favoriteTickBox = new GUITickBox(new RectTransform(Vector2.One, buttonContainer.RectTransform, scaleBasis: ScaleBasis.Smallest), + "", null, "GUIServerListFavoriteTickBox") + { + UserData = this, + Selected = isFavorite, + ToolTip = favoriteTickBoxToolTip(isFavorite), + OnSelected = tickbox => + { + ServerInfo info = (ServerInfo)tickbox.UserData; + if (tickbox.Selected) + { + GameMain.ServerListScreen.AddToFavoriteServers(info); + } + else + { + GameMain.ServerListScreen.RemoveFromFavoriteServers(info); + } + tickbox.ToolTip = favoriteTickBoxToolTip(tickbox.Selected); + return true; + } + }; + + new GUIButton(new RectTransform(Vector2.One, buttonContainer.RectTransform, scaleBasis: ScaleBasis.Smallest), style: "GUIServerListReportServer") + { + ToolTip = TextManager.Get("reportserver"), + OnClicked = (_, _) => {ServerListScreen.CreateReportPrompt(this); return true; } + }; + + new GUIButton(new RectTransform(Vector2.One, buttonContainer.RectTransform, scaleBasis: ScaleBasis.Smallest), style: "GUIServerListHideServer") + { + ToolTip = TextManager.Get("filterserver"), + OnClicked = (_, _) => + { + ServerListScreen.CreateFilterServerPrompt(this); + return true; + } + }; + // playstyle tags ----------------------------------------------------------------------------- var playStyleContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), content.RectTransform), isHorizontal: true) @@ -309,6 +340,14 @@ namespace Barotrauma.Networking TextManager.Get(GameMode.IsEmpty ? "Unknown" : "GameMode." + GameMode).Fallback(GameMode.Value), textAlignment: Alignment.Right); + if (!string.IsNullOrEmpty(SelectedSub)) + { + var submarineText = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("Submarine")); + new GUITextBlock(new RectTransform(Vector2.One, submarineText.RectTransform), + SelectedSub, + textAlignment: Alignment.Right); + } + GUITextBlock playStyleText = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("serverplaystyle")); new GUITextBlock(new RectTransform(Vector2.One, playStyleText.RectTransform), TextManager.Get("servertag." + playStyle), textAlignment: Alignment.Right); @@ -385,6 +424,15 @@ namespace Barotrauma.Networking } } } + if (ContentPackageCount > ContentPackages.Length) + { + new GUITextBlock( + new RectTransform(new Vector2(1.0f, 0.15f), contentPackageList.Content.RectTransform) { MinSize = new Point(0, 15) }, + TextManager.GetWithVariable("workshopitemdownloadprompttruncated", "[number]", (ContentPackageCount - ContentPackages.Length).ToString())) + { + CanBeFocused = false + }; + } } // ----------------------------------------------------------------------------- @@ -423,14 +471,16 @@ namespace Barotrauma.Networking AllowSpectating = getBool("allowspectating"); AllowRespawn = getBool("allowrespawn"); VoipEnabled = getBool("voicechatenabled"); - GameMode = valueGetter("gamemode")?.ToIdentifier() ?? Identifier.Empty; if (float.TryParse(valueGetter("traitors"), NumberStyles.Any, CultureInfo.InvariantCulture, out float traitorProbability)) { TraitorProbability = traitorProbability; } if (Enum.TryParse(valueGetter("playstyle"), out PlayStyle playStyle)) { PlayStyle = playStyle; } Language = valueGetter("language")?.ToLanguageIdentifier() ?? LanguageIdentifier.None; + SelectedSub = valueGetter("submarine") ?? string.Empty; ContentPackages = ExtractContentPackageInfo(ServerName, valueGetter).ToImmutableArray(); - + ContentPackageCount = ContentPackages.Length; + if (int.TryParse(valueGetter("packagecount"), out int packageCount)) { ContentPackageCount = packageCount; } + bool getBool(string key) { string? data = valueGetter(key); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index 82f553984..fa077332e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -936,6 +936,10 @@ namespace Barotrauma.Networking TextManager.Get("ServerSettingsAllowVoteKick")); GetPropertyData(nameof(AllowVoteKick)).AssignGUIComponent(voteKickBox); + var allowImmediateItemDeliveryBox = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), + TextManager.Get("ServerSettingsImmediateItemDelivery")); + GetPropertyData(nameof(AllowImmediateItemDelivery)).AssignGUIComponent(allowImmediateItemDeliveryBox); + GUITextBlock.AutoScaleAndNormalize(tickBoxContainer.Content.Children.Select(c => ((GUITickBox)c).TextBlock)); tickBoxContainer.RectTransform.MinSize = new Point(0, (int)(tickBoxContainer.Content.Children.First().Rect.Height * 2.0f + tickBoxContainer.Padding.Y + tickBoxContainer.Padding.W)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs index 4bd6e7605..2eac72fa2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +#nullable enable +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Xml.Linq; @@ -148,7 +149,7 @@ namespace Barotrauma.Particles Prefab = prefab; } - public void Emit(float deltaTime, Vector2 position, Hull hullGuess = null, float angle = 0.0f, float particleRotation = 0.0f, float velocityMultiplier = 1.0f, float sizeMultiplier = 1.0f, float amountMultiplier = 1.0f, Color? colorMultiplier = null, ParticlePrefab overrideParticle = null, bool mirrorAngle = false, Tuple tracerPoints = null) + public void Emit(float deltaTime, Vector2 position, Hull? hullGuess = null, float angle = 0.0f, float particleRotation = 0.0f, float velocityMultiplier = 1.0f, float sizeMultiplier = 1.0f, float amountMultiplier = 1.0f, Color? colorMultiplier = null, ParticlePrefab? overrideParticle = null, bool mirrorAngle = false, Tuple? tracerPoints = null) { if (GameMain.Client?.MidRoundSyncing ?? false) { return; } @@ -191,16 +192,17 @@ namespace Barotrauma.Particles burstEmitTimer = Prefab.Properties.EmitInterval; for (int i = 0; i < Prefab.Properties.ParticleAmount * amountMultiplier; i++) { - Emit(position, hullGuess, angle, particleRotation, velocityMultiplier, sizeMultiplier, colorMultiplier, overrideParticle, tracerPoints: tracerPoints); + Emit(position, hullGuess, angle, particleRotation, velocityMultiplier, sizeMultiplier, colorMultiplier, overrideParticle, mirrorAngle, tracerPoints: tracerPoints); } } - private void Emit(Vector2 position, Hull hullGuess, float angle, float particleRotation, float velocityMultiplier, float sizeMultiplier, Color? colorMultiplier = null, ParticlePrefab overrideParticle = null, bool mirrorAngle = false, Tuple tracerPoints = null) + private void Emit(Vector2 position, Hull? hullGuess, float angle, float particleRotation, float velocityMultiplier, float sizeMultiplier, Color? colorMultiplier = null, ParticlePrefab? overrideParticle = null, bool mirrorAngle = false, Tuple? tracerPoints = null) { var particlePrefab = overrideParticle ?? Prefab.ParticlePrefab; if (particlePrefab == null) { - DebugConsole.AddWarning($"Could not find the particle prefab \"{Prefab.ParticlePrefabName}\"."); + DebugConsole.AddWarning($"Could not find the particle prefab \"{Prefab.ParticlePrefabName}\".", + contentPackage: Prefab.ContentPackage); return; } @@ -271,7 +273,7 @@ namespace Barotrauma.Particles { public readonly Identifier ParticlePrefabName; - public ParticlePrefab ParticlePrefab + public ParticlePrefab? ParticlePrefab { get { @@ -282,12 +284,16 @@ namespace Barotrauma.Particles public readonly ParticleEmitterProperties Properties; - public bool DrawOnTop => Properties.DrawOnTop || ParticlePrefab.DrawOnTop; + public readonly ContentPackage? ContentPackage; + + public bool DrawOnTop => Properties.DrawOnTop || ParticlePrefab is { DrawOnTop: true }; public ParticleEmitterPrefab(ContentXElement element) { - Properties = new ParticleEmitterProperties(element); + if (element == null) { throw new ArgumentNullException(nameof(element)); } + Properties = new ParticleEmitterProperties(element!); ParticlePrefabName = element.GetAttributeIdentifier("particle", ""); + ContentPackage = element.ContentPackage; } public ParticleEmitterPrefab(ParticlePrefab prefab, ParticleEmitterProperties properties) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs index c550fbfee..050ee9ded 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs @@ -244,7 +244,8 @@ namespace Barotrauma.Particles if (Sprites.Count == 0) { - DebugConsole.ThrowError($"Particle prefab \"{Name}\" in the file \"{file}\" has no sprites defined!"); + DebugConsole.ThrowError($"Particle prefab \"{Name}\" in the file \"{file}\" has no sprites defined!", + contentPackage: element.ContentPackage); } //if velocity change in water is not given, it defaults to the normal velocity change diff --git a/Barotrauma/BarotraumaClient/ClientSource/Program.cs b/Barotrauma/BarotraumaClient/ClientSource/Program.cs index 4dd29adfa..b541794b2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Program.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Program.cs @@ -66,15 +66,22 @@ namespace Barotrauma private static void CrashHandler(object sender, UnhandledExceptionEventArgs args) { + Exception unhandledException = args.ExceptionObject as Exception; try { Game?.Exit(); - CrashDump(Game, "crashreport.log", (Exception)args.ExceptionObject); + CrashDump(Game, "crashreport.log", unhandledException); Game?.Dispose(); } - catch (Exception e) + catch (Exception exceptionHandlerError) { - Debug.WriteLine(e.Message); + Debug.WriteLine(exceptionHandlerError.Message); + string slimCrashReport = "Exception handler failed: " + exceptionHandlerError.Message + "\n" + exceptionHandlerError.StackTrace; + if (unhandledException != null) + { + slimCrashReport += "\n\nInitial exception: " + unhandledException.Message + "\n" + unhandledException.StackTrace; + } + File.WriteAllText("crashreportslim.log", slimCrashReport); //exception handler is broken, we have a serious problem here!! return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index 1cefb2a67..67be291c8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -40,7 +40,8 @@ namespace Barotrauma public CampaignMode Campaign { get; } public CrewManagement CrewManagement { get; set; } - private Store Store { get; set; } + + public Store Store { get; private set; } public UpgradeStore UpgradeStore { get; set; } @@ -254,7 +255,7 @@ namespace Barotrauma RelativeSpacing = 0.02f, }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), location.Name, font: GUIStyle.LargeFont) + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), location.DisplayName, font: GUIStyle.LargeFont) { AutoScaleHorizontal = true }; @@ -598,9 +599,10 @@ namespace Barotrauma break; case CampaignMode.InteractionType.Crew: CrewManagement.UpdateCrew(); + CrewManagement.UpdateHireables(); break; case CampaignMode.InteractionType.PurchaseSub: - if (submarineSelection == null) submarineSelection = new SubmarineSelection(false, () => Campaign.ShowCampaignUI = false, tabs[(int)CampaignMode.InteractionType.PurchaseSub].RectTransform); + submarineSelection ??= new SubmarineSelection(false, () => Campaign.ShowCampaignUI = false, tabs[(int)CampaignMode.InteractionType.PurchaseSub].RectTransform); submarineSelection.RefreshSubmarineDisplay(true, setTransferOptionToTrue: true); break; case CampaignMode.InteractionType.Map: diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index 917bf10f6..bc85fcb99 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -258,8 +258,8 @@ namespace Barotrauma graphics.BlendState = BlendState.NonPremultiplied; graphics.SamplerStates[0] = SamplerState.LinearWrap; - Quad.UseBasicEffect(renderTargetBackground); - Quad.Render(); + GraphicsQuad.UseBasicEffect(renderTargetBackground); + GraphicsQuad.Render(); //Draw the rest of the structures, characters and front structures spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, null, DepthStencilState.None, null, null, cam.Transform); @@ -312,8 +312,8 @@ namespace Barotrauma graphics.BlendState = BlendState.Opaque; graphics.SamplerStates[0] = SamplerState.LinearWrap; - Quad.UseBasicEffect(renderTarget); - Quad.Render(); + GraphicsQuad.UseBasicEffect(renderTarget); + GraphicsQuad.Render(); //draw alpha blended particles that are inside a sub spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, null, DepthStencilState.DepthRead, null, null, cam.Transform); @@ -379,8 +379,8 @@ namespace Barotrauma graphics.DepthStencilState = DepthStencilState.None; graphics.SamplerStates[0] = SamplerState.LinearWrap; graphics.BlendState = CustomBlendStates.Multiplicative; - Quad.UseBasicEffect(GameMain.LightManager.LightMap); - Quad.Render(); + GraphicsQuad.UseBasicEffect(GameMain.LightManager.LightMap); + GraphicsQuad.Render(); } spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.LinearWrap, DepthStencilState.None, null, null, cam.Transform); @@ -389,6 +389,8 @@ namespace Barotrauma c.DrawFront(spriteBatch, cam); } + GameMain.LightManager.DebugDrawVertices(spriteBatch); + Level.Loaded?.DrawDebugOverlay(spriteBatch, cam); if (GameMain.DebugDraw) { @@ -437,7 +439,7 @@ namespace Barotrauma graphics.SamplerStates[0] = SamplerState.PointClamp; graphics.SamplerStates[1] = SamplerState.PointClamp; GameMain.LightManager.LosEffect.CurrentTechnique.Passes[0].Apply(); - Quad.Render(); + GraphicsQuad.Render(); graphics.SamplerStates[0] = SamplerState.LinearWrap; graphics.SamplerStates[1] = SamplerState.LinearWrap; } @@ -505,7 +507,7 @@ namespace Barotrauma graphics.DepthStencilState = DepthStencilState.None; if (string.IsNullOrEmpty(postProcessTechnique)) { - Quad.UseBasicEffect(renderTargetFinal); + GraphicsQuad.UseBasicEffect(renderTargetFinal); } else { @@ -514,7 +516,7 @@ namespace Barotrauma PostProcessEffect.CurrentTechnique = PostProcessEffect.Techniques[postProcessTechnique]; PostProcessEffect.CurrentTechnique.Passes[0].Apply(); } - Quad.Render(); + GraphicsQuad.Render(); if (fadeToBlackState > 0.0f) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index 9d122c629..e0a8da297 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -221,7 +221,17 @@ namespace Barotrauma currentLevelData.AllowInvalidOutpost = allowInvalidOutpost.Selected; var dummyLocations = GameSession.CreateDummyLocations(currentLevelData); Level.Generate(currentLevelData, mirror: mirrorLevel.Selected, startLocation: dummyLocations[0], endLocation: dummyLocations[1]); - Submarine.MainSub?.SetPosition(Level.Loaded.StartPosition); + + if (Submarine.MainSub != null) + { + Vector2 startPos = Level.Loaded.StartPosition; + if (Level.Loaded.StartOutpost != null) + { + startPos.Y -= Level.Loaded.StartOutpost.Borders.Height / 2 + Submarine.MainSub.Borders.Height / 2; + } + Submarine.MainSub?.SetPosition(startPos); + } + GameMain.LightManager.AddLight(pointerLightSource); if (!wasLevelLoaded || Cam.Position.X < 0 || Cam.Position.Y < 0 || Cam.Position.Y > Level.Loaded.Size.X || Cam.Position.Y > Level.Loaded.Size.Y) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index bd977496b..0982130cf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -76,6 +76,8 @@ namespace Barotrauma private GUITextBlock tutorialHeader, tutorialDescription; private GUIListBox tutorialList; + private GUIComponent versionMismatchWarning; + #region Creation public MainMenuScreen(GameMain game) { @@ -105,6 +107,28 @@ namespace Barotrauma } }; + versionMismatchWarning = new GUIFrame(new RectTransform(new Vector2(0.7f, 0.065f), Frame.RectTransform) { AbsoluteOffset = new Point(GUI.IntScale(15)) }, style: "InnerFrame", color: GUIStyle.Red) + { + IgnoreLayoutGroups = true, + Visible = false + }; + var versionMismatchContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), versionMismatchWarning.RectTransform, Anchor.Center), isHorizontal: true) + { + RelativeSpacing = 0.05f, + }; + new GUIImage(new RectTransform(new Vector2(1.0f), versionMismatchContent.RectTransform, scaleBasis: ScaleBasis.Smallest), style: "GUINotificationButton") + { + Color = GUIStyle.Orange + }; + new GUITextBlock(new RectTransform(new Vector2(0.85f, 1.0f), versionMismatchContent.RectTransform), + TextManager.GetWithVariables("versionmismatchwarning", + ("[gameversion]", GameMain.Version.ToString()), + ("[contentversion]", ContentPackageManager.VanillaCorePackage.GameVersion.ToString())), + wrap: true) + { + TextColor = GUIStyle.Orange + }; + new GUIImage(new RectTransform(new Vector2(0.4f, 0.25f), Frame.RectTransform, Anchor.BottomRight) { RelativeOffset = new Vector2(0.08f, 0.05f), AbsoluteOffset = new Point(-8, -8) }, style: "TitleText") @@ -141,6 +165,7 @@ namespace Barotrauma } } #else + SpamServerFilters.RequestGlobalSpamFilter(); FetchRemoteContent(); #endif @@ -587,7 +612,9 @@ namespace Barotrauma GameMain.SubEditorScreen?.ClearBackedUpSubInfo(); Submarine.Unload(); - + + versionMismatchWarning.Visible = GameMain.Version < ContentPackageManager.VanillaCorePackage.GameVersion; + ResetButtonStates(null); } @@ -663,7 +690,18 @@ namespace Barotrauma .ToArray(); foreach (var newServerExe in newServerExes) { - serverExecutableDropdown.AddItem($"{newServerExe.ContentPackage.Name} - {Path.GetFileNameWithoutExtension(newServerExe.Path.Value)}", userData: newServerExe); + var serverExeEntry = serverExecutableDropdown.AddItem($"{newServerExe.ContentPackage.Name} - {Path.GetFileNameWithoutExtension(newServerExe.Path.Value)}", userData: newServerExe); + if (newServerExe.ContentPackage.GameVersion < GameMain.VanillaContent.GameVersion) + { + serverExeEntry.ToolTip = + TextManager.GetWithVariables("versionmismatchwarning", + ("[gameversion]", newServerExe.ContentPackage.GameVersion.ToString()), + ("[contentversion]", GameMain.VanillaContent.GameVersion.ToString())); + if (serverExeEntry is GUITextBlock serverExeText) + { + serverExeText.TextColor = GUIStyle.Red; + } + } } serverExecutableDropdown.ListBox.Content.Children.ForEach(c => { @@ -1472,34 +1510,58 @@ namespace Barotrauma { OnClicked = (btn, userdata) => { - string name = serverNameBox.Text; - if (string.IsNullOrEmpty(name)) - { - serverNameBox.Flash(); - return false; - } - - if (isPublicBox.Selected && ForbiddenWordFilter.IsForbidden(name, out string forbiddenWord)) - { - var msgBox = new GUIMessageBox("", - TextManager.GetWithVariables("forbiddenservernameverification", ("[forbiddenword]", forbiddenWord), ("[servername]", name)), - new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); - msgBox.Buttons[0].OnClicked += (_, __) => - { - TryStartServer(); - msgBox.Close(); - return true; - }; - msgBox.Buttons[1].OnClicked += msgBox.Close; - } - else - { - TryStartServer(); - } - + CheckServerName(); return true; } }; + + void CheckServerName() + { + string name = serverNameBox.Text; + if (string.IsNullOrEmpty(name)) + { + serverNameBox.Flash(); + return; + } + if (isPublicBox.Selected && ForbiddenWordFilter.IsForbidden(name, out string forbiddenWord)) + { + var msgBox = new GUIMessageBox("", + TextManager.GetWithVariables("forbiddenservernameverification", ("[forbiddenword]", forbiddenWord), ("[servername]", name)), + new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); + msgBox.Buttons[0].OnClicked += (_, __) => + { + CheckServerExe(); + msgBox.Close(); + return true; + }; + msgBox.Buttons[1].OnClicked += msgBox.Close; + return; + } + CheckServerExe(); + } + + void CheckServerExe() + { + if (serverExecutableDropdown?.SelectedData is ServerExecutableFile serverExe && + serverExe.ContentPackage.GameVersion < GameMain.VanillaContent.GameVersion) + { + var msgBox = new GUIMessageBox(string.Empty, + TextManager.GetWithVariables("versionmismatchwarning", + ("[gameversion]", serverExe.ContentPackage.GameVersion.ToString()), + ("[contentversion]", GameMain.VanillaContent.GameVersion.ToString())) + "\n\n"+ + TextManager.GetWithVariable("versionmismatch.verifylaunch", "[exename]", serverExe.ContentPackage.Name), + new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); + msgBox.Buttons[0].OnClicked += (_, __) => + { + TryStartServer(); + msgBox.Close(); + return true; + }; + msgBox.Buttons[1].OnClicked += msgBox.Close; + return; + } + TryStartServer(); + } } private void SetServerPlayStyle(PlayStyle playStyle) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index f004779d1..f35e8c9d7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -1520,6 +1520,7 @@ namespace Barotrauma }; bool nameChangePending = isGameRunning && GameMain.Client.PendingName != string.Empty && GameMain.Client?.Character?.Name != GameMain.Client.PendingName; + changesPendingText?.Parent?.RemoveChild(changesPendingText); changesPendingText = null; if (TabMenu.PendingChanges) @@ -2389,10 +2390,20 @@ namespace Barotrauma options.Add(kickOption); } - options.Add(new ContextMenuOption("Ban", isEnabled: canBan, onSelected: delegate + if (GameMain.Client?.ServerSettings?.BanList?.BannedPlayers?.Any(bp => bp.MatchesClient(client)) ?? false) { - GameMain.Client?.CreateKickReasonPrompt(client.Name, true); - })); + options.Add(new ContextMenuOption("clientpermission.unban", isEnabled: canBan, onSelected: delegate + { + GameMain.Client?.UnbanPlayer(client.Name); + })); + } + else + { + options.Add(new ContextMenuOption("Ban", isEnabled: canBan, onSelected: delegate + { + GameMain.Client?.CreateKickReasonPrompt(client.Name, true); + })); + } GUIContextMenu.CreateContextMenu(null, client.Name, headerColor: clientColor, options.ToArray()); } @@ -2591,11 +2602,11 @@ namespace Barotrauma foreach (DebugConsole.Command command in DebugConsole.Commands) { var commandTickBox = new GUITickBox(new RectTransform(new Vector2(0.15f, 0.15f), commandList.Content.RectTransform), - command.names[0], font: GUIStyle.SmallFont) + command.Names[0].Value, font: GUIStyle.SmallFont) { Selected = selectedClient.PermittedConsoleCommands.Contains(command), Enabled = !myClient, - ToolTip = command.help, + ToolTip = command.Help, UserData = command }; commandTickBox.OnSelected += (GUITickBox tickBox) => @@ -2630,12 +2641,25 @@ namespace Barotrauma { if (GameMain.Client.HasPermission(ClientPermissions.Ban)) { - var banButton = new GUIButton(new RectTransform(new Vector2(0.34f, 1.0f), buttonAreaTop.RectTransform), - TextManager.Get("Ban")) + GUIButton banButton; + if (GameMain.Client?.ServerSettings?.BanList?.BannedPlayers?.Any(bp => bp.MatchesClient(selectedClient)) ?? false) { - UserData = selectedClient - }; - banButton.OnClicked = (bt, userdata) => { BanPlayer(selectedClient); return true; }; + banButton = new GUIButton(new RectTransform(new Vector2(0.34f, 1.0f), buttonAreaTop.RectTransform), + TextManager.Get("clientpermission.unban")) + { + UserData = selectedClient + }; + banButton.OnClicked = (bt, userdata) => { GameMain.Client?.UnbanPlayer(selectedClient.Name); return true; }; + } + else + { + banButton = new GUIButton(new RectTransform(new Vector2(0.34f, 1.0f), buttonAreaTop.RectTransform), + TextManager.Get("Ban")) + { + UserData = selectedClient + }; + banButton.OnClicked = (bt, userdata) => { BanPlayer(selectedClient); return true; }; + } banButton.OnClicked += ClosePlayerFrame; } @@ -3147,12 +3171,12 @@ namespace Barotrauma GUIButton jobButton = null; var availableJobs = JobPrefab.Prefabs.Where(jobPrefab => - jobPrefab.MaxNumber > 0 && JobList.Content.Children.All(c => !(c.UserData is JobVariant prefab) || prefab.Prefab != jobPrefab) + !jobPrefab.HiddenJob && jobPrefab.MaxNumber > 0 && JobList.Content.Children.All(c => c.UserData is not JobVariant prefab || prefab.Prefab != jobPrefab) ).Select(j => new JobVariant(j, 0)); availableJobs = availableJobs.Concat( JobPrefab.Prefabs.Where(jobPrefab => - jobPrefab.MaxNumber > 0 && JobList.Content.Children.Any(c => (c.UserData is JobVariant prefab) && prefab.Prefab == jobPrefab) + !jobPrefab.HiddenJob && jobPrefab.MaxNumber > 0 && JobList.Content.Children.Any(c => (c.UserData is JobVariant prefab) && prefab.Prefab == jobPrefab) ).Select(j => (JobVariant)JobList.Content.FindChild(c => (c.UserData is JobVariant prefab) && prefab.Prefab == j).UserData)); availableJobs = availableJobs.ToList(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs index d9a3de1c8..d48c4bc39 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs @@ -655,7 +655,8 @@ namespace Barotrauma ScrollBarVisible = true, OnSelected = (btn, obj) => { - if (!(obj is ServerInfo serverInfo)) { return false; } + if (GUI.MouseOn is GUIButton) { return false; } + if (obj is not ServerInfo serverInfo) { return false; } joinButton.Enabled = true; selectedServer = Option.Some(serverInfo); @@ -852,6 +853,13 @@ namespace Barotrauma }); } + public void HideServerPreview() + { + serverPreviewContainer.Visible = false; + panelAnimator.RightEnabled = false; + panelAnimator.RightVisible = false; + } + private void InsertServer(ServerInfo serverInfo, GUIComponent component) { var children = serverList.Content.RectTransform.Children.Reverse().ToList(); @@ -973,7 +981,7 @@ namespace Barotrauma } } - private void FilterServers() + public void FilterServers() { RemoveMsgFromServerList(MsgUserData.NoMatchingServers); foreach (GUIComponent child in serverList.Content.Children) @@ -1013,6 +1021,7 @@ namespace Barotrauma return false; } #endif + if (SpamServerFilters.IsFiltered(serverInfo)) { return false; } if (!string.IsNullOrEmpty(searchBox.Text) && !serverInfo.ServerName.Contains(searchBox.Text, StringComparison.OrdinalIgnoreCase)) { return false; } @@ -1553,15 +1562,169 @@ namespace Barotrauma var serverFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.06f), serverList.Content.RectTransform) { MinSize = new Point(0, 35) }, style: "ListBoxElement") { - UserData = serverInfo + UserData = serverInfo, }; + + serverFrame.OnSecondaryClicked += (_, data) => + { + if (data is not ServerInfo info) { return false; } + CreateContextMenu(info); + return true; + }; + new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), serverFrame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = false }; UpdateServerInfoUI(serverInfo); if (!skipPing) { PingUtils.GetServerPing(serverInfo, UpdateServerInfoUI); } + } + private static readonly Vector2 confirmPopupSize = new Vector2(0.2f, 0.2625f); + private static readonly Point confirmPopupMinSize = new Point(300, 300); + + private void CreateContextMenu(ServerInfo info) + { + var favoriteOption = new ContextMenuOption(IsFavorite(info) ? "removefromfavorites" : "addtofavorites", isEnabled: true, () => + { + if (IsFavorite(info)) + { + RemoveFromFavoriteServers(info); + } + else + { + AddToFavoriteServers(info); + } + FilterServers(); + }); + var reportOption = new ContextMenuOption("reportserver", isEnabled: true, () => { CreateReportPrompt(info); }); + var filterOption = new ContextMenuOption("filterserver", isEnabled: true, () => + { + CreateFilterServerPrompt(info); + }) + { + Tooltip = TextManager.Get("filterservertooltip") + }; + + GUIContextMenu.CreateContextMenu(favoriteOption, filterOption, reportOption); + } + + public static void CreateFilterServerPrompt(ServerInfo info) + { + GUI.AskForConfirmation( + header: TextManager.Get("filterserver"), + body: TextManager.GetWithVariables("filterserverconfirm", ("[server]", info.ServerName), ("[filepath]", SpamServerFilter.SavePath)), + onConfirm: () => + { + SpamServerFilters.AddServerToLocalSpamList(info); + + if (GameMain.ServerListScreen is not { } serverListScreen) { return; } + + if (serverListScreen.selectedServer.TryUnwrap(out var selectedServer) && selectedServer.Equals(info)) + { + serverListScreen.HideServerPreview(); + } + serverListScreen.FilterServers(); + }, relativeSize: confirmPopupSize, minSize: confirmPopupMinSize); + } + + private enum ReportReason + { + Spam, + Advertising, + Inappropriate + } + + public static void CreateReportPrompt(ServerInfo info) + { + if (!GameAnalyticsManager.SendUserStatistics) + { + GUI.NotifyPrompt(TextManager.Get("reportserver"), TextManager.Get("reportserverdisabled")); + return; + } + + var msgBox = new GUIMessageBox( + headerText: TextManager.Get("reportserver"), + text: string.Empty, + relativeSize: new Vector2(0.2f, 0.4f), + minSize: new Point(380, 430), + buttons: Array.Empty()); + + var layout = new GUILayoutGroup(new RectTransform(Vector2.One, msgBox.Content.RectTransform, Anchor.Center)); + + new GUITextBlock(new RectTransform(new Vector2(1f, 0.3f), layout.RectTransform), TextManager.GetWithVariable("reportserverexplanation", "[server]", info.ServerName), wrap: true) + { + ToolTip = TextManager.Get("reportserverprompttooltip") + }; + + var listBox = new GUIListBox(new RectTransform(new Vector2(1f, 0.3f), layout.RectTransform)); + + var enums = Enum.GetValues(); + foreach (ReportReason reason in enums) + { + new GUITickBox(new RectTransform(new Vector2(1f, 1f / enums.Length), listBox.Content.RectTransform), TextManager.Get($"reportreason.{reason}")) + { + UserData = reason + }; + } + + // padding + new GUIFrame(new RectTransform(new Vector2(1f, 0.05f), layout.RectTransform), style: null); + + var buttonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.3f), layout.RectTransform)) + { + Stretch = true + }; + + var reportAndHideButton = new GUIButton(new RectTransform(new Vector2(1f, 0.333f), buttonLayout.RectTransform), TextManager.Get("reportoption.reportandhide")) + { + Enabled = false, + OnClicked = (_, _) => + { + CreateFilterServerPrompt(info); + msgBox.Close(); + return true; + } + }; + var reportButton = new GUIButton(new RectTransform(new Vector2(1f, 0.333f), buttonLayout.RectTransform), TextManager.Get("reportoption.report")) + { + Enabled = false, + OnClicked = (_, _) => + { + ReportServer(info, GetUserSelectedReasons()); + msgBox.Close(); + return true; + } + }; + + new GUIButton(new RectTransform(new Vector2(1f, 0.333f), buttonLayout.RectTransform), TextManager.Get("cancel")) + { + OnClicked = (_, _) => + { + msgBox.Close(); + return true; + } + }; + + foreach (var child in listBox.Content.GetAllChildren()) + { + child.OnSelected += _ => + { + reportAndHideButton.Enabled = reportButton.Enabled = GetUserSelectedReasons().Any(); + return true; + }; + } + + IEnumerable GetUserSelectedReasons() + => listBox.Content.Children + .Where(static c => c.UserData is ReportReason && c.Selected) + .Select(static c => (ReportReason)c.UserData).ToArray(); + } + + private static void ReportServer(ServerInfo info, IEnumerable reasons) + { + if (!reasons.Any()) { return; } + GameAnalyticsManager.AddErrorEvent(GameAnalyticsManager.ErrorSeverity.Info, $"[Spam] Reported server: Name: \"{info.ServerName}\", Message: \"{info.ServerMessage}\", Endpoint: \"{info.Endpoint.StringRepresentation}\". Reason: \"{string.Join(", ", reasons)}\"."); } private void UpdateServerInfoUI(ServerInfo serverInfo) @@ -1571,7 +1734,6 @@ namespace Barotrauma serverFrame.UserData = serverInfo; - serverFrame.ToolTip = ""; var serverContent = serverFrame.Children.First() as GUILayoutGroup; serverContent.ClearChildren(); @@ -1583,15 +1745,14 @@ namespace Barotrauma new RectTransform(new Vector2(columns[label].RelativeWidth, 1.0f), serverContent.RectTransform), style: null); } - - void errorTooltip(RichString toolTip) + + void disableElementFocus() { sections.Values.ForEach(c => { c.CanBeFocused = false; c.Children.First().CanBeFocused = false; }); - serverFrame.ToolTip = toolTip; } RectTransform columnRT(ColumnLabel label, float scale = 0.95f) @@ -1611,7 +1772,7 @@ namespace Barotrauma NetworkMember.IsCompatible(GameMain.Version, serverInfo.GameVersion), UserData = "compatible" }; - + var passwordBox = new GUITickBox(columnRT(ColumnLabel.ServerListHasPassword, scale: 0.6f), label: "", style: "GUIServerListPasswordTickBox") { Selected = serverInfo.HasPassword, @@ -1664,9 +1825,10 @@ namespace Barotrauma serverPingText.TextColor = Color.DarkRed; } + LocalizedString toolTip = ""; if (!serverInfo.Checked) { - errorTooltip(TextManager.Get("ServerOffline")); + toolTip = TextManager.Get("ServerOffline"); serverName.TextColor *= 0.8f; serverPlayers.TextColor *= 0.8f; } @@ -1681,7 +1843,6 @@ namespace Barotrauma } else if (!compatibleBox.Selected) { - LocalizedString toolTip = ""; if (serverInfo.GameVersion != GameMain.Version) { toolTip = TextManager.GetWithVariable("ServerListIncompatibleVersion", "[version]", serverInfo.GameVersion.ToString()); @@ -1707,14 +1868,12 @@ namespace Barotrauma toolTip += '\n' + TextManager.GetWithVariable("workshopitemdownloadprompttruncated", "[number]", (incompatibleModNames.Count - maxIncompatibleToList).ToString()); } } - errorTooltip(toolTip); serverName.TextColor *= 0.5f; serverPlayers.TextColor *= 0.5f; } else { - LocalizedString toolTip = ""; foreach (var contentPackage in serverInfo.ContentPackages) { if (ContentPackageManager.EnabledPackages.All.None(cp => cp.Hash.StringRepresentation == contentPackage.Hash)) @@ -1724,8 +1883,11 @@ namespace Barotrauma break; } } - errorTooltip(toolTip); } + disableElementFocus(); + + string separator = toolTip.IsNullOrWhiteSpace() ? "" : "\n\n"; + serverFrame.ToolTip = RichString.Rich(toolTip + separator + $"‖color:gui.blue‖{TextManager.GetWithVariable("serverlisttooltip", "[button]", PlayerInput.SecondaryMouseLabel)}‖end‖"); foreach (var section in sections.Values) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 1fd3ca751..a9a34256c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -16,9 +16,6 @@ namespace Barotrauma { class SubEditorScreen : EditorScreen { - public const string CircuitBoxDeletionWarningHeader = "Selection contains circuit boxes", - CircuitBoxDeletionWarningBody = "Are you sure you want to delete the selection? Any wiring inside circuit boxes will be lost and cannot be recovered."; - public const int MaxStructures = 2000; public const int MaxWalls = 500; public const int MaxItems = 5000; @@ -1289,7 +1286,8 @@ namespace Barotrauma if (legacy) { textBlock.TextColor *= 0.6f; } if (name.IsNullOrEmpty()) { - DebugConsole.AddWarning($"Entity \"{ep.Identifier.Value}\" has no name!"); + DebugConsole.AddWarning($"Entity \"{ep.Identifier.Value}\" has no name!", + contentPackage: ep.ContentPackage); textBlock.Text = frame.ToolTip = ep.Identifier.Value; textBlock.TextColor = GUIStyle.Red; } @@ -1559,8 +1557,17 @@ namespace Barotrauma if (editorSelectedTime.TryUnwrap(out DateTime selectedTime)) { TimeSpan timeInEditor = DateTime.Now - selectedTime; - SteamAchievementManager.IncrementStat("hoursineditor".ToIdentifier(), (float)timeInEditor.TotalHours); - editorSelectedTime = Option.None(); + if (timeInEditor.TotalSeconds > Timing.TotalTime) + { + DebugConsole.ThrowErrorAndLogToGA( + "SubEditorScreen.DeselectEditorSpecific:InvalidTimeInEditor", + $"Error in sub editor screen. Calculated time in editor {timeInEditor} was larger than the time the game has run ({Timing.TotalTime} s)."); + } + else + { + SteamAchievementManager.IncrementStat("hoursineditor".ToIdentifier(), (float)timeInEditor.TotalHours); + editorSelectedTime = Option.None(); + } } #endif @@ -2365,49 +2372,58 @@ namespace Barotrauma //--------------------------------------- - var beaconSettingsContainer = new GUILayoutGroup(new RectTransform(Vector2.One, subTypeDependentSettingFrame.RectTransform)) + var extraSettingsContainer = new GUILayoutGroup(new RectTransform(new Vector2(1, 0.5f), subTypeDependentSettingFrame.RectTransform)) { CanBeFocused = true, Visible = false, Stretch = true }; - // ------------------- - - var beaconMinDifficultyGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), beaconSettingsContainer.RectTransform), isHorizontal: true) + var minDifficultyGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), extraSettingsContainer.RectTransform), isHorizontal: true) { Stretch = true }; - new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), beaconMinDifficultyGroup.RectTransform), + new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), minDifficultyGroup.RectTransform), TextManager.Get("minleveldifficulty"), textAlignment: Alignment.CenterLeft, wrap: true); - var numInput = new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), beaconMinDifficultyGroup.RectTransform), NumberType.Int) + var numInput = new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), minDifficultyGroup.RectTransform), NumberType.Int) { - IntValue = (int)(MainSub?.Info?.BeaconStationInfo?.MinLevelDifficulty ?? 0), + IntValue = (int)(MainSub?.Info?.GetExtraSubmarineInfo?.MinLevelDifficulty ?? 0), MinValueInt = 0, MaxValueInt = 100, OnValueChanged = (numberInput) => { - MainSub.Info.BeaconStationInfo.MinLevelDifficulty = numberInput.IntValue; + MainSub.Info.GetExtraSubmarineInfo.MinLevelDifficulty = numberInput.IntValue; } }; - beaconMinDifficultyGroup.RectTransform.MaxSize = numInput.TextBox.RectTransform.MaxSize; - var beaconMaxDifficultyGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), beaconSettingsContainer.RectTransform), isHorizontal: true) + minDifficultyGroup.RectTransform.MaxSize = numInput.TextBox.RectTransform.MaxSize; + var maxDifficultyGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), extraSettingsContainer.RectTransform), isHorizontal: true) { Stretch = true }; - new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), beaconMaxDifficultyGroup.RectTransform), + new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), maxDifficultyGroup.RectTransform), TextManager.Get("maxleveldifficulty"), textAlignment: Alignment.CenterLeft, wrap: true); - numInput = new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), beaconMaxDifficultyGroup.RectTransform), NumberType.Int) + numInput = new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), maxDifficultyGroup.RectTransform), NumberType.Int) { - IntValue = (int)(MainSub?.Info?.BeaconStationInfo?.MaxLevelDifficulty ?? 100), + IntValue = (int)(MainSub?.Info?.GetExtraSubmarineInfo?.MaxLevelDifficulty ?? 100), MinValueInt = 0, MaxValueInt = 100, OnValueChanged = (numberInput) => { - MainSub.Info.BeaconStationInfo.MaxLevelDifficulty = numberInput.IntValue; + MainSub.Info.GetExtraSubmarineInfo.MaxLevelDifficulty = numberInput.IntValue; } }; - beaconMaxDifficultyGroup.RectTransform.MaxSize = numInput.TextBox.RectTransform.MaxSize; + maxDifficultyGroup.RectTransform.MaxSize = numInput.TextBox.RectTransform.MaxSize; + + + //--------------------------------------- + + var beaconSettingsContainer = new GUILayoutGroup(new RectTransform(Vector2.One, extraSettingsContainer.RectTransform)) + { + CanBeFocused = true, + Visible = false, + Stretch = true + }; + new GUITickBox(new RectTransform(new Vector2(1.0f, 0.25f), beaconSettingsContainer.RectTransform), TextManager.Get("allowdamagedwalls")) { Selected = MainSub?.Info?.BeaconStationInfo?.AllowDamagedWalls ?? true, @@ -2669,8 +2685,13 @@ namespace Barotrauma { MainSub.Info.BeaconStationInfo ??= new BeaconStationInfo(MainSub.Info); } + else if (type == SubmarineType.Wreck) + { + MainSub.Info.WreckInfo ??= new WreckInfo(MainSub.Info); + } previewImageButtonHolder.Children.ForEach(c => c.Enabled = MainSub.Info.AllowPreviewImage); outpostSettingsContainer.Visible = type == SubmarineType.OutpostModule; + extraSettingsContainer.Visible = type == SubmarineType.BeaconStation || type == SubmarineType.Wreck; beaconSettingsContainer.Visible = type == SubmarineType.BeaconStation; subSettingsContainer.Visible = type == SubmarineType.Player; return true; @@ -3918,28 +3939,15 @@ namespace Barotrauma 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))), - new ContextMenuOption("delete", isEnabled: hasTargets, onSelected: () => RemoveEntitiesWithPossibleWarning(targets)), - new ContextMenuOption(TextManager.Get("editortip.shiftforextraoptions") + '\n' + TextManager.Get("editortip.altforruler"), isEnabled: false, onSelected: null)); - } - } - - public static void RemoveEntitiesWithPossibleWarning(List targets) - { - if (targets.Any(static t => t is Item it && it.GetComponent() is not null)) - { - GUI.AskForConfirmation(CircuitBoxDeletionWarningHeader, CircuitBoxDeletionWarningBody, onConfirm: Delete); - return; - } - - Delete(); - - void Delete() - { - StoreCommand(new AddOrDeleteCommand(targets, true)); - foreach (var me in targets) - { - if (!me.Removed) { me.Remove(); } - } + new ContextMenuOption("delete", isEnabled: hasTargets, onSelected: () => + { + StoreCommand(new AddOrDeleteCommand(targets, true)); + foreach (var me in targets) + { + if (!me.Removed) { me.Remove(); } + } + }), + new ContextMenuOption(TextManager.GetWithVariable("editortip.shiftforextraoptions", "[button]", PlayerInput.SecondaryMouseLabel) + '\n' + TextManager.Get("editortip.altforruler"), isEnabled: false, onSelected: null)); } } @@ -4439,6 +4447,7 @@ namespace Barotrauma MapEntity.SelectEntity(itemContainer); dummyCharacter.SelectedItem = itemContainer; FilterEntities(entityFilterBox.Text); + MapEntity.StopSelection(); } /// @@ -5469,9 +5478,11 @@ namespace Barotrauma { foreach (LightComponent lightComponent in item.GetComponents()) { - lightComponent.Light.Color = item.Container != null || (item.body != null && !item.body.Enabled) ? - Color.Transparent : - lightComponent.LightColor; + lightComponent.Light.Color = + item.body == null || item.body.Enabled || + (item.ParentInventory is ItemInventory itemInventory && !itemInventory.Container.HideItems) ? + lightComponent.LightColor : + Color.Transparent; lightComponent.Light.LightSpriteEffect = lightComponent.Item.SpriteEffects; } } @@ -5556,11 +5567,32 @@ namespace Barotrauma dummyCharacter.Submarine = MainSub; } - // Deposit item from our "infinite stack" into inventory slots - var inv = dummyCharacter?.SelectedItem?.OwnInventory; - if (inv?.visualSlots != null && !PlayerInput.IsCtrlDown()) + if (dummyCharacter?.SelectedItem != null) { - var dragginMouse = MouseDragStart != Vector2.Zero && Vector2.Distance(PlayerInput.MousePosition, MouseDragStart) >= GUI.Scale * 20; + // Deposit item from our "infinite stack" into inventory slots + TryDragItemsToItem(dummyCharacter.SelectedItem); + foreach (Item linkedItem in dummyCharacter.SelectedItem.linkedTo.OfType()) + { + TryDragItemsToItem(linkedItem); + } + } + + void TryDragItemsToItem(Item item) + { + foreach (ItemContainer ic in item.GetComponents()) + { + if (ic.Inventory?.visualSlots != null) + { + TryDragItemsToInventory(ic.Inventory); + } + } + } + + void TryDragItemsToInventory(Inventory inv) + { + if (PlayerInput.IsCtrlDown()) { return; } + + var draggingMouse = MouseDragStart != Vector2.Zero && Vector2.Distance(PlayerInput.MousePosition, MouseDragStart) >= GUI.Scale * 20; // So we don't accidentally drag inventory items while doing this if (DraggedItemPrefab != null) { Inventory.DraggingItems.Clear(); } @@ -5568,134 +5600,134 @@ namespace Barotrauma switch (DraggedItemPrefab) { // regular item prefabs - case ItemPrefab itemPrefab when PlayerInput.PrimaryMouseButtonClicked() || dragginMouse: - { - bool spawnedItem = false; - for (var i = 0; i < inv.Capacity; i++) + case ItemPrefab itemPrefab when PlayerInput.PrimaryMouseButtonClicked() || draggingMouse: { - var slot = inv.visualSlots[i]; - var itemContainer = inv.GetItemAt(i)?.GetComponent(); - - // check if the slot is empty or if we can place the item into a container, for example an oxygen tank into a diving suit - if (Inventory.IsMouseOnSlot(slot)) + bool spawnedItem = false; + for (var i = 0; i < inv.Capacity; i++) { - var newItem = new Item(itemPrefab, Vector2.Zero, MainSub); + var slot = inv.visualSlots[i]; + var itemContainer = inv.GetItemAt(i)?.GetComponent(); - if (inv.CanBePutInSlot(itemPrefab, i, condition: null)) + // check if the slot is empty or if we can place the item into a container, for example an oxygen tank into a diving suit + if (Inventory.IsMouseOnSlot(slot)) { - bool placedItem = inv.TryPutItem(newItem, i, false, true, dummyCharacter); - spawnedItem |= placedItem; + var newItem = new Item(itemPrefab, Vector2.Zero, MainSub); - if (!placedItem) + if (inv.CanBePutInSlot(itemPrefab, i, condition: null)) { - newItem.Remove(); + bool placedItem = inv.TryPutItem(newItem, i, false, true, dummyCharacter); + spawnedItem |= placedItem; + + if (!placedItem) + { + newItem.Remove(); + } } - } - else if (itemContainer != null && itemContainer.Inventory.CanBePut(itemPrefab)) - { - bool placedItem = itemContainer.Inventory.TryPutItem(newItem, dummyCharacter); - spawnedItem |= placedItem; - - // try to place the item into the inventory of the item we are hovering over - if (!placedItem) + else if (itemContainer != null && itemContainer.Inventory.CanBePut(itemPrefab)) { - newItem.Remove(); + bool placedItem = itemContainer.Inventory.TryPutItem(newItem, dummyCharacter); + spawnedItem |= placedItem; + + // try to place the item into the inventory of the item we are hovering over + if (!placedItem) + { + newItem.Remove(); + } + else + { + slot.ShowBorderHighlight(GUIStyle.Green, 0.1f, 0.4f); + } } else { - slot.ShowBorderHighlight(GUIStyle.Green, 0.1f, 0.4f); + newItem.Remove(); + slot.ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.4f); + } + + if (!newItem.Removed) + { + BulkItemBufferInUse = ItemAddMutex; + BulkItemBuffer.Add(new AddOrDeleteCommand(new List { newItem }, false)); + } + + if (!draggingMouse) + { + SoundPlayer.PlayUISound(spawnedItem ? GUISoundType.PickItem : GUISoundType.PickItemFail); } } - else - { - newItem.Remove(); - slot.ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.4f); - } - - if (!newItem.Removed) - { - BulkItemBufferInUse = ItemAddMutex; - BulkItemBuffer.Add(new AddOrDeleteCommand(new List { newItem }, false)); - } - - if (!dragginMouse) - { - SoundPlayer.PlayUISound(spawnedItem ? GUISoundType.PickItem : GUISoundType.PickItemFail); - } } + break; } - break; - } // item assemblies case ItemAssemblyPrefab assemblyPrefab when PlayerInput.PrimaryMouseButtonClicked(): - { - bool spawnedItems = false; - for (var i = 0; i < inv.visualSlots.Length; i++) { - var slot = inv.visualSlots[i]; - var item = inv?.GetItemAt(i); - var itemContainer = item?.GetComponent(); - if (item == null && Inventory.IsMouseOnSlot(slot)) + bool spawnedItems = false; + for (var i = 0; i < inv.visualSlots.Length; i++) { - // load the items - var itemInstance = LoadItemAssemblyInventorySafe(assemblyPrefab); - - // counter for items that failed so we so we known that slot remained empty - var failedCount = 0; - - for (var j = 0; j < itemInstance.Count(); j++) + var slot = inv.visualSlots[i]; + var item = inv?.GetItemAt(i); + var itemContainer = item?.GetComponent(); + if (item == null && Inventory.IsMouseOnSlot(slot)) { - var newItem = itemInstance[j]; - var newSpot = i + j - failedCount; + // load the items + var itemInstance = LoadItemAssemblyInventorySafe(assemblyPrefab); - // try to find a valid slot to put the items - while (inv.visualSlots.Length > newSpot) + // counter for items that failed so we so we known that slot remained empty + var failedCount = 0; + + for (var j = 0; j < itemInstance.Count; j++) { - if (inv.GetItemAt(newSpot) == null) { break; } - newSpot++; - } + var newItem = itemInstance[j]; + var newSpot = i + j - failedCount; - // valid slot found - if (inv.visualSlots.Length > newSpot) - { - var placedItem = inv.TryPutItem(newItem, newSpot, false, true, dummyCharacter); - spawnedItems |= placedItem; - - if (!placedItem) + // try to find a valid slot to put the items + while (inv.visualSlots.Length > newSpot) { - failedCount++; - // delete the included items too so we don't get a popup asking if we want to keep them - newItem?.OwnInventory?.DeleteAllItems(); - newItem.Remove(); + if (inv.GetItemAt(newSpot) == null) { break; } + newSpot++; + } + + // valid slot found + if (inv.visualSlots.Length > newSpot) + { + var placedItem = inv.TryPutItem(newItem, newSpot, false, true, dummyCharacter); + spawnedItems |= placedItem; + + if (!placedItem) + { + failedCount++; + // delete the included items too so we don't get a popup asking if we want to keep them + newItem?.OwnInventory?.DeleteAllItems(); + newItem.Remove(); + } + } + else + { + var placedItem = inv.TryPutItem(newItem, dummyCharacter); + spawnedItems |= placedItem; + + // if our while loop didn't find a valid slot then let the inventory decide where to put it as a last resort + if (!placedItem) + { + // delete the included items too so we don't get a popup asking if we want to keep them + newItem?.OwnInventory?.DeleteAllItems(); + newItem.Remove(); + } } } - else + + List placedEntities = itemInstance.Where(it => !it.Removed).Cast().ToList(); + if (placedEntities.Any()) { - var placedItem = inv.TryPutItem(newItem, dummyCharacter); - spawnedItems |= placedItem; - - // if our while loop didn't find a valid slot then let the inventory decide where to put it as a last resort - if (!placedItem) - { - // delete the included items too so we don't get a popup asking if we want to keep them - newItem?.OwnInventory?.DeleteAllItems(); - newItem.Remove(); - } + BulkItemBufferInUse = ItemAddMutex; + BulkItemBuffer.Add(new AddOrDeleteCommand(placedEntities, false)); } } - - List placedEntities = itemInstance.Where(it => !it.Removed).Cast().ToList(); - if (placedEntities.Any()) - { - BulkItemBufferInUse = ItemAddMutex; - BulkItemBuffer.Add(new AddOrDeleteCommand(placedEntities, false)); - } } - } - SoundPlayer.PlayUISound(spawnedItems ? GUISoundType.PickItem : GUISoundType.PickItemFail); - break; - } + SoundPlayer.PlayUISound(spawnedItems ? GUISoundType.PickItem : GUISoundType.PickItemFail); + break; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index 8cd9300f5..38f110c95 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -571,16 +571,37 @@ namespace Barotrauma numberInput.MinValueFloat = editableAttribute.MinValueFloat; numberInput.MaxValueFloat = editableAttribute.MaxValueFloat; numberInput.DecimalsToDisplay = editableAttribute.DecimalCount; - numberInput.valueStep = editableAttribute.ValueStep; + numberInput.ValueStep = editableAttribute.ValueStep; + numberInput.ForceShowPlusMinusButtons = editableAttribute.ForceShowPlusMinusButtons; numberInput.FloatValue = value; - numberInput.OnValueChanged += (numInput) => + numberInput.OnValueChanged += numInput => { if (SetPropertyValue(property, entity, numInput.FloatValue)) { TrySendNetworkUpdate(entity, property); } }; + + // Lots of UI boilerplate to handle all(?) cases where the property's setter may be called + // and modify the input value (e.g. rotation value wrapping) + void HandleSetterModifyingInput(GUINumberInput numInput) + { + var inputFloatValue = numInput.FloatValue; + var resultingFloatValue = property.GetFloatValue(entity); + if (!MathUtils.NearlyEqual(resultingFloatValue, inputFloatValue)) + { + numInput.FloatValue = resultingFloatValue; + } + } + bool HandleSetterModifyingInputOnButtonPressed() { HandleSetterModifyingInput(numberInput); return true; } + bool HandleSetterModifyingInputOnButtonClicked(GUIButton _, object __) { HandleSetterModifyingInput(numberInput); return true; } + + numberInput.OnValueEntered += HandleSetterModifyingInput; + numberInput.PlusButton.OnPressed += HandleSetterModifyingInputOnButtonPressed; + numberInput.PlusButton.OnClicked += HandleSetterModifyingInputOnButtonClicked; + numberInput.MinusButton.OnPressed += HandleSetterModifyingInputOnButtonPressed; + numberInput.MinusButton.OnClicked += HandleSetterModifyingInputOnButtonClicked; refresh += () => { if (!numberInput.TextBox.Selected) { numberInput.FloatValue = (float)property.GetValue(entity); } @@ -859,7 +880,7 @@ namespace Barotrauma numberInput.MinValueFloat = editableAttribute.MinValueFloat; numberInput.MaxValueFloat = editableAttribute.MaxValueFloat; numberInput.DecimalsToDisplay = editableAttribute.DecimalCount; - numberInput.valueStep = editableAttribute.ValueStep; + numberInput.ValueStep = editableAttribute.ValueStep; if (i == 0) numberInput.FloatValue = value.X; @@ -930,7 +951,7 @@ namespace Barotrauma numberInput.MinValueFloat = editableAttribute.MinValueFloat; numberInput.MaxValueFloat = editableAttribute.MaxValueFloat; numberInput.DecimalsToDisplay = editableAttribute.DecimalCount; - numberInput.valueStep = editableAttribute.ValueStep; + numberInput.ValueStep = editableAttribute.ValueStep; if (i == 0) numberInput.FloatValue = value.X; @@ -1006,7 +1027,7 @@ namespace Barotrauma numberInput.MinValueFloat = editableAttribute.MinValueFloat; numberInput.MaxValueFloat = editableAttribute.MaxValueFloat; numberInput.DecimalsToDisplay = editableAttribute.DecimalCount; - numberInput.valueStep = editableAttribute.ValueStep; + numberInput.ValueStep = editableAttribute.ValueStep; if (i == 0) numberInput.FloatValue = value.X; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs index 4d443268b..24d40dd72 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs @@ -727,7 +727,21 @@ namespace Barotrauma Slider(layout, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.InventoryScale, v => unsavedConfig.Graphics.InventoryScale = v); Label(layout, TextManager.Get("TextScale"), GUIStyle.SubHeadingFont); Slider(layout, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.TextScale, v => unsavedConfig.Graphics.TextScale = v); - + Spacer(layout); + var resetSpamListFilter = + new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), layout.RectTransform), + TextManager.Get("clearserverlistfilters"), style: "GUIButtonSmall") + { + OnClicked = static (_, _) => + { + GUI.AskForConfirmation( + header: TextManager.Get("clearserverlistfilters"), + body: TextManager.Get("clearserverlistfiltersconfirmation"), + onConfirm: SpamServerFilters.ClearLocalSpamFilter); + return true; + } + }; + Spacer(layout); #if !OSX Spacer(layout); var statisticsTickBox = new GUITickBox(NewItemRectT(layout), TextManager.Get("statisticsconsenttickbox")) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs index f816a71a8..d050e09c4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs @@ -1,22 +1,23 @@ -using System; +using NVorbis; using OpenAL; -using NVorbis; +using System; using System.Collections.Generic; using System.Threading.Tasks; -using System.Xml.Linq; namespace Barotrauma.Sounds { sealed class OggSound : Sound { - private VorbisReader streamReader; + private readonly VorbisReader streamReader; + + public long MaxStreamSamplePos => streamReader == null ? 0 : streamReader.TotalSamples * streamReader.Channels * 2; private List playbackAmplitude; private const int AMPLITUDE_SAMPLE_COUNT = 4410; //100ms in a 44100hz file private short[] sampleBuffer = Array.Empty(); private short[] muffleBuffer = Array.Empty(); - public OggSound(SoundManager owner, string filename, bool stream, XElement xElement) : base(owner, filename, + public OggSound(SoundManager owner, string filename, bool stream, ContentXElement xElement) : base(owner, filename, stream, true, xElement) { var reader = new VorbisReader(Filename); @@ -101,7 +102,7 @@ namespace Barotrauma.Sounds if (!Stream) { throw new Exception("Called FillStreamBuffer on a non-streamed sound!"); } if (streamReader == null) { throw new Exception("Called FillStreamBuffer when the reader is null!"); } - if (samplePos >= streamReader.TotalSamples * streamReader.Channels * 2) return 0; + if (samplePos >= MaxStreamSamplePos) { return 0; } samplePos /= streamReader.Channels * 2; streamReader.DecodedPosition = samplePos; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs index 8d2ea80c6..8c815b9c0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs @@ -18,7 +18,7 @@ namespace Barotrauma.Sounds public readonly string Filename; - public readonly XElement XElement; + public readonly ContentXElement XElement; public readonly bool Stream; @@ -60,14 +60,14 @@ namespace Barotrauma.Sounds public float BaseNear; public float BaseFar; - public Sound(SoundManager owner, string filename, bool stream, bool streamsReliably, XElement xElement = null, bool getFullPath = true) + public Sound(SoundManager owner, string filename, bool stream, bool streamsReliably, ContentXElement xElement = null, bool getFullPath = true) { Owner = owner; Filename = getFullPath ? Path.GetFullPath(filename.CleanUpPath()).CleanUpPath() : filename; Stream = stream; StreamsReliably = streamsReliably; XElement = xElement; - sourcePoolIndex = XElement.GetAttributeEnum("sourcepool", SoundManager.SourcePoolIndex.Default); + sourcePoolIndex = XElement?.GetAttributeEnum("sourcepool", SoundManager.SourcePoolIndex.Default) ?? SoundManager.SourcePoolIndex.Default; BaseGain = 1.0f; BaseNear = 100.0f; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs index b08ed5862..08e9811ce 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs @@ -444,6 +444,18 @@ namespace Barotrauma.Sounds } } + public long MaxStreamSeekPos + { + get + { + if (!IsStream || Sound is not OggSound oggSound) + { + return 0; + } + return oggSound.MaxStreamSamplePos; + } + } + private readonly object mutex; public bool IsPlaying @@ -564,7 +576,7 @@ namespace Barotrauma.Sounds throw new Exception("Generated streamBuffer[" + i.ToString() + "] is invalid! " + debugName); } } - Sound.Owner.InitStreamThread(); + Sound.Owner.InitUpdateChannelThread(); SetProperties(); } } @@ -609,6 +621,7 @@ namespace Barotrauma.Sounds public void FadeOutAndDispose() { FadingOutAndDisposing = true; + Sound.Owner.InitUpdateChannelThread(); } public void Dispose() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs index e6cefeb2d..c9a06e685 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs @@ -39,7 +39,7 @@ namespace Barotrauma.Sounds public bool Disconnected { get; private set; } - private Thread streamingThread; + private Thread updateChannelsThread; private Vector3 listenerPosition; public Vector3 ListenerPosition @@ -201,7 +201,7 @@ namespace Barotrauma.Sounds public SoundManager() { loadedSounds = new List(); - streamingThread = null; + updateChannelsThread = null; sourcePools = new SoundSourcePool[2]; playingChannels[(int)SourcePoolIndex.Default] = new SoundChannel[SOURCE_COUNT]; @@ -696,7 +696,7 @@ namespace Barotrauma.Sounds CompressionDynamicRangeGain = 1.0f; } - if (streamingThread == null || streamingThread.ThreadState.HasFlag(ThreadState.Stopped)) + if (updateChannelsThread == null || updateChannelsThread.ThreadState.HasFlag(ThreadState.Stopped)) { bool startedStreamThread = false; for (int i = 0; i < playingChannels.Length; i++) @@ -708,7 +708,7 @@ namespace Barotrauma.Sounds if (playingChannels[i][j] == null) { continue; } if (playingChannels[i][j].IsStream && playingChannels[i][j].IsPlaying) { - InitStreamThread(); + InitUpdateChannelThread(); startedStreamThread = true; } if (startedStreamThread) { break; } @@ -727,37 +727,43 @@ namespace Barotrauma.Sounds SetCategoryGainMultiplier("music", GameSettings.CurrentConfig.Audio.MusicVolume, 0); SetCategoryGainMultiplier("voip", Math.Min(GameSettings.CurrentConfig.Audio.VoiceChatVolume, 1.0f), 0); } - - public void InitStreamThread() + + /// + /// Initializes the thread that handles streaming audio and fading out and disposing channels that are no longer needed. + /// + public void InitUpdateChannelThread() { if (Disabled) { return; } - bool isStreamThreadDying; + bool isUpdateChannelsThreadDying; lock (threadDeathMutex) { - isStreamThreadDying = !areStreamsPlaying; + isUpdateChannelsThreadDying = !needsUpdateChannels; } - if (streamingThread == null || streamingThread.ThreadState.HasFlag(ThreadState.Stopped) || isStreamThreadDying) + if (updateChannelsThread == null || updateChannelsThread.ThreadState.HasFlag(ThreadState.Stopped) || isUpdateChannelsThreadDying) { - if (streamingThread != null && !streamingThread.Join(1000)) + if (updateChannelsThread != null && !updateChannelsThread.Join(1000)) { - DebugConsole.ThrowError("Sound stream thread join timed out!"); + DebugConsole.ThrowError("SoundManager.UpdateChannels thread join timed out!"); } - areStreamsPlaying = true; - streamingThread = new Thread(UpdateStreaming) + needsUpdateChannels = true; + updateChannelsThread = new Thread(UpdateChannels) { - Name = "SoundManager Streaming Thread", + Name = "SoundManager.UpdateChannels Thread", IsBackground = true //this should kill the thread if the game crashes }; - streamingThread.Start(); + updateChannelsThread.Start(); } } - bool areStreamsPlaying = false; - ManualResetEvent streamMre = null; + private bool needsUpdateChannels = false; + private ManualResetEvent updateChannelsMre = null; - void UpdateStreaming() + /// + /// Handles streaming audio and fading out and disposing channels that are no longer needed. + /// + private void UpdateChannels() { - streamMre = new ManualResetEvent(false); + updateChannelsMre = new ManualResetEvent(false); bool killThread = false; while (!killThread) { @@ -784,6 +790,7 @@ namespace Barotrauma.Sounds } else if (playingChannels[i][j].FadingOutAndDisposing) { + killThread = false; playingChannels[i][j].Gain -= 0.1f; if (playingChannels[i][j].Gain <= 0.0f) { @@ -794,18 +801,18 @@ namespace Barotrauma.Sounds } } } - streamMre.WaitOne(10); - streamMre.Reset(); + updateChannelsMre.WaitOne(10); + updateChannelsMre.Reset(); lock (threadDeathMutex) { - areStreamsPlaying = !killThread; + needsUpdateChannels = !killThread; } } } public void ForceStreamUpdate() { - streamMre?.Set(); + updateChannelsMre?.Set(); } private void ReloadSounds() @@ -824,12 +831,12 @@ namespace Barotrauma.Sounds { for (int j = 0; j < playingChannels[i].Length; j++) { - if (playingChannels[i][j] != null) playingChannels[i][j].Dispose(); + playingChannels[i][j]?.Dispose(); } } } - streamingThread?.Join(); + updateChannelsThread?.Join(); for (int i = loadedSounds.Count - 1; i >= 0; i--) { if (keepSounds) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index e04b5a60f..3be4a5571 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -709,6 +709,11 @@ namespace Barotrauma { musicChannel[i].StreamSeekPos = targetMusic[i].PreviousTime; } + else if (targetMusic[i].StartFromRandomTime) + { + musicChannel[i].StreamSeekPos = + (int)(musicChannel[i].MaxStreamSeekPos * Rand.Range(0.0f, 1.0f, Rand.RandSync.Unsynced)); + } musicChannel[i].Looping = true; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs index bb3af5eee..f6c8a2beb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs @@ -241,6 +241,7 @@ namespace Barotrauma public readonly bool MuteIntensityTracks; public readonly float? ForceIntensityTrack; + public readonly bool StartFromRandomTime; public readonly bool ContinueFromPreviousTime; public int PreviousTime; @@ -255,6 +256,7 @@ namespace Barotrauma ForceIntensityTrack = element.GetAttributeFloat(nameof(ForceIntensityTrack), 0.0f); } Volume = element.GetAttributeFloat(nameof(Volume), 1.0f); + StartFromRandomTime = element.GetAttributeBool(nameof(StartFromRandomTime), false); ContinueFromPreviousTime = element.GetAttributeBool(nameof(ContinueFromPreviousTime), false); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/SpamServerFilter.cs b/Barotrauma/BarotraumaClient/ClientSource/SpamServerFilter.cs new file mode 100644 index 000000000..07da313e3 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/SpamServerFilter.cs @@ -0,0 +1,330 @@ +#nullable enable + +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Net; +using System.Net.Cache; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Linq; +using Barotrauma.IO; +using Barotrauma.Networking; +using RestSharp; +using XmlWriter = Barotrauma.IO.XmlWriter; + +namespace Barotrauma +{ + public enum SpamServerFilterType + { + Invalid, + NameEquals, + NameContains, + MessageEquals, + MessageContains, + PlayerCountLarger, + PlayerCountExact, + MaxPlayersLarger, + MaxPlayersExact, + GameModeEquals, + PlayStyleEquals, + Endpoint, + LanguageEquals + } + + internal readonly record struct SpamFilter(ImmutableHashSet<(SpamServerFilterType Type, string Value)> Filters) + { + public bool IsFiltered(ServerInfo info) + { + if (!Filters.Any()) { return false; } + + foreach (var (type, value) in Filters) + { + if (!IsFiltered(info, type, value)) { return false; } + } + + return true; + } + + private static bool IsFiltered(ServerInfo info, SpamServerFilterType type, string value) + { + string desc = info.ServerMessage, + name = info.ServerName; + + int.TryParse(value, out int parsedInt); + + return type switch + { + SpamServerFilterType.NameEquals => CompareEquals(name, value), + SpamServerFilterType.NameContains => CompareContains(name, value), + + SpamServerFilterType.MessageEquals => CompareEquals(desc, value), + SpamServerFilterType.MessageContains => CompareContains(desc, value), + + SpamServerFilterType.Endpoint => info.Endpoint.StringRepresentation.Equals(value, StringComparison.OrdinalIgnoreCase), + + SpamServerFilterType.PlayerCountLarger => info.PlayerCount > parsedInt, + SpamServerFilterType.PlayerCountExact => info.PlayerCount == parsedInt, + + SpamServerFilterType.MaxPlayersLarger => info.MaxPlayers > parsedInt, + SpamServerFilterType.MaxPlayersExact => info.MaxPlayers == parsedInt, + + SpamServerFilterType.GameModeEquals => info.GameMode == value, + SpamServerFilterType.PlayStyleEquals => info.PlayStyle.ToIdentifier() == value, + + SpamServerFilterType.LanguageEquals => info.Language.Value == value, + _ => false + }; + + static bool CompareEquals(string a, string b) + => a.Equals(b, StringComparison.OrdinalIgnoreCase) || Homoglyphs.Compare(a, b); + + static bool CompareContains(string a, string b) + => a.Contains(b, StringComparison.OrdinalIgnoreCase); + } + + public XElement Serialize() + { + var element = new XElement("Filter"); + + foreach (var (type, value) in Filters) + { + element.Add(new XAttribute(type.ToString().ToLowerInvariant(), value)); + } + + return element; + } + + public static bool TryParse(XElement element, out SpamFilter filter) + { + var builder = ImmutableHashSet.CreateBuilder<(SpamServerFilterType Type, string Value)>(); + foreach (var attribute in element.Attributes()) + { + if (!Enum.TryParse(attribute.Name.ToString(), ignoreCase: true, out SpamServerFilterType e)) + { + DebugConsole.ThrowError($"Failed to parse spam filter attribute \"{attribute.Name}\""); + continue; + } + if (e is SpamServerFilterType.Invalid) { continue; } + builder.Add((e, attribute.Value)); + } + + if (builder.Any()) + { + filter = new SpamFilter(builder.ToImmutable()); + return true; + } + + filter = default; + return false; + } + + public override string ToString() + { + return !Filters.Any() ? "Invalid Filter" : string.Join(", ", Filters.Select(static f => $"{f.Type}: {f.Value}")); + } + } + + internal sealed class SpamServerFilter + { + public readonly ImmutableArray Filters; + + public bool IsFiltered(ServerInfo info) + { + foreach (var f in Filters) + { + if (f.IsFiltered(info)) { return true; } + } + + return false; + } + + public SpamServerFilter(XElement element) + { + var builder = ImmutableArray.CreateBuilder(); + foreach (var subElement in element.Elements()) + { + if (SpamFilter.TryParse(subElement, out var filter)) + { + builder.Add(filter); + } + } + Filters = builder.ToImmutable(); + } + + public SpamServerFilter(ImmutableArray filters) + => Filters = filters; + + public readonly static string SavePath = Path.Combine("Data", "serverblacklist.xml"); + + public void Save(string path) + { + var comment = new XComment(SpamServerFilters.LocalFilterComment); + var doc = new XDocument(comment, new XElement("Filters")); + foreach (var filter in Filters) + { + doc.Root?.Add(filter.Serialize()); + } + + try + { + using var writer = XmlWriter.Create(path, new XmlWriterSettings { Indent = true }); + doc.SaveSafe(writer); + } + catch (Exception e) + { + DebugConsole.ThrowError("Saving spam filter failed.", e); + } + } + } + + internal static class SpamServerFilters + { + public static Option LocalSpamFilter; + public static Option GlobalSpamFilter; + + public const string LocalFilterComment = @" +This file contains a list of filters that can be used to hide servers from the server list. +You can add filters by right-clicking a server in the server list and selecting ""Hide server"" or by reporting the server and choosing ""Report and hide server"". +The filters are saved in this file, which you can edit manually if you want to. + +The available filter types are: +- NameEquals: The server name must equal the specified value. Homoglyphs are also checked. +- NameContains: The server name must contain the specified value. +- MessageEquals: The server description must equal the specified value. Homoglyphs are also checked. +- MessageContains: The server description must contain the specified value. +- PlayerCountLarger: The player count must be larger than the specified value. +- PlayerCountExact: The player count must match the specified value exactly. +- MaxPlayersLarger: The max player count must be larger than the specified value. +- MaxPlayersExact: The max player count must match the specified value exactly. +- GameModeEquals: The game mode identifier must match the specified value exactly. +- PlayStyleEquals: The play style must match the specified value exactly. +- Endpoint: The server endpoint, which is a Steam ID or an IP address, must match the specified value exactly. Steam ID is in the format of STEAM_X:Y:Z. +- LanguageEquals: The server language must match the specified value exactly. + +The filter values are case-insensitive and adding multiple conditions on one filter will require all of them to be met. +Homoglyph comparison is used for NameEquals and MessageEquals filters, which means that it checks whether the words look the same, meaning you can't abuse identical-looking but different symbols to work around the filter. For example ""lmaobox"" and ""lmаobox"" (with a cyrillic a) are considered equal. + +Examples: + + + + + +These will hide all servers that have a discord.gg link in their name or description and servers with the name ""get good get lmaobox"" that have 999 max players. +"; + static SpamServerFilters() + { + XDocument? doc; + if (!File.Exists(SpamServerFilter.SavePath)) + { + var comment = new XComment(LocalFilterComment); + + doc = new XDocument(comment, new XElement("Filters")); + + try + { + using var writer = XmlWriter.Create(SpamServerFilter.SavePath, new XmlWriterSettings { Indent = true }); + doc.SaveSafe(writer); + } + catch (Exception e) + { + DebugConsole.ThrowError("Saving spam filter failed.", e); + } + } + else + { + doc = XMLExtensions.TryLoadXml(SpamServerFilter.SavePath); + } + + if (doc?.Root is { } root) + { + LocalSpamFilter = Option.Some(new SpamServerFilter(root)); + } + } + + public static bool IsFiltered(ServerInfo info) + { + if (LocalSpamFilter.TryUnwrap(out var localFilter) && localFilter.IsFiltered(info)) { return true; } + if (GlobalSpamFilter.TryUnwrap(out var globalFilter) && globalFilter.IsFiltered(info)) { return true; } + return false; + } + + public static void AddServerToLocalSpamList(ServerInfo info) + { + if (!LocalSpamFilter.TryUnwrap(out var localFilter)) { return; } + if (localFilter.IsFiltered(info)) { return; } + + var filters = localFilter.Filters.Add(new SpamFilter(ImmutableHashSet.Create((NameExact: SpamServerFilterType.NameEquals, info.ServerName)))); + var newFilter = new SpamServerFilter(filters); + newFilter.Save(SpamServerFilter.SavePath); + LocalSpamFilter = Option.Some(newFilter); + } + + public static void ClearLocalSpamFilter() + { + var newFilter = new SpamServerFilter(ImmutableArray.Empty); + newFilter.Save(SpamServerFilter.SavePath); + LocalSpamFilter = Option.Some(newFilter); + } + + public static void RequestGlobalSpamFilter() + { + if (GameSettings.CurrentConfig.DisableGlobalSpamList) { return; } + + string remoteContentUrl = GameSettings.CurrentConfig.RemoteMainMenuContentUrl; + if (string.IsNullOrEmpty(remoteContentUrl)) { return; } + + try + { + var client = new RestClient($"{remoteContentUrl}spamfilter") + { + CachePolicy = new HttpRequestCachePolicy(HttpRequestCacheLevel.NoCacheNoStore) + }; + client.AddDefaultHeader("Cache-Control", "no-cache"); + client.AddDefaultHeader("Pragma", "no-cache"); + var request = new RestRequest("serve_spamlist.php", Method.GET); + TaskPool.Add("RequestGlobalSpamFilter", client.ExecuteAsync(request), RemoteContentReceived); + } + catch (Exception e) + { +#if DEBUG + DebugConsole.ThrowError("Fetching global spam list failed.", e); +#endif + GameAnalyticsManager.AddErrorEventOnce("SpamServerFilters.RequestGlobalSpamFilter:Exception", GameAnalyticsManager.ErrorSeverity.Error, + "Fetching global spam list failed. " + e.Message); + } + + static void RemoteContentReceived(Task t) + { + try + { + if (!t.TryGetResult(out IRestResponse remoteContentResponse)) { throw new Exception("Task did not return a valid result"); } + if (remoteContentResponse.StatusCode != HttpStatusCode.OK) + { + DebugConsole.AddWarning( + "Failed to receive global spam filter." + + "There may be an issue with your internet connection, or the master server might be temporarily unavailable " + + $"(error code: {remoteContentResponse.StatusCode})"); + return; + } + string data = remoteContentResponse.Content; + if (string.IsNullOrWhiteSpace(data)) { return; } + + if (XDocument.Parse(data).Root is { } root) + { + GlobalSpamFilter = Option.Some(new SpamServerFilter(root)); + } + } + catch (Exception e) + { +#if DEBUG + DebugConsole.ThrowError("Reading received global spam filter failed.", e); +#endif + GameAnalyticsManager.AddErrorEventOnce("SpamServerFilters.RemoteContentReceived:Exception", GameAnalyticsManager.ErrorSeverity.Error, + "Reading received global spam filter failed. " + e.Message); + } + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs index e599e9c41..5314247ec 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs @@ -111,7 +111,7 @@ namespace Barotrauma partial void LoadTexture(ref Vector4 sourceVector, ref bool shouldReturn) { - texture = LoadTexture(FilePath.Value, Compress); + texture = LoadTexture(FilePath.Value, Compress, contentPackage: SourceElement?.ContentPackage); if (texture == null) { @@ -175,7 +175,7 @@ namespace Barotrauma return; } texture.Dispose(); - texture = TextureLoader.FromFile(FilePath.Value, Compress); + texture = TextureLoader.FromFile(FilePath.Value, Compress, contentPackage: SourceElement?.ContentPackage); Identifier pathKey = FullPath.ToIdentifier(); if (textureRefCounts.ContainsKey(pathKey)) { @@ -195,7 +195,7 @@ namespace Barotrauma sourceRect = new Rectangle(0, 0, texture.Width, texture.Height); } - public static Texture2D LoadTexture(string file, bool compress = true) + public static Texture2D LoadTexture(string file, bool compress = true, ContentPackage contentPackage = null) { if (string.IsNullOrWhiteSpace(file)) { @@ -221,11 +221,11 @@ namespace Barotrauma if (!ToolBox.IsProperFilenameCase(file)) { #if DEBUG - DebugConsole.ThrowError("Texture file \"" + file + "\" has incorrect case!"); + DebugConsole.ThrowError("Texture file \"" + file + "\" has incorrect case!", contentPackage: contentPackage); #endif } - Texture2D newTexture = TextureLoader.FromFile(file, compress); + Texture2D newTexture = TextureLoader.FromFile(file, compress, contentPackage: contentPackage); lock (list) { if (!textureRefCounts.TryAdd(fullPath, @@ -284,17 +284,44 @@ namespace Barotrauma } } - public void DrawTiled(ISpriteBatch spriteBatch, Vector2 position, Vector2 targetSize, - Color? color = null, Vector2? startOffset = null, Vector2? textureScale = null, float? depth = null) + public void DrawTiled(ISpriteBatch spriteBatch, + Vector2 position, + Vector2 targetSize, + float rotation = 0f, + Vector2? origin = null, + Color? color = null, + Vector2? startOffset = null, + Vector2? textureScale = null, + float? depth = null, + SpriteEffects? spriteEffects = null) { if (Texture == null) { return; } + + spriteEffects ??= effects; + bool flipHorizontal = (spriteEffects.Value & SpriteEffects.FlipHorizontally) != 0; + bool flipVertical = (spriteEffects.Value & SpriteEffects.FlipVertically) != 0; + + float addedRotation = rotation + this.rotation; + if (flipHorizontal != flipVertical) { addedRotation = -addedRotation; } + + Vector2 advanceX = addedRotation == 0.0f ? Vector2.UnitX : new Vector2((float)Math.Cos(addedRotation), (float)Math.Sin(addedRotation)); + Vector2 advanceY = new Vector2(-advanceX.Y, advanceX.X); + //Init optional values Vector2 drawOffset = startOffset ?? Vector2.Zero; Vector2 scale = textureScale ?? Vector2.One; Color drawColor = color ?? Color.White; + Vector2 transformedOrigin = origin ?? Vector2.Zero; - bool flipHorizontal = (effects & SpriteEffects.FlipHorizontally) != 0; - bool flipVertical = (effects & SpriteEffects.FlipVertically) != 0; + transformedOrigin = advanceX * transformedOrigin.X + advanceY * transformedOrigin.Y; + + void drawSection(Vector2 slicePos, Rectangle sliceRect) + { + Vector2 transformedPos = slicePos - position; + transformedPos = advanceX * transformedPos.X + advanceY * transformedPos.Y; + transformedPos += position - transformedOrigin; + spriteBatch.Draw(texture, transformedPos, sliceRect, drawColor, addedRotation, Vector2.Zero, scale, spriteEffects.Value, depth ?? this.depth); + } //wrap the drawOffset inside the sourceRect drawOffset.X = (drawOffset.X / scale.X) % sourceRect.Width; @@ -368,8 +395,8 @@ namespace Barotrauma { slicePos.Y += flippedDrawOffset.Y; } - - spriteBatch.Draw(texture, slicePos, sliceRect, drawColor, rotation, Vector2.Zero, scale, effects, depth ?? this.depth); + + drawSection(slicePos, sliceRect); currDrawPosition.X = slicePos.X + sliceWidth; } } @@ -416,7 +443,7 @@ namespace Barotrauma sliceRect.Y = SourceRect.Y; sliceRect.Height = (int)(sliceHeight / scale.Y); - spriteBatch.Draw(texture, slicePos, sliceRect, drawColor, rotation, Vector2.Zero, scale, effects, depth ?? this.depth); + drawSection(slicePos, sliceRect); currDrawPosition.Y = slicePos.Y + sliceHeight; } @@ -433,8 +460,7 @@ namespace Barotrauma } } - spriteBatch.Draw(texture, currDrawPosition, - texPerspective, drawColor, rotation, Vector2.Zero, scale, effects, depth ?? this.depth); + drawSection(currDrawPosition, texPerspective); currDrawPosition.Y += texPerspective.Height * scale.Y; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs index 41be59e49..3eb48d01a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs @@ -210,7 +210,7 @@ namespace Barotrauma statusEffect.soundChannel.FadeOutAndDispose(); statusEffect.soundChannel = null; } - else + else if (statusEffect.soundEmitter is { Removed: false }) { statusEffect.soundChannel.Position = new Vector3(statusEffect.soundEmitter.WorldPosition, 0.0f); if (doMuffleCheck && !statusEffect.ignoreMuffling) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs index 264cf243f..e42bf4dd3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs @@ -101,13 +101,36 @@ namespace Barotrauma.Steam { Color = GUIStyle.Green }; + var textShadow = new GUITextBlock(new RectTransform(Vector2.One, itemDownloadProgress.RectTransform) { AbsoluteOffset = new Point(GUI.IntScale(3)) }, "", + textColor: Color.Black, textAlignment: Alignment.Center); + var text = new GUITextBlock(new RectTransform(Vector2.One, itemDownloadProgress.RectTransform), "", + textAlignment: Alignment.Center); var itemDownloadProgressUpdater = new GUICustomComponent( new RectTransform(Vector2.Zero, msgBox.Content.RectTransform), onUpdate: (f, component) => { float progress = 0.0f; - if (item.IsDownloading) { progress = item.DownloadAmount; } - else if (itemDownloadProgress.BarSize > 0.0f) { progress = 1.0f; } + if (item.IsDownloading) + { + progress = item.DownloadAmount; + text.Text = textShadow.Text = TextManager.GetWithVariable( + "PublishPopupDownload", + "[percentage]", + ((int)MathF.Round(item.DownloadAmount * 100)).ToString()); + } + else if (itemDownloadProgress.BarSize > 0.0f) + { + if (!item.IsInstalled && !SteamManager.Workshop.CanBeInstalled(item.Id)) + { + itemDownloadProgress.Color = GUIStyle.Red; + text.Text = textShadow.Text = TextManager.Get("workshopiteminstallfailed"); + } + else + { + text.Text = textShadow.Text = TextManager.Get(item.IsInstalled ? "workshopiteminstalled" : "PublishPopupInstall"); + } + progress = 1.0f; + } itemDownloadProgress.BarSize = Math.Max(itemDownloadProgress.BarSize, MathHelper.Lerp(itemDownloadProgress.BarSize, progress, 0.1f)); @@ -134,9 +157,16 @@ namespace Barotrauma.Steam { foreach (var item in itemsToDownload) { + DebugConsole.Log($"Reinstalling {item.Title}..."); await SteamManager.Workshop.Reinstall(item); - if (!GUIMessageBox.MessageBoxes.Contains(msgBox)) { break; } + DebugConsole.Log($"Finished installing {item.Title}..."); + if (!GUIMessageBox.MessageBoxes.Contains(msgBox)) + { + DebugConsole.Log($"Download prompt closed, interrupting {nameof(DownloadItems)}."); + break; + } } + DebugConsole.Log($"{nameof(DownloadItems)} finished."); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs index 2748b691c..90c86e709 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs @@ -120,6 +120,10 @@ namespace Barotrauma.Steam currentLobby?.SetData("playstyle", serverSettings.PlayStyle.ToString()); currentLobby?.SetData("gamemode", GameMain.NetLobbyScreen?.SelectedMode?.Identifier.Value ?? ""); currentLobby?.SetData("language", serverSettings.Language.ToString()); + if (GameMain.NetLobbyScreen?.SelectedSub != null) + { + currentLobby?.SetData("submarine", GameMain.NetLobbyScreen.SelectedSub.Name); + } DebugConsole.Log("Lobby updated!"); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs index 0159decbd..dd21a0e2a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs @@ -298,7 +298,7 @@ namespace Barotrauma.Steam public static void OnItemDownloadComplete(ulong id, bool forceInstall = false) { - if (!(Screen.Selected is MainMenuScreen) && !forceInstall) + if (Screen.Selected is not MainMenuScreen && !forceInstall) { if (!MainMenuScreen.WorkshopItemsToUpdate.Contains(id)) { @@ -306,13 +306,26 @@ namespace Barotrauma.Steam } return; } - else if (CanBeInstalled(id) - && !ContentPackageManager.WorkshopPackages.Any(p => + else if (!CanBeInstalled(id)) + { + DebugConsole.Log($"Cannot install {id}"); + InstallWaiter.StopWaiting(id); + } + else if (ContentPackageManager.WorkshopPackages.Any(p => p.UgcId.TryUnwrap(out var ugcId) && ugcId is SteamWorkshopId workshopId - && workshopId.Value == id) - && !InstallTaskCounter.IsInstalling(id)) + && workshopId.Value == id)) { + DebugConsole.Log($"Already installed {id}."); + InstallWaiter.StopWaiting(id); + } + else if (InstallTaskCounter.IsInstalling(id)) + { + DebugConsole.Log($"Already installing {id}."); + } + else + { + DebugConsole.Log($"Finished downloading {id}, installing..."); TaskPool.Add($"InstallItem{id}", InstallMod(id), t => InstallWaiter.StopWaiting(id)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs b/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs index 39f998cef..50a3f5f90 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; +using System.Xml.Linq; using Barotrauma.Items.Components; using Microsoft.Xna.Framework; @@ -116,6 +117,9 @@ namespace Barotrauma private readonly bool WasDeleted; private readonly List ContainedItemsCommand = new List(); + // We need to 'snapshot' the state of the circuit box and the best way to do that is to save it to XML. + private readonly List CircuitBoxData = new List(); + /// /// Creates a command where all entities share the same state. /// @@ -143,13 +147,17 @@ namespace Barotrauma List itemsToDelete = new List(); foreach (MapEntity receiver in Receivers) { - if (receiver is Item it) + if (receiver is not Item it) { continue; } + + foreach (var cb in it.GetComponents()) { - foreach (ItemContainer component in it.GetComponents()) - { - if (component.Inventory == null) { continue; } - itemsToDelete.AddRange(component.Inventory.AllItems.Where(item => !item.Removed)); - } + CircuitBoxData.Add(cb.Save(new XElement("root"))); + } + + foreach (ItemContainer component in it.GetComponents()) + { + if (component.Inventory == null) { continue; } + itemsToDelete.AddRange(component.Inventory.AllItems.Where(static item => !item.Removed)); } } @@ -192,34 +200,50 @@ namespace Barotrauma } public override void Execute() - { - var items = DeleteUndelete(true); - ContainedItemsCommand?.ForEach(static cmd => cmd.Execute()); - CircuitBoxWorkaround(items); - } + => Process(true); public override void UnExecute() + => Process(false); + + private void Process(bool redo) { - var items = DeleteUndelete(false); - ContainedItemsCommand?.ForEach(static cmd => cmd.UnExecute()); - CircuitBoxWorkaround(items); + var items = DeleteUndelete(redo); + foreach (var cmd in ContainedItemsCommand) + { + cmd.Process(redo); + } + ApplyCircuitBoxDataIfAny(items); } - // FIXME Temporary workaround for circuit boxes throwing console errors and breaking completely when undoing a deletion - private static void CircuitBoxWorkaround(Option> entitiesOption) + /// + /// We need to manually copy over the circuit box data because of how the undo handles inventory items. + /// The undo system recursively deletes inventory items and creates a separate command for each one. + /// This causes the circuit box to lose its internal inventory when it's cloned and then restored and make it + /// unable to load the state from XML. + /// + /// The workaround to this is to ignore the XML that is being loaded when the item is created and instead + /// save the XML into the command and then load it back after the undo system has restored the items which + /// is what this function does. + /// + private void ApplyCircuitBoxDataIfAny(ImmutableArray items) { - if (!entitiesOption.TryUnwrap(out var entities)) { return; } - - foreach (var entity in entities) + int cbIndex = 0; + foreach (var newItem in items) { - if (entity is not Item it) { continue; } - - if (it.GetComponent() is not null) + foreach (ItemComponent component in newItem.Components) { - foreach (var container in it.GetComponents()) + if (component is not CircuitBox cb) { continue; } + + if (cbIndex < 0 || cbIndex >= CircuitBoxData.Count) { - container.Inventory.DeleteAllItems(); + DebugConsole.ThrowError("Unable to restore wiring in circuit box, index out of range."); + continue; } + + var cbData = CircuitBoxData[cbIndex]; + cbIndex++; + + cb.LoadFromXML(new ContentXElement(null, cbData)); } } } @@ -238,15 +262,19 @@ namespace Barotrauma Receivers.Clear(); PreviousInventories?.Clear(); ContainedItemsCommand?.ForEach(static cmd => cmd.Cleanup()); + CircuitBoxData.Clear(); } - private Option> DeleteUndelete(bool redo) + private ImmutableArray DeleteUndelete(bool redo) { bool wasDeleted = WasDeleted; // We are redoing instead of undoing, flip the behavior if (redo) { wasDeleted = !wasDeleted; } + // collect newly created items so we can update their circuit boxes if any + var builder = ImmutableArray.CreateBuilder(); + if (wasDeleted) { Debug.Assert(Receivers.All(static entity => entity.GetReplacementOrThis().Removed), "Tried to redo a deletion but some items were not deleted"); @@ -260,6 +288,7 @@ namespace Barotrauma if (receiver.GetReplacementOrThis() is Item item && clone is Item cloneItem) { + builder.Add(cloneItem); foreach (ItemComponent ic in item.Components) { int index = item.GetComponentIndex(ic); @@ -303,7 +332,7 @@ namespace Barotrauma clone.Submarine = Submarine.MainSub; } - return Option.Some(clones.ToImmutableArray()); + return builder.ToImmutable(); } else { @@ -316,7 +345,7 @@ namespace Barotrauma } } - return Option.None; + return builder.ToImmutable(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/Quad.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/GraphicsQuad.cs similarity index 98% rename from Barotrauma/BarotraumaClient/ClientSource/Utils/Quad.cs rename to Barotrauma/BarotraumaClient/ClientSource/Utils/GraphicsQuad.cs index d14a9a885..5f2461316 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/Quad.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/GraphicsQuad.cs @@ -6,7 +6,7 @@ using System.Text; namespace Barotrauma { - static class Quad + static class GraphicsQuad { private static VertexBuffer vertexBuffer = null; private static IndexBuffer indexBuffer = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs index f7c0b35fc..063a2e634 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs @@ -35,7 +35,7 @@ namespace Barotrauma Vector2 pos, Rectangle srcRect, Color color, - float rotation, + float rotationRad, Vector2 origin, Vector2 scale, SpriteEffects effects, @@ -55,9 +55,8 @@ namespace Barotrauma (srcRectBottom, srcRectTop) = (srcRectTop, srcRectBottom); } - rotation = MathHelper.ToRadians(rotation); - float sin = (float)Math.Sin(rotation); - float cos = (float)Math.Cos(rotation); + float sin = (float)Math.Sin(rotationRad); + float cos = (float)Math.Cos(rotationRad); var size = srcRect.Size.ToVector2() * scale; @@ -183,11 +182,11 @@ namespace Barotrauma commandList.Add(command); } - public void Draw(Texture2D texture, Vector2 pos, Rectangle? srcRect, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float depth) + public void Draw(Texture2D texture, Vector2 pos, Rectangle? srcRect, Color color, float rotationRad, Vector2 origin, Vector2 scale, SpriteEffects effects, float depth) { if (isDisposed) { return; } - var command = Command.FromTransform(texture, pos, srcRect ?? texture.Bounds, color, rotation, origin, scale, effects, depth, commandList.Count); + var command = Command.FromTransform(texture, pos, srcRect ?? texture.Bounds, color, rotationRad, origin, scale, effects, depth, commandList.Count); AppendCommand(command); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/TextureLoader.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/TextureLoader.cs index 1fcbab5fb..3ab6a1a90 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/TextureLoader.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/TextureLoader.cs @@ -151,13 +151,13 @@ namespace Barotrauma output[outputOffset + 10] = (byte)((g2_565 << 5) | b2_565); } - public static Texture2D FromFile(string path, bool compress = true, bool mipmap = false) + public static Texture2D FromFile(string path, bool compress = true, bool mipmap = false, ContentPackage contentPackage = null) { using FileStream fileStream = File.OpenRead(path); - return FromStream(fileStream, path, compress, mipmap); + return FromStream(fileStream, path, compress, mipmap, contentPackage); } - public static Texture2D FromStream(System.IO.Stream stream, string path = null, bool compress = true, bool mipmap = false) + public static Texture2D FromStream(System.IO.Stream stream, string path = null, bool compress = true, bool mipmap = false, ContentPackage contentPackage = null) { try { @@ -176,7 +176,8 @@ namespace Barotrauma } else { - DebugConsole.AddWarning($"Could not compress a texture because the dimensions aren't a multiple of 4 (path: {path ?? "null"}, size: {width}x{height})"); + DebugConsole.AddWarning($"Could not compress a texture because the dimensions aren't a multiple of 4 (path: {path ?? "null"}, size: {width}x{height})", + contentPackage); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs index c89d0cbfb..7b13a797d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs @@ -76,7 +76,7 @@ namespace Barotrauma float zoom = (float)texWidth / (float)boundingBox.Width; int texHeight = (int)(zoom * boundingBox.Height); - using Camera cam = new Camera(); + Camera cam = new Camera(); cam.SetResolution(new Point(texWidth, texHeight)); cam.MaxZoom = zoom; cam.MinZoom = zoom * 0.5f; diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index a5b67175d..cba9e2ea5 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.1.19.3 + 1.2.6.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 12fa0df4c..8694f9367 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.1.19.3 + 1.2.6.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index a469892ba..0f7aacf16 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.1.19.3 + 1.2.6.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 7b072552b..e983630ac 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.1.19.3 + 1.2.6.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 16781acf4..56fca5e74 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.1.19.3 + 1.2.6.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs index 0f0014017..fc368dace 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs @@ -80,7 +80,9 @@ namespace Barotrauma { msg.WriteUInt32(Job.Prefab.UintIdentifier); msg.WriteByte((byte)Job.Variant); - foreach (SkillPrefab skillPrefab in Job.Prefab.Skills.OrderBy(s => s.Identifier)) + var skills = Job.Prefab.Skills.OrderBy(s => s.Identifier); + msg.WriteByte((byte)skills.Count()); + foreach (SkillPrefab skillPrefab in skills) { msg.WriteSingle(Job.GetSkill(skillPrefab.Identifier)?.Level ?? 0.0f); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index fd59111ec..aebcc26a5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -34,8 +34,8 @@ namespace Barotrauma { if (!CheatsEnabled && IsCheat) { - NewMessage("Client \"" + client.Name + "\" attempted to use the command \"" + names[0] + "\". Cheats must be enabled using \"enablecheats\" before the command can be used.", Color.Red); - GameMain.Server.SendConsoleMessage("You need to enable cheats using the command \"enablecheats\" before you can use the command \"" + names[0] + "\".", client, Color.Red); + NewMessage("Client \"" + client.Name + "\" attempted to use the command \"" + Names[0] + "\". Cheats must be enabled using \"enablecheats\" before the command can be used.", Color.Red); + GameMain.Server.SendConsoleMessage("You need to enable cheats using the command \"enablecheats\" before you can use the command \"" + Names[0] + "\".", client, Color.Red); #if USE_STEAM NewMessage("Enabling cheats will disable Steam achievements during this play session.", Color.Red); @@ -317,7 +317,7 @@ namespace Barotrauma private static void AssignOnClientRequestExecute(string names, Action onClientRequestExecute) { - var matchingCommand = commands.Find(c => c.names.Intersect(names.Split('|')).Count() > 0); + var matchingCommand = commands.Find(c => c.Names.Intersect(names.Split('|').ToIdentifiers()).Any()); if (matchingCommand == null) { throw new Exception("AssignOnClientRequestExecute failed. Command matching the name(s) \"" + names + "\" not found."); @@ -654,8 +654,10 @@ namespace Barotrauma ShowQuestionPrompt("Console command permissions to grant to \"" + client.Name + "\"? You may enter multiple commands separated with a space, or \"all\" to allow using any console command.", (commandsStr) => { - string[] splitCommands = commandsStr.Split(' '); - bool giveAll = splitCommands.Length > 0 && splitCommands[0].Equals("all", StringComparison.OrdinalIgnoreCase); + Identifier[] splitCommands = commandsStr.Split(' ') + .Select(s => s.Trim()) + .ToIdentifiers().ToArray(); + bool giveAll = splitCommands.Length > 0 && splitCommands[0] == "all"; List grantedCommands = new List(); if (giveAll) @@ -664,13 +666,12 @@ namespace Barotrauma } else { - for (int i = 0; i < splitCommands.Length; i++) + foreach (Identifier command in splitCommands) { - splitCommands[i] = splitCommands[i].Trim().ToLowerInvariant(); - Command matchingCommand = commands.Find(c => c.names.Contains(splitCommands[i])); + Command matchingCommand = commands.Find(c => c.Names.Contains(command)); if (matchingCommand == null) { - ThrowError("Could not find the command \"" + splitCommands[i] + "\"!"); + ThrowError("Could not find the command \"" + command + "\"!"); } else { @@ -688,7 +689,7 @@ namespace Barotrauma } else if (grantedCommands.Count > 0) { - NewMessage("Gave the client \"" + client.Name + "\" the permission to use console commands " + string.Join(", ", grantedCommands.Select(c => c.names[0])) + ".", Color.White); + NewMessage("Gave the client \"" + client.Name + "\" the permission to use console commands " + string.Join(", ", grantedCommands.Select(c => c.Names[0])) + ".", Color.White); } }, args, 1); @@ -717,22 +718,23 @@ namespace Barotrauma ShowQuestionPrompt("Console command permissions to revoke from \"" + client.Name + "\"? You may enter multiple commands separated with a space.", (commandsStr) => { - string[] splitCommands = commandsStr.Split(' '); + Identifier[] splitCommands = commandsStr.Split(' ') + .Select(s => s.Trim()) + .ToIdentifiers().ToArray(); List revokedCommands = new List(); - bool revokeAll = splitCommands.Length > 0 && splitCommands[0].Equals("all", StringComparison.OrdinalIgnoreCase); + bool revokeAll = splitCommands.Length > 0 && splitCommands[0] == "all"; if (revokeAll) { revokedCommands.AddRange(commands); } else { - for (int i = 0; i < splitCommands.Length; i++) + foreach (Identifier command in splitCommands) { - splitCommands[i] = splitCommands[i].Trim().ToLowerInvariant(); - Command matchingCommand = commands.Find(c => c.names.Contains(splitCommands[i])); + Command matchingCommand = commands.Find(c => c.Names.Contains(command)); if (matchingCommand == null) { - ThrowError("Could not find the command \"" + splitCommands[i] + "\"!"); + ThrowError("Could not find the command \"" + command + "\"!"); } else { @@ -749,7 +751,7 @@ namespace Barotrauma } else if (revokedCommands.Any()) { - NewMessage("Revoked \"" + client.Name + "\"'s permission to use the console commands " + string.Join(", ", revokedCommands.Select(c => c.names[0])) + ".", Color.White); + NewMessage("Revoked \"" + client.Name + "\"'s permission to use the console commands " + string.Join(", ", revokedCommands.Select(c => c.Names[0])) + ".", Color.White); } }, args, 1); }); @@ -793,7 +795,7 @@ namespace Barotrauma NewMessage("Permitted console commands:", Color.White); foreach (Command permittedCommand in client.PermittedConsoleCommands) { - NewMessage(" - " + permittedCommand.names[0], Color.White); + NewMessage(" - " + permittedCommand.Names[0], Color.White); } } } @@ -1132,7 +1134,12 @@ namespace Barotrauma createMessage("Traitors:"); foreach (var ev in traitorManager.ActiveEvents) { - createMessage($" - {ev.Traitor.Name}: {ev.TraitorEvent.Prefab.Identifier} ({ev.TraitorEvent.CurrentState})"); + string msg = $" - {ev.TraitorEvent.Prefab.Identifier} ({ev.TraitorEvent.CurrentState}): {ev.Traitor.Name}"; + if (ev.TraitorEvent.SecondaryTraitors.Any()) + { + msg += $" secondary traitors: {string.Join(", ", ev.TraitorEvent.SecondaryTraitors.Select(t => t.Name))}"; + } + createMessage(msg); } } @@ -1156,6 +1163,23 @@ namespace Barotrauma } ); + commands.Add(new Command("debugjobassignment", "debugjobassignment: Shows information about how jobs were assigned for the most recent round.", (string[] args) => + { + if (GameMain.Server == null) { return; } + foreach (var debugMsg in GameMain.Server.JobAssignmentDebugLog) + { + NewMessage(debugMsg, Color.Cyan); + } + })); + AssignOnClientRequestExecute("debugjobassignment", (Client client, Vector2 cursorWorldPos, string[] args) => + { + if (GameMain.Server == null) { return; } + foreach (var debugMsg in GameMain.Server.JobAssignmentDebugLog) + { + GameMain.Server.SendConsoleMessage(debugMsg, client); + } + }); + commands.Add(new Command("setpassword|setserverpassword|password", "setpassword [password]: Changes the password of the server that's being hosted.", (string[] args) => { if (GameMain.Server == null) { return; } @@ -1397,7 +1421,7 @@ namespace Barotrauma if (GameMain.GameSession?.GameMode is MultiPlayerCampaign mpCampaign && GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign) { - MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath); + MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath, client: null); } else { @@ -1432,7 +1456,6 @@ namespace Barotrauma GameMain.Server.PrintSenderTransters(); })); - commands.Add(new Command("forcelocationtypechange", "", (string[] args) => { if (GameMain.Server == null || GameMain.GameSession?.Campaign == null) { return; } @@ -1443,7 +1466,7 @@ namespace Barotrauma return; } - var location = GameMain.GameSession.Campaign.Map.Locations.FirstOrDefault(l => l.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase)); + var location = GameMain.GameSession.Campaign.Map.Locations.FirstOrDefault(l => l.DisplayName.Equals(args[0], StringComparison.OrdinalIgnoreCase)); if (location == null) { ThrowError($"Could not find a location with the name {args[0]}."); @@ -1466,7 +1489,7 @@ namespace Barotrauma return new string[][] { - GameMain.GameSession.Campaign.Map.Locations.Select(l => l.Name).ToArray(), + GameMain.GameSession.Campaign.Map.Locations.Select(l => l.DisplayName.Value).ToArray(), LocationType.Prefabs.Select(lt => lt.Name.Value).ToArray() }; })); @@ -1568,6 +1591,19 @@ namespace Barotrauma GameMain.Server.SendChatMessage(ToolBox.RandomSeed(msgLength), ChatMessageType.Default); } })); + + commands.Add(new Command("multiclienttestmode", "Makes the server assign campaign characters based on the name of the client and the character, as opposed to just checking the account ID or address. Useful for testing the campaign with multiple clients running locally.", (string[] args) => + { + CharacterCampaignData.RequireClientNameMatch = !CharacterCampaignData.RequireClientNameMatch; + if (CharacterCampaignData.RequireClientNameMatch) + { + NewMessage("Enabled RequireClientNameMatch (clients' names must match their campaign character)"); + } + else + { + NewMessage("Disabled RequireClientNameMatch"); + } + })); #endif AssignOnClientRequestExecute( @@ -1751,17 +1787,32 @@ namespace Barotrauma { Submarine.MainSub.SetPosition(Level.Loaded.StartPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height); } - else + else if (args[0].Equals("end", StringComparison.OrdinalIgnoreCase)) { Submarine.MainSub.SetPosition(Level.Loaded.EndPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height); } + else if (args[0].Equals("endoutpost", StringComparison.OrdinalIgnoreCase)) + { + Submarine.MainSub.SetPosition(Level.Loaded.EndExitPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height); + var submarineDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == Submarine.MainSub); + if (Level.Loaded?.EndOutpost == null) + { + NewMessage("Can't teleport the sub to the end outpost (no outpost at the end of the level).", Color.Red); + return; + } + var outpostDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == Level.Loaded.EndOutpost); + if (submarineDockingPort != null && outpostDockingPort != null) + { + submarineDockingPort.Dock(outpostDockingPort); + } + } } ); AssignOnClientRequestExecute("togglecampaignteleport", (Client client, Vector2 cursorWorldPos, string[] args) => { - if (!(GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign)) + if (GameMain.GameSession?.Campaign is not MultiPlayerCampaign mpCampaign) { GameMain.Server.SendConsoleMessage("No campaign active.", client, Color.Red); return; @@ -2171,21 +2222,21 @@ namespace Barotrauma } List grantedCommands = new List(); - string[] splitCommands = args.Skip(1).ToArray(); - bool giveAll = splitCommands.Length > 0 && splitCommands[0].Equals("all", StringComparison.OrdinalIgnoreCase); + Identifier[] splitCommands = args.Skip(1) + .Select(s => s.Trim()).ToIdentifiers().ToArray(); + bool giveAll = splitCommands.Length > 0 && splitCommands[0] == "all"; if (giveAll) { grantedCommands.AddRange(commands); } else { - for (int i = 0; i < splitCommands.Length; i++) + foreach (Identifier command in splitCommands) { - splitCommands[i] = splitCommands[i].Trim().ToLowerInvariant(); - Command matchingCommand = commands.Find(c => c.names.Contains(splitCommands[i])); + Command matchingCommand = commands.Find(c => c.Names.Contains(command)); if (matchingCommand == null) { - GameMain.Server.SendConsoleMessage("Could not find the command \"" + splitCommands[i] + "\"!", senderClient, Color.Red); + GameMain.Server.SendConsoleMessage("Could not find the command \"" + command + "\"!", senderClient, Color.Red); } else { @@ -2204,7 +2255,7 @@ namespace Barotrauma } else if (grantedCommands.Count > 0) { - GameMain.Server.SendConsoleMessage("Gave the client \"" + client.Name + "\" the permission to use console commands " + string.Join(", ", grantedCommands.Select(c => c.names[0])) + ".", senderClient); + GameMain.Server.SendConsoleMessage("Gave the client \"" + client.Name + "\" the permission to use console commands " + string.Join(", ", grantedCommands.Select(c => c.Names[0])) + ".", senderClient); } } ); @@ -2227,21 +2278,21 @@ namespace Barotrauma return; } List revokedCommands = new List(); - string[] splitCommands = args.Skip(1).ToArray(); - bool revokeAll = splitCommands.Length > 0 && splitCommands[0].Equals("all", StringComparison.OrdinalIgnoreCase); + Identifier[] splitCommands = args.Skip(1) + .Select(s => s.Trim()).ToIdentifiers().ToArray(); + bool revokeAll = splitCommands.Length > 0 && splitCommands[0] == "all"; if (revokeAll) { revokedCommands.AddRange(commands); } else { - for (int i = 0; i < splitCommands.Length; i++) + foreach (Identifier command in splitCommands) { - splitCommands[i] = splitCommands[i].Trim().ToLowerInvariant(); - Command matchingCommand = commands.Find(c => c.names.Contains(splitCommands[i])); + Command matchingCommand = commands.Find(c => c.Names.Contains(command)); if (matchingCommand == null) { - GameMain.Server.SendConsoleMessage("Could not find the command \"" + splitCommands[i] + "\"!", senderClient, Color.Red); + GameMain.Server.SendConsoleMessage("Could not find the command \"" + command + "\"!", senderClient, Color.Red); } else { @@ -2256,14 +2307,14 @@ namespace Barotrauma client.RemovePermission(ClientPermissions.ConsoleCommands); } GameMain.Server.UpdateClientPermissions(client); - GameMain.Server.SendConsoleMessage("Revoked \"" + client.Name + "\"'s permission to use the console commands " + string.Join(", ", revokedCommands.Select(c => c.names[0])) + ".", senderClient); + GameMain.Server.SendConsoleMessage("Revoked \"" + client.Name + "\"'s permission to use the console commands " + string.Join(", ", revokedCommands.Select(c => c.Names[0])) + ".", senderClient); if (revokeAll) { GameMain.Server.SendConsoleMessage("Revoked \"" + client.Name + "\"'s permission to use console commands.", senderClient); } else if (revokedCommands.Count > 0) { - GameMain.Server.SendConsoleMessage("Revoked \"" + client.Name + "\"'s permission to use the console commands " + string.Join(", ", revokedCommands.Select(c => c.names[0])) + ".", senderClient); + GameMain.Server.SendConsoleMessage("Revoked \"" + client.Name + "\"'s permission to use the console commands " + string.Join(", ", revokedCommands.Select(c => c.Names[0])) + ".", senderClient); } } ); @@ -2308,7 +2359,7 @@ namespace Barotrauma GameMain.Server.SendConsoleMessage("Permitted console commands:", senderClient); foreach (Command permittedCommand in client.PermittedConsoleCommands) { - GameMain.Server.SendConsoleMessage(" - " + permittedCommand.names[0], senderClient); + GameMain.Server.SendConsoleMessage(" - " + permittedCommand.Names[0], senderClient); } } } @@ -2411,7 +2462,7 @@ namespace Barotrauma } Location location = campaign.Map.CurrentLocation.Connections[destinationIndex].OtherLocation(campaign.Map.CurrentLocation); campaign.Map.SelectLocation(location); - GameMain.Server.SendConsoleMessage(location.Name + " selected.", senderClient); + GameMain.Server.SendConsoleMessage($"{location.DisplayName.Value} selected.", senderClient); } ); @@ -2585,10 +2636,10 @@ namespace Barotrauma } string[] splitCommand = ToolBox.SplitCommand(command); - Command matchingCommand = commands.Find(c => c.names.Contains(splitCommand[0].ToLowerInvariant())); + Command matchingCommand = commands.Find(c => c.Names.Contains(splitCommand[0].ToIdentifier())); if (matchingCommand != null && !client.PermittedConsoleCommands.Contains(matchingCommand) && client.Connection != GameMain.Server.OwnerConnection) { - GameMain.Server.SendConsoleMessage("You are not permitted to use the command\"" + matchingCommand.names[0] + "\"!", client, Color.Red); + GameMain.Server.SendConsoleMessage("You are not permitted to use the command\"" + matchingCommand.Names[0] + "\"!", client, Color.Red); GameServer.Log(GameServer.ClientLogName(client) + " attempted to execute the console command \"" + command + "\" without a permission to use the command.", ServerLog.MessageType.ConsoleUsage); return; } @@ -2612,14 +2663,14 @@ namespace Barotrauma } catch (Exception e) { - ThrowError("Executing the command \"" + matchingCommand.names[0] + "\" by request from \"" + GameServer.ClientLogName(client) + "\" failed.", e); + ThrowError("Executing the command \"" + matchingCommand.Names[0] + "\" by request from \"" + GameServer.ClientLogName(client) + "\" failed.", e); } } static partial void ShowHelpMessage(Command command) { - NewMessage(command.names[0], Color.Cyan); - NewMessage(command.help, Color.Gray); + NewMessage(command.Names[0].Value, Color.Cyan); + NewMessage(command.Help, Color.Gray); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventLogAction.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventLogAction.cs index 976984e76..86d0297f6 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventLogAction.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventLogAction.cs @@ -27,10 +27,11 @@ partial class EventLogAction : EventAction } else { - DebugConsole.AddWarning($"{target} is not a valid target for an EventLogAction. The target should be a character."); + DebugConsole.AddWarning($"{target} is not a valid target for an EventLogAction. The target should be a character.", + ParentEvent.Prefab.ContentPackage); } } - if (eventLog.TryAddEntry(ParentEvent.Prefab.Identifier, Id, displayText, targetClients) && ShowInServerLog) + if (eventLog!.TryAddEntry(ParentEvent.Prefab.Identifier, Id, displayText, targetClients) && ShowInServerLog) { Log(targetClients); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/HighlightAction.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/HighlightAction.cs new file mode 100644 index 000000000..98c06c3f9 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/HighlightAction.cs @@ -0,0 +1,24 @@ +#nullable enable +using Barotrauma.Networking; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma; + +partial class HighlightAction : EventAction +{ + partial void SetHighlightProjSpecific(Entity entity, IEnumerable? targetCharacters) + { + if (entity is Item item && GameMain.Server != null) + { + IEnumerable? targetClients = null; + if (targetCharacters != null) + { + targetClients = targetCharacters + .Select(c => GameMain.Server.ConnectedClients.FirstOrDefault(client => client.Character == c)) + .Where(c => c != null)!; + } + GameMain.Server?.CreateEntityEvent(item, new Item.SetHighlightEventData(State, highlightColor, targetClients)); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs index 64f8f99b6..456037115 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs @@ -97,7 +97,13 @@ namespace Barotrauma void GiveMissionExperience(CharacterInfo info) { if (info == null) { return; } - var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f); + var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f, info.Character); + //check if anyone else in the crew has talents that could give a bonus to this one + foreach (var c in crew) + { + if (c == info.Character) { continue; } + c.CheckTalents(AbilityEffectType.OnAllyGainMissionExperience, experienceGainMultiplierIndividual); + } info.Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual); int finalExperienceGain = (int)(experienceGain * experienceGainMultiplierIndividual.Value); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index d21e80fbd..7b2abfa96 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -118,26 +118,13 @@ namespace Barotrauma private void CheckContentPackage() { - //TODO: reimplement using only core package? - /*foreach (ContentPackage contentPackage in Config.AllEnabledPackages) + if (Version < VanillaContent.GameVersion) { - var exePaths = contentPackage.GetFilesOfType(ContentType.ServerExecutable); - if (exePaths.Count() > 0 && AppDomain.CurrentDomain.FriendlyName != exePaths.First()) - { - DebugConsole.NewMessage(AppDomain.CurrentDomain.FriendlyName); - DebugConsole.ShowQuestionPrompt(TextManager.GetWithVariables("IncorrectExe", new string[2] { "[selectedpackage]", "[exename]" }, new string[2] { contentPackage.Name, exePaths.First() }), - (option) => - { - if (option.ToLower() == "y" || option.ToLower() == "yes") - { - string fullPath = Path.GetFullPath(exePaths.First()); - ToolBox.OpenFileWithShell(fullPath); - ShouldRun = false; - } - }); - break; - } - }*/ + DebugConsole.ThrowError( + TextManager.GetWithVariables("versionmismatchwarning", + ("[gameversion]", Version.ToString()), + ("[contentversion]", VanillaContent.GameVersion.ToString()))); + } } public void StartServer() diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs index 79597042e..f3737955a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs @@ -2,27 +2,12 @@ using System.Collections.Generic; using System.Linq; using Barotrauma.Networking; +using System.Text; namespace Barotrauma { partial class CargoManager { - public void SellBackPurchasedItems(Identifier storeIdentifier, List itemsToSell, Client client) - { - // Check all the prices before starting the transaction to make sure the modifiers stay the same for the whole transaction - var buyValues = GetBuyValuesAtCurrentLocation(storeIdentifier, itemsToSell.Select(i => i.ItemPrefab)); - var store = Location.GetStore(storeIdentifier); - if (store == null) { return; } - var storeSpecificItems = GetPurchasedItems(storeIdentifier); - foreach (var item in itemsToSell) - { - var itemValue = item.Quantity * buyValues[item.ItemPrefab]; - store.Balance -= itemValue; - campaign.GetWallet(client).Give(itemValue); - storeSpecificItems?.Remove(item); - } - } - public void BuyBackSoldItems(Identifier storeIdentifier, List itemsToBuy, Client client) { var store = Location.GetStore(storeIdentifier); @@ -80,6 +65,21 @@ namespace Barotrauma OnSoldItemsChanged?.Invoke(this); } + public void LogNewItemPurchases(Identifier storeIdentifier, List newItems, Client client) + { + StringBuilder sb = new StringBuilder(); + int price = 0; + Dictionary buyValues = GetBuyValuesAtCurrentLocation(storeIdentifier, newItems.Select(i => i.ItemPrefab)); + foreach (PurchasedItem item in newItems) + { + int itemValue = item.Quantity * buyValues[item.ItemPrefab]; + GameAnalyticsManager.AddMoneySpentEvent(itemValue, GameAnalyticsManager.MoneySink.Store, item.ItemPrefab.Identifier.Value); + sb.Append($"\n - {item.ItemPrefab.Name} x{item.Quantity}"); + price += itemValue; + } + GameServer.Log($"{NetworkMember.ClientLogName(client, client?.Name ?? "Unknown")} purchased {newItems.Count} item(s) for {TextManager.FormatCurrency(price)}{sb.ToString()}", ServerLog.MessageType.Money); + } + public void ClearSoldItemsProjSpecific() { SoldItems.Clear(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs index f3d53216a..7aa80a84e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs @@ -1,5 +1,4 @@ -using Barotrauma.Extensions; -using Barotrauma.Networking; +using Barotrauma.Networking; namespace Barotrauma { @@ -27,6 +26,15 @@ namespace Barotrauma AnyOneAllowedToManageCampaign(permissions); } + public static bool AllowImmediateItemDelivery(Client client) + { + if (client == null || GameMain.Server == null) { return false; } + return + GameMain.Server.ServerSettings.AllowImmediateItemDelivery || + client.HasPermission(ClientPermissions.ManageCampaign) || + client.Connection == GameMain.Server.OwnerConnection; + } + public static bool AllowedToManageWallets(Client client) { return AllowedToManageCampaign(client, ClientPermissions.ManageMoney); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs index ddc0ae1d5..5b2daa59b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs @@ -6,6 +6,14 @@ namespace Barotrauma { partial class CharacterCampaignData { +#if DEBUG + /// + /// If enabled, client names must match the name of the character. Useful for testing the campaign with multiple clients running locally: + /// without this, the clients would all get assigned the same character due to all of them having the same AccountId or Address. + /// + public static bool RequireClientNameMatch = false; +#endif + public bool HasSpawned; public bool HasItemData @@ -76,7 +84,7 @@ namespace Barotrauma { case "character": case "characterinfo": - CharacterInfo = new CharacterInfo(subElement); + CharacterInfo = new CharacterInfo(new ContentXElement(contentPackage: null, subElement)); break; case "inventory": itemData = subElement; @@ -103,6 +111,12 @@ namespace Barotrauma } else { +#if DEBUG + if (RequireClientNameMatch) + { + return ClientAddress == client.Connection.Endpoint.Address && client.Name == Name; + } +#endif return ClientAddress == client.Connection.Endpoint.Address; } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index e1b3de615..0081b16f2 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -120,28 +120,41 @@ namespace Barotrauma SaveUtil.SaveGame(GameMain.GameSession.SavePath); DebugConsole.NewMessage("Campaign started!", Color.Cyan); - DebugConsole.NewMessage("Current location: " + GameMain.GameSession.Map.CurrentLocation.Name, Color.Cyan); + DebugConsole.NewMessage("Current location: " + GameMain.GameSession.Map.CurrentLocation.DisplayName, Color.Cyan); ((MultiPlayerCampaign)GameMain.GameSession.GameMode).LoadInitialLevel(); } - public static void LoadCampaign(string selectedSave) + public static void LoadCampaign(string selectedSave, Client client) { GameMain.NetLobbyScreen.ToggleCampaignMode(true); - SaveUtil.LoadGame(selectedSave); - if (GameMain.GameSession.GameMode is MultiPlayerCampaign mpCampaign) + try { - mpCampaign.LastSaveID++; + SaveUtil.LoadGame(selectedSave); + if (GameMain.GameSession.GameMode is MultiPlayerCampaign mpCampaign) + { + mpCampaign.LastSaveID++; + } + else + { + DebugConsole.ThrowError("Failed to load a campaign. Unexpected game mode: " + GameMain.GameSession.GameMode ?? "none"); + return; + } } - else + catch (Exception e) { - DebugConsole.ThrowError("Unexpected game mode: " + GameMain.GameSession.GameMode); + string errorMsg = $"Error while loading the save {selectedSave}"; + if (client != null) + { + GameMain.Server?.SendDirectChatMessage($"{errorMsg}: {e.Message}\n{e.StackTrace}", client, ChatMessageType.Error); + } + DebugConsole.ThrowError(errorMsg, e); return; } DebugConsole.NewMessage("Campaign loaded!", Color.Cyan); DebugConsole.NewMessage( GameMain.GameSession.Map.SelectedLocation == null ? - GameMain.GameSession.Map.CurrentLocation.Name : - GameMain.GameSession.Map.CurrentLocation.Name + " -> " + GameMain.GameSession.Map.SelectedLocation.Name, Color.Cyan); + GameMain.GameSession.Map.CurrentLocation.DisplayName : + GameMain.GameSession.Map.CurrentLocation.DisplayName + " -> " + GameMain.GameSession.Map.SelectedLocation.DisplayName, Color.Cyan); } protected override void LoadInitialLevel() @@ -188,7 +201,14 @@ namespace Barotrauma } else { - LoadCampaign(saveFiles[saveIndex].FilePath); + try + { + LoadCampaign(saveFiles[saveIndex].FilePath, client: null); + } + catch (Exception ex) + { + DebugConsole.ThrowError("Failed to load the campaign.", ex); + } } }); } @@ -377,7 +397,7 @@ namespace Barotrauma { PendingSubmarineSwitch = null; GameMain.Server.EndGame(TransitionType.None, wasSaved: false); - LoadCampaign(GameMain.GameSession.SavePath); + LoadCampaign(GameMain.GameSession.SavePath, client: null); LastSaveID++; IncrementAllLastUpdateIds(); yield return CoroutineStatus.Success; @@ -806,7 +826,7 @@ namespace Barotrauma UInt16 itemToRemoveID = msg.ReadUInt16(); Identifier itemToInstallIdentifier = msg.ReadIdentifier(); ItemPrefab itemToInstall = itemToInstallIdentifier.IsEmpty ? null : ItemPrefab.Find(string.Empty, itemToInstallIdentifier); - if (!(Entity.FindEntityByID(itemToRemoveID) is Item itemToRemove)) { continue; } + if (Entity.FindEntityByID(itemToRemoveID) is not Item itemToRemove) { continue; } purchasedItemSwaps.Add(new PurchasedItemSwap(itemToRemove, itemToInstall)); } @@ -894,7 +914,7 @@ namespace Barotrauma int availableQuantity = map.CurrentLocation.Stores[store.Key].Stock.Find(s => s.ItemPrefab == item.ItemPrefab)?.Quantity ?? 0; int alreadyPurchasedQuantity = CargoManager.GetBuyCrateItem(store.Key, item.ItemPrefab)?.Quantity ?? 0 + - CargoManager.GetPurchasedItem(store.Key, item.ItemPrefab)?.Quantity ?? 0; + CargoManager.GetPurchasedItemCount(store.Key, item.ItemPrefab); item.Quantity = MathHelper.Clamp(item.Quantity, 0, availableQuantity - alreadyPurchasedQuantity); CargoManager.ModifyItemQuantityInBuyCrate(store.Key, item.ItemPrefab, item.Quantity, sender); } @@ -905,9 +925,41 @@ namespace Barotrauma { prevPurchasedItems.Add(kvp.Key, new List(kvp.Value)); } - foreach (var kvp in prevPurchasedItems) + + foreach (var storeId in purchasedItems.Keys) { - CargoManager.SellBackPurchasedItems(kvp.Key, kvp.Value, sender); + DebugConsole.Log($"Purchased items ({storeId}):\n"); + if (prevPurchasedItems.TryGetValue(storeId, out var alreadyPurchased)) + { + var delivered = alreadyPurchased.Where(it => it.Delivered); + var notDelivered = alreadyPurchased.Where(it => !it.Delivered); + if (delivered.Any()) + { + DebugConsole.Log($" Already delivered:\n" + string.Concat(delivered.Select(it => $" - {it.ItemPrefab.Name} (x{it.Quantity})"))); + } + if (notDelivered.Any()) + { + DebugConsole.Log($" Already purchased:\n" + string.Concat(notDelivered.Where(it => !it.Delivered).Select(it => $" - {it.ItemPrefab.Name} (x{it.Quantity})"))); + } + } + DebugConsole.Log($" New purchases:"); + foreach (var purchasedItem in purchasedItems[storeId]) + { + if (purchasedItem.Delivered) { continue; } + int quantity = purchasedItem.Quantity; + if (alreadyPurchased != null) + { + quantity -= alreadyPurchased.Where(it => it.DeliverImmediately == purchasedItem.DeliverImmediately && it.ItemPrefab == purchasedItem.ItemPrefab).Sum(it => it.Quantity); + } + if (quantity > 0) + { + DebugConsole.Log($" - {purchasedItem.ItemPrefab.Name} (x{quantity})"); + } + } + } + foreach (var storeId in soldItems.Keys) + { + DebugConsole.Log($"Sold items:\n" + string.Concat(soldItems[storeId].Select(it => $" - {it.ItemPrefab.Name}"))); } foreach (var kvp in purchasedItems) @@ -916,17 +968,23 @@ namespace Barotrauma var purchasedItemList = kvp.Value; foreach (var purchasedItem in purchasedItemList) { + int desiredQuantity = purchasedItem.Quantity; + if (prevPurchasedItems.TryGetValue(storeId, out var alreadyPurchasedList) && + alreadyPurchasedList.FirstOrDefault(p => p.ItemPrefab == purchasedItem.ItemPrefab) is { } alreadyPurchased) + { + desiredQuantity -= alreadyPurchased.Quantity; + } int availableQuantity = map.CurrentLocation.Stores[storeId].Stock.Find(s => s.ItemPrefab == purchasedItem.ItemPrefab)?.Quantity ?? 0; - purchasedItem.Quantity = Math.Min(purchasedItem.Quantity, availableQuantity); - } - CargoManager.PurchaseItems(storeId, purchasedItemList, false, sender); + purchasedItem.Quantity = Math.Min(desiredQuantity, availableQuantity); + } + CargoManager.PurchaseItems(storeId, purchasedItemList, removeFromCrate: false, client: sender); } foreach (var (storeIdentifier, items) in CargoManager.PurchasedItems) { if (!prevPurchasedItems.ContainsKey(storeIdentifier)) { - CargoManager.OnNewItemsPurchased(storeIdentifier, items, sender); + CargoManager.LogNewItemPurchases(storeIdentifier, items, sender); continue; } @@ -941,7 +999,6 @@ namespace Barotrauma newItems.Add(item); continue; } - if (matching.Quantity < item.Quantity) { newItems.Add(new PurchasedItem(item.ItemPrefab, item.Quantity - matching.Quantity, sender)); @@ -950,7 +1007,7 @@ namespace Barotrauma if (newItems.Any()) { - CargoManager.OnNewItemsPurchased(storeIdentifier, newItems, sender); + CargoManager.LogNewItemPurchases(storeIdentifier, newItems, sender); } } @@ -1015,7 +1072,7 @@ namespace Barotrauma UpgradeManager.PurchaseUpgrade(prefab, category, client: sender); // unstable logging - int price = prefab.Price.GetBuyPrice(UpgradeManager.GetUpgradeLevel(prefab, category), Map?.CurrentLocation, characterList); + int price = prefab.Price.GetBuyPrice(prefab, UpgradeManager.GetUpgradeLevel(prefab, category), Map?.CurrentLocation, characterList); int level = UpgradeManager.GetUpgradeLevel(prefab, category); GameServer.Log($"SERVER: Purchased level {level} {category.Identifier}.{prefab.Identifier} for {price}", ServerLog.MessageType.ServerMessage); } @@ -1194,13 +1251,13 @@ namespace Barotrauma { foreach (CharacterInfo hireInfo in location.HireManager.PendingHires) { - if (TryHireCharacter(location, hireInfo, sender)) + if (TryHireCharacter(location, hireInfo, sender.Character, sender)) { hiredCharacters.Add(hireInfo); - }; + } } } - + if (updatePending) { List pendingHireInfos = new List(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs index c961063c5..b0b24e75c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs @@ -304,7 +304,8 @@ namespace Barotrauma.Items.Components private void ThrowError(string message, Client c) { - DebugConsole.ThrowError(message); + DebugConsole.ThrowError(message, + contentPackage: item.Prefab.ContentPackage); SendToClient(CircuitBoxOpcode.Error, new CircuitBoxErrorEvent(message), c); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs index a063286d5..0ce5b7ab5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs @@ -66,7 +66,7 @@ namespace Barotrauma { foreach (Item item in slots[i].Items.ToList()) { - if (!receivedItemIdsFromClient[i].Contains(item.ID)) + if (!receivedItemIdsFromClient[i].Contains(item.ID) && item.IsInteractable(c.Character)) { Item droppedItem = item; Entity prevOwner = Owner; @@ -107,10 +107,10 @@ namespace Barotrauma 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()) + (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"})"); + 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; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index f59311120..62c2477a1 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -161,6 +161,20 @@ namespace Barotrauma msg.WriteUInt16(droppedItem.ID); } break; + case SetHighlightEventData highlightEventData: + bool isTargetedForClient = + highlightEventData.TargetClients.IsEmpty || + highlightEventData.TargetClients.Contains(c); + msg.WriteBoolean(isTargetedForClient); + if (isTargetedForClient) + { + msg.WriteBoolean(highlightEventData.Highlighted); + if (highlightEventData.Highlighted) + { + msg.WriteColorR8G8B8A8(highlightEventData.Color); + } + } + break; default: throw error($"Unsupported event type {itemEventData.GetType().Name}"); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/ItemEventData.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/ItemEventData.cs index b2a6b0dda..a37508ccf 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/ItemEventData.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/ItemEventData.cs @@ -1,4 +1,7 @@ -using System.Collections.Generic; +#nullable enable +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -16,4 +19,20 @@ partial class Item Items = items.Distinct().ToImmutableArray(); } } + + public readonly struct SetHighlightEventData : IEventData + { + public EventType EventType => EventType.SetHighlight; + public readonly bool Highlighted; + public readonly Color Color; + + public readonly ImmutableArray TargetClients; + + public SetHighlightEventData(bool highlighted, Color color, IEnumerable? targetClients) + { + Highlighted = highlighted; + Color = color; + TargetClients = (targetClients ?? Enumerable.Empty()).ToImmutableArray(); + } + } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs index bdd772286..814152a26 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs @@ -124,7 +124,7 @@ namespace Barotrauma out NetworkFireSource[] newFireSources); if (!c.HasPermission(ClientPermissions.ConsoleCommands) || - !c.PermittedConsoleCommands.Any(command => command.names.Contains("fire") || command.names.Contains("editfire"))) + !c.PermittedConsoleCommands.Any(command => command.Names.Contains("fire".ToIdentifier()) || command.Names.Contains("editfire".ToIdentifier()))) { return; } @@ -138,7 +138,7 @@ namespace Barotrauma var newFire = i < FireSources.Count ? FireSources[i] : - new FireSource(Submarine == null ? pos : pos + Submarine.Position, null, true); + new FireSource(Submarine == null ? pos : pos + Submarine.Position, sourceCharacter: null, isNetworkMessage: true); newFire.Position = pos; newFire.Size = new Vector2(size, newFire.Size.Y); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index c08936ba3..cede18f2d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -254,6 +254,7 @@ namespace Barotrauma.Networking private void OnInitializationComplete(NetworkConnection connection, string clientName) { + clientName = Client.SanitizeName(clientName); Client newClient = new Client(clientName, GetNewClientSessionId()); newClient.InitClientSync(); newClient.Connection = connection; @@ -804,7 +805,7 @@ namespace Barotrauma.Networking { using (dosProtection.Pause(connectedClient)) { - MultiPlayerCampaign.LoadCampaign(saveName); + MultiPlayerCampaign.LoadCampaign(saveName, connectedClient); } } } @@ -1230,11 +1231,6 @@ namespace Barotrauma.Networking } c.LastRecvEntityEventID = lastRecvEntityEventID; - #warning TODO: remove this later - /*if (!CoroutineManager.IsCoroutineRunning("RoundRestartLoop")) - { - CoroutineManager.StartCoroutine(RoundRestartLoop(), "RoundRestartLoop"); - }*/ } else if (lastRecvEntityEventID != c.LastRecvEntityEventID && GameSettings.CurrentConfig.VerboseLogging) { @@ -1484,7 +1480,7 @@ namespace Barotrauma.Networking { using (dosProtection.Pause(sender)) { - MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath); + MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath, sender); } } } @@ -2538,7 +2534,7 @@ namespace Barotrauma.Networking { spawnList.Add(new PurchasedItem(kvp.Key, kvp.Value, buyer: null)); } - CargoManager.CreateItems(spawnList, sub, cargoManager: null); + CargoManager.DeliverItemsToSub(spawnList, sub, cargoManager: null); } } @@ -2585,6 +2581,7 @@ namespace Barotrauma.Networking msg.WriteBoolean(ServerSettings.AllowRespawn && missionAllowRespawn); msg.WriteBoolean(ServerSettings.AllowDisguises); msg.WriteBoolean(ServerSettings.AllowRewiring); + msg.WriteBoolean(ServerSettings.AllowImmediateItemDelivery); msg.WriteBoolean(ServerSettings.AllowFriendlyFire); msg.WriteBoolean(ServerSettings.LockAllDefaultWires); msg.WriteBoolean(ServerSettings.AllowLinkingWifiToChat); @@ -3710,8 +3707,12 @@ namespace Barotrauma.Networking } } + public readonly List JobAssignmentDebugLog = new List(); + public void AssignJobs(List unassigned) { + JobAssignmentDebugLog.Clear(); + var jobList = JobPrefab.Prefabs.ToList(); unassigned = new List(unassigned); unassigned = unassigned.OrderBy(sp => Rand.Int(int.MaxValue)).ToList(); @@ -3733,10 +3734,11 @@ namespace Barotrauma.Networking //remove already assigned clients from unassigned unassigned.RemoveAll(u => campaignAssigned.ContainsKey(u)); //add up to assigned client count - foreach (KeyValuePair clientJob in campaignAssigned) + foreach ((Client client, Job job) in campaignAssigned) { - assignedClientCount[clientJob.Value.Prefab]++; - clientJob.Key.AssignedJob = new JobVariant(clientJob.Value.Prefab, clientJob.Value.Variant); + assignedClientCount[job.Prefab]++; + client.AssignedJob = new JobVariant(job.Prefab, job.Variant); + JobAssignmentDebugLog.Add($"Client {client.Name} has an existing campaign character, keeping the job {job.Name}."); } } @@ -3755,6 +3757,7 @@ namespace Barotrauma.Networking { if (unassigned[i].JobPreferences.Count == 0) { continue; } if (!unassigned[i].JobPreferences.Any() || !unassigned[i].JobPreferences[0].Prefab.AllowAlways) { continue; } + JobAssignmentDebugLog.Add($"Client {unassigned[i].Name} has {unassigned[i].JobPreferences[0].Prefab.Name} as their first preference, assigning it because the job is always allowed."); unassigned[i].AssignedJob = unassigned[i].JobPreferences[0]; unassigned.RemoveAt(i); } @@ -3773,6 +3776,7 @@ namespace Barotrauma.Networking Client client = FindClientWithJobPreference(unassigned, jobPrefab, forceAssign: false); if (client != null) { + JobAssignmentDebugLog.Add($"At least {jobPrefab.MinNumber} {jobPrefab.Name} required. Assigning {client.Name} as a {jobPrefab.Name} (has the job in their preferences)."); AssignJob(client, jobPrefab); } } @@ -3784,7 +3788,11 @@ namespace Barotrauma.Networking { if (unassigned.Count == 0) { break; } if (jobPrefab.MinNumber < 1 || assignedClientCount[jobPrefab] >= jobPrefab.MinNumber) { continue; } - AssignJob(FindClientWithJobPreference(unassigned, jobPrefab, forceAssign: true), jobPrefab); + var client = FindClientWithJobPreference(unassigned, jobPrefab, forceAssign: true); + JobAssignmentDebugLog.Add( + $"At least {jobPrefab.MinNumber} {jobPrefab.Name} required. "+ + $"A random client needs to be assigned because no one has the job in their preferences. Assigning {client.Name} as a {jobPrefab.Name}."); + AssignJob(client, jobPrefab); } } @@ -3802,32 +3810,6 @@ namespace Barotrauma.Networking } } - List availableSpawnPoints = WayPoint.WayPointList.FindAll(wp => - wp.SpawnType == SpawnType.Human && - wp.Submarine != null && wp.Submarine.TeamID == teamID); - - /*bool canAssign = false; - do - { - canAssign = false; - foreach (WayPoint spawnPoint in unassignedSpawnPoints) - { - if (unassigned.Count == 0) { break; } - - JobPrefab job = spawnPoint.AssignedJob ?? JobPrefab.List.Values.GetRandom(); - if (assignedClientCount[job] >= job.MaxNumber) { continue; } - - Client assignedClient = FindClientWithJobPreference(unassigned, job, true); - if (assignedClient != null) - { - assignedClient.AssignedJob = job; - assignedClientCount[job]++; - unassigned.Remove(assignedClient); - canAssign = true; - } - } - } while (unassigned.Count > 0 && canAssign);*/ - // Attempt to give the clients a job they have in their job preferences. // First evaluate all the primary preferences, then all the secondary etc. for (int preferenceIndex = 0; preferenceIndex < 3; preferenceIndex++) @@ -3838,12 +3820,17 @@ namespace Barotrauma.Networking if (preferenceIndex >= client.JobPreferences.Count) { continue; } var preferredJob = client.JobPreferences[preferenceIndex]; JobPrefab jobPrefab = preferredJob.Prefab; - if (assignedClientCount[jobPrefab] >= jobPrefab.MaxNumber || client.Karma < jobPrefab.MinKarma) + if (assignedClientCount[jobPrefab] >= jobPrefab.MaxNumber) { - //can't assign this job if maximum number has reached or the clien't karma is too low + JobAssignmentDebugLog.Add($"{client.Name} has {jobPrefab.Name} as their {preferenceIndex + 1}. preference. Cannot assign, maximum number of the job has been reached."); continue; } - + if (client.Karma < jobPrefab.MinKarma) + { + JobAssignmentDebugLog.Add($"{client.Name} has {jobPrefab.Name} as their {preferenceIndex + 1}. preference. Cannot assign, karma too low ({client.Karma} < {jobPrefab.MinKarma})."); + continue; + } + JobAssignmentDebugLog.Add($"{client.Name} has {jobPrefab.Name} as their {preferenceIndex + 1}. preference. Assigning {client.Name} as a {jobPrefab.Name}."); client.AssignedJob = preferredJob; assignedClientCount[jobPrefab]++; unassigned.RemoveAt(i); @@ -3859,7 +3846,9 @@ namespace Barotrauma.Networking //all jobs taken, give a random job if (remainingJobs.Count == 0) { - DebugConsole.ThrowError("Failed to assign a suitable job for \"" + c.Name + "\" (all jobs already have the maximum numbers of players). Assigning a random job..."); + string errorMsg = $"Failed to assign a suitable job for \"{c.Name}\" (all jobs already have the maximum numbers of players). Assigning a random job..."; + DebugConsole.ThrowError(errorMsg); + JobAssignmentDebugLog.Add(errorMsg); int jobIndex = Rand.Range(0, jobList.Count); int skips = 0; while (c.Karma < jobList[jobIndex].MinKarma) @@ -3875,19 +3864,20 @@ namespace Barotrauma.Networking assignedClientCount[c.AssignedJob.Prefab]++; } //if one of the client's preferences is still available, give them that job - else if (c.JobPreferences.Any(jp => remainingJobs.Contains(jp.Prefab))) + else if (c.JobPreferences.FirstOrDefault(jp => remainingJobs.Contains(jp.Prefab)) is { } remainingJob) { - foreach (JobVariant preferredJob in c.JobPreferences) - { - c.AssignedJob = preferredJob; - assignedClientCount[preferredJob.Prefab]++; - break; - } + JobAssignmentDebugLog.Add( + $"{c.Name} has {remainingJob.Prefab.Name} as their {c.JobPreferences.IndexOf(remainingJob) + 1}. preference, and it is still available."+ + $" Assigning {c.Name} as a {remainingJob.Prefab.Name}."); + c.AssignedJob = remainingJob; + assignedClientCount[remainingJob.Prefab]++; } else //none of the client's preferred jobs available, choose a random job { c.AssignedJob = new JobVariant(remainingJobs[Rand.Range(0, remainingJobs.Count)], 0); assignedClientCount[c.AssignedJob.Prefab]++; + JobAssignmentDebugLog.Add( + $"No suitable jobs available for {c.Name} (karma {c.Karma}). Assigning a random job: {c.AssignedJob.Prefab.Name}."); } } } @@ -3951,7 +3941,6 @@ namespace Barotrauma.Networking 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..."); - #warning TODO: is this randsync correct? c.Job = Job.Random(Rand.RandSync.ServerAndClient); assignedPlayerCount[c.Job.Prefab]++; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs index 6bf35ee39..cf6595515 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs @@ -426,8 +426,9 @@ namespace Barotrauma.Networking } else { - double midRoundSyncTimeOut = uniqueEvents.Count / 100 * server.UpdateInterval.TotalSeconds; - midRoundSyncTimeOut = Math.Max(10.0f, midRoundSyncTimeOut * 10.0f); + //assume we can get at least 10 events per second through + double midRoundSyncTimeOut = uniqueEvents.Count / 10 * server.UpdateInterval.TotalSeconds; + midRoundSyncTimeOut = Math.Max(midRoundSyncTimeOut, server.ServerSettings.MinimumMidRoundSyncTimeout); client.UnreceivedEntityEventCount = (UInt16)uniqueEvents.Count; client.NeedsMidRoundSync = true; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index 187f0f1af..0d32a93e8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -537,7 +537,7 @@ namespace Barotrauma.Networking } //add the ID card tags they should've gotten when spawning in the shuttle - character.GiveIdCardTags(shuttleSpawnPoints[i], requireSpawnPointTagsNotGiven: false, createNetworkEvent: true); + character.GiveIdCardTags(shuttleSpawnPoints[i], createNetworkEvent: true); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index ec0e8e101..7f29c7662 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -611,7 +611,7 @@ namespace Barotrauma.Networking { foreach (DebugConsole.Command command in clientPermission.PermittedCommands) { - clientElement.Add(new XElement("command", new XAttribute("name", command.names[0]))); + clientElement.Add(new XElement("command", new XAttribute("name", command.Names[0]))); } } doc.Root.Add(clientElement); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Program.cs b/Barotrauma/BarotraumaServer/ServerSource/Program.cs index 2a127c1ba..e1d371659 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Program.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Program.cs @@ -2,6 +2,7 @@ using Barotrauma.Steam; using System; +using System.Diagnostics; using Barotrauma.IO; using System.Linq; using System.Text; @@ -109,18 +110,25 @@ namespace Barotrauma } } + Exception unhandledException = args.ExceptionObject as Exception; string reportFilePath = ""; try { reportFilePath = "servercrashreport.log"; - CrashDump(ref reportFilePath, (Exception)args.ExceptionObject); + CrashDump(ref reportFilePath, unhandledException); } - catch + catch (Exception exceptionHandlerError) { - //fuck + Debug.WriteLine(exceptionHandlerError.Message); + string slimCrashReport = "Exception handler failed: " + exceptionHandlerError.Message + "\n" + exceptionHandlerError.StackTrace; + if (unhandledException != null) + { + slimCrashReport += "\n\nInitial exception: " + unhandledException.Message + "\n" + unhandledException.StackTrace; + } + File.WriteAllText("servercrashreportslim.log", slimCrashReport); reportFilePath = ""; } - swallowExceptions(() => NotifyCrash(reportFilePath, (Exception)args.ExceptionObject)); + swallowExceptions(() => NotifyCrash(reportFilePath, unhandledException)); swallowExceptions(() => Game?.Exit()); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs index e0967195b..039e30109 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs @@ -58,15 +58,20 @@ namespace Barotrauma.Steam Steamworks.SteamServer.SetKey("message", server.ServerSettings.ServerMessageText); Steamworks.SteamServer.SetKey("version", GameMain.Version.ToString()); Steamworks.SteamServer.SetKey("playercount", server.ConnectedClients.Count.ToString()); + + //a2s seems to break if too much data is added (seems to be related to MTU?) + //let's restrict the number of packages to 10, clients can use packagecount to tell when the list has been truncated + const int MaxPackagesToList = 10; int index = 0; - foreach (var contentPackage in contentPackages) + foreach (var contentPackage in contentPackages.Take(MaxPackagesToList)) { string ugcIdStr = contentPackage.UgcId.TryUnwrap(out var ugcId) ? ugcId.StringRepresentation : string.Empty; Steamworks.SteamServer.SetKey( - $"contentpackage{index}", - contentPackage.Name+","+ contentPackage.Hash.StringRepresentation + "," + ugcIdStr); + $"contentpackage{index}", + contentPackage.Name + "," + contentPackage.Hash.StringRepresentation + "," + ugcIdStr); index++; } + Steamworks.SteamServer.SetKey("packagecount", contentPackages.Count().ToString()); Steamworks.SteamServer.SetKey("modeselectionmode", server.ServerSettings.ModeSelectionMode.ToString()); Steamworks.SteamServer.SetKey("subselectionmode", server.ServerSettings.SubSelectionMode.ToString()); Steamworks.SteamServer.SetKey("voicechatenabled", server.ServerSettings.VoiceChatEnabled.ToString()); @@ -79,6 +84,10 @@ namespace Barotrauma.Steam Steamworks.SteamServer.SetKey("gamemode", server.ServerSettings.GameModeIdentifier.Value); Steamworks.SteamServer.SetKey("playstyle", server.ServerSettings.PlayStyle.ToString()); Steamworks.SteamServer.SetKey("language", server.ServerSettings.Language.ToString()); + if (GameMain.NetLobbyScreen?.SelectedSub != null) + { + Steamworks.SteamServer.SetKey("submarine", GameMain.NetLobbyScreen.SelectedSub.Name); + } Steamworks.SteamServer.DedicatedServer = true; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs index 696d36f9d..498108b29 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs @@ -224,7 +224,8 @@ namespace Barotrauma var selectedTraitor = SelectRandomTraitor(); if (selectedTraitor == null) { - DebugConsole.ThrowError($"Could not find a suitable traitor for the event \"{selectedPrefab.Identifier}\"."); + DebugConsole.ThrowError($"Could not find a suitable traitor for the event \"{selectedPrefab.Identifier}\".", + contentPackage: selectedPrefab.ContentPackage); return false; } CreateTraitorEvent(eventManager, selectedPrefab, selectedTraitor); @@ -262,8 +263,9 @@ namespace Barotrauma if (amountToChoose > viableTraitors.Count) { DebugConsole.ThrowError( - $"Error in traitor event {traitorEvent.Prefab.Identifier}. Not enough players to choose {amountToChoose} secondary traitors."+ - $"Make sure the {nameof(traitorEvent.Prefab.MinPlayerCount)} of the event is high enough to support to desired amount of secondary traitors."); + $"Error in traitor event {traitorEvent.Prefab.Identifier}. Not enough players to choose {amountToChoose} secondary traitors. " + + $"Make sure the {nameof(traitorEvent.Prefab.MinPlayerCount)} of the event is high enough to support to desired amount of secondary traitors.", + contentPackage: traitorEvent.Prefab.ContentPackage); amountToChoose = viableTraitors.Count; } @@ -352,7 +354,8 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"Failed to create an instance of the traitor event prefab \"{selectedPrefab.Identifier}\"!"); + DebugConsole.ThrowError($"Failed to create an instance of the traitor event prefab \"{selectedPrefab.Identifier}\"!", + contentPackage: selectedPrefab.ContentPackage); } } @@ -365,7 +368,8 @@ namespace Barotrauma var traitor = SelectRandomTraitor(); if (traitor == null) { - DebugConsole.ThrowError($"Could not find a suitable traitor for the event \"{traitorEventPrefab.Identifier}\"."); + DebugConsole.ThrowError($"Could not find a suitable traitor for the event \"{traitorEventPrefab.Identifier}\".", + contentPackage: traitorEventPrefab.ContentPackage); return; } CreateTraitorEvent(eventManager, traitorEventPrefab, traitor); @@ -451,6 +455,15 @@ namespace Barotrauma activeEvent.TraitorEvent.Prefab, activeEvent.TraitorEvent.CurrentState, activeEvent.Traitor)); + + if (activeEvent.TraitorEvent.CurrentState == TraitorEvent.State.Completed) + { + SteamAchievementManager.OnTraitorWin(activeEvent.TraitorEvent.Traitor?.Character); + foreach (var secondaryTraitor in activeEvent.TraitorEvent.SecondaryTraitors) + { + SteamAchievementManager.OnTraitorWin(secondaryTraitor?.Character); + } + } } if (previousTraitorEvents.Count > MaxPreviousEventHistory) { diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 6948b9453..0ece0e532 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.1.19.3 + 1.2.6.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/Data/campaignsettings.xml b/Barotrauma/BarotraumaShared/Data/campaignsettings.xml index ef782ae23..a9be7381b 100644 --- a/Barotrauma/BarotraumaShared/Data/campaignsettings.xml +++ b/Barotrauma/BarotraumaShared/Data/campaignsettings.xml @@ -12,7 +12,9 @@ StartingBalanceAmount="High" StartItemSet="easy" MaxMissionCount="3" - Difficulty="Easy"/> + Difficulty="Easy" + MinStolenItemInspectionProbability="0.2" + MaxStolenItemInspectionProbability="0.9"/> + Difficulty="Medium" + MinStolenItemInspectionProbability="0.3" + MaxStolenItemInspectionProbability="0.9"/> + Difficulty="Hard" + MinStolenItemInspectionProbability="0.4" + MaxStolenItemInspectionProbability="1.0"/> \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs index 2fc63b541..eb469f60a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs @@ -91,6 +91,11 @@ namespace Barotrauma private IEnumerable visibleHulls; private float hullVisibilityTimer; const float hullVisibilityInterval = 0.5f; + + /// + /// Returns hulls that are visible to the character, including the current hull. + /// Note that this is not an accurate visibility check, it only checks for open gaps between the adjacent and linked hulls. + /// public IEnumerable VisibleHulls { get @@ -353,7 +358,7 @@ namespace Barotrauma public static void UnequipContainedItems(Character character, Item parentItem, Func predicate, bool avoidDroppingInSea = true, int? unequipMax = null) { var inventory = parentItem.OwnInventory; - if (inventory == null) { return; } + if (inventory == null || !inventory.Container.DrawInventory) { return; } int removed = 0; if (predicate == null || inventory.AllItems.Any(predicate)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 9495c3e08..d4188f275 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -262,7 +262,8 @@ namespace Barotrauma if (aiElements.Count == 0) { - DebugConsole.ThrowError("Error in file \"" + c.Params.File + "\" - no AI element found."); + DebugConsole.ThrowError("Error in file \"" + c.Params.File.Path + "\" - no AI element found.", + contentPackage: c.Prefab?.ContentPackage); outsideSteering = new SteeringManager(this); insideSteering = new IndoorsSteeringManager(this, false, false); return; @@ -311,7 +312,7 @@ namespace Barotrauma } ReevaluateAttacks(); outsideSteering = new SteeringManager(this); - insideSteering = new IndoorsSteeringManager(this, Character.Params.AI.CanOpenDoors, canAttackDoors); + insideSteering = new IndoorsSteeringManager(this, AIParams.CanOpenDoors, canAttackDoors); steeringManager = outsideSteering; State = AIState.Idle; requiredHoleCount = (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderWidth) / Structure.WallSectionSize); @@ -321,6 +322,10 @@ namespace Barotrauma } private CharacterParams.AIParams _aiParams; + /// + /// Shorthand for with null checking. + /// + /// or an empty params. Does not return nulls. public CharacterParams.AIParams AIParams { get @@ -330,7 +335,8 @@ namespace Barotrauma _aiParams = Character.Params.AI; if (_aiParams == null) { - DebugConsole.ThrowError($"No AI Params defined for {Character.SpeciesName}. AI disabled."); + DebugConsole.ThrowError($"No AI Params defined for {Character.SpeciesName}. AI disabled.", + contentPackage: Character.Prefab.ContentPackage); Enabled = false; _aiParams = new CharacterParams.AIParams(null, Character.Params); } @@ -563,7 +569,7 @@ namespace Barotrauma } } - if (Character.Params.UsePathFinding && Character.Params.AI.UsePathFindingToGetInside && AIParams.CanOpenDoors) + if (Character.Params.UsePathFinding && AIParams.UsePathFindingToGetInside && AIParams.CanOpenDoors) { // Meant for monsters outside the player sub that target something inside the sub and can use the doors to access the sub (Husk). bool IsCloseEnoughToTargetSub(float threshold) => SelectedAiTarget?.Entity?.Submarine is Submarine sub && sub != null && Vector2.DistanceSquared(Character.WorldPosition, sub.WorldPosition) < MathUtils.Pow(Math.Max(sub.Borders.Size.X, sub.Borders.Size.Y) / 2 + threshold, 2); @@ -2503,7 +2509,8 @@ namespace Barotrauma Limb mouthLimb = Character.AnimController.GetLimb(LimbType.Head); if (mouthLimb == null) { - DebugConsole.ThrowError("Character \"" + Character.SpeciesName + "\" failed to eat a target (No head limb defined)"); + DebugConsole.ThrowError("Character \"" + Character.SpeciesName + "\" failed to eat a target (No head limb defined)", + contentPackage: Character.Prefab.ContentPackage); State = AIState.Idle; return; } @@ -2540,7 +2547,11 @@ namespace Barotrauma item.body.LinearVelocity -= velocity * 0.25f; bool wasBroken = item.Condition <= 0.0f; item.LastEatenTime = (float)Timing.TotalTimeUnpaused; - item.AddDamage(Character, item.WorldPosition, new Attack(0.0f, 0.0f, 0.0f, 0.0f, 0.02f * Character.Params.EatingSpeed), deltaTime); + item.AddDamage(Character, + item.WorldPosition, + new Attack(0.0f, 0.0f, 0.0f, 0.0f, 0.02f * Character.Params.EatingSpeed), + impulseDirection: Vector2.Zero, + deltaTime); Character.ApplyStatusEffects(ActionType.OnEating, deltaTime); if (item.Condition <= 0.0f) { @@ -3090,7 +3101,10 @@ namespace Barotrauma break; } - valueModifier *= targetMemory.Priority / (float)Math.Sqrt(dist); + valueModifier *= + targetMemory.Priority / + //sqrt = the further the target is, the less the distance matters + MathF.Sqrt(dist); if (valueModifier > targetValue) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 698f938b4..a8dfedc43 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -685,8 +685,8 @@ namespace Barotrauma } if (removeDivingSuit) { - var divingSuit = Character.Inventory.FindItemByTag(Tags.HeavyDivingGear); - if (divingSuit != null && !divingSuit.HasTag(Tags.DivingGearWearableIndoors)) + var divingSuit = Character.Inventory.FindEquippedItemByTag(Tags.HeavyDivingGear); + if (divingSuit != null && !divingSuit.HasTag(Tags.DivingGearWearableIndoors) && divingSuit.IsInteractable(Character)) { if (shouldActOnSuffocation || Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority) { @@ -727,54 +727,51 @@ namespace Barotrauma } } if (takeMaskOff) - { - if (Character.HasEquippedItem(Tags.LightDivingGear)) + { + var mask = Character.Inventory.FindEquippedItemByTag(Tags.LightDivingGear); + if (mask != null) { - var mask = Character.Inventory.FindItemByTag(Tags.LightDivingGear); - if (mask != null) + if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List() { InvSlotType.Any })) { - if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List() { InvSlotType.Any })) + if (Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority) { - if (Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority) + mask.Drop(Character); + HandleRelocation(mask); + ReequipUnequipped(); + } + else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingMask) + { + findItemState = FindItemState.DivingMask; + if (FindSuitableContainer(mask, out Item targetContainer)) { - mask.Drop(Character); - HandleRelocation(mask); - ReequipUnequipped(); - } - else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingMask) - { - findItemState = FindItemState.DivingMask; - if (FindSuitableContainer(mask, out Item targetContainer)) + findItemState = FindItemState.None; + itemIndex = 0; + if (targetContainer != null) { - findItemState = FindItemState.None; - itemIndex = 0; - if (targetContainer != null) + var decontainObjective = new AIObjectiveDecontainItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent()); + decontainObjective.Abandoned += () => { - var decontainObjective = new AIObjectiveDecontainItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent()); - decontainObjective.Abandoned += () => - { - ReequipUnequipped(); - IgnoredItems.Add(targetContainer); - }; - decontainObjective.Completed += () => ReequipUnequipped(); - ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true); - return; - } - else - { - mask.Drop(Character); - HandleRelocation(mask); ReequipUnequipped(); - } + IgnoredItems.Add(targetContainer); + }; + decontainObjective.Completed += () => ReequipUnequipped(); + ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true); + return; + } + else + { + mask.Drop(Character); + HandleRelocation(mask); + ReequipUnequipped(); } } } - else - { - ReequipUnequipped(); - } } - } + else + { + ReequipUnequipped(); + } + } } } } @@ -784,13 +781,11 @@ namespace Barotrauma if (findItemState == FindItemState.None || findItemState == FindItemState.OtherItem) { - for (int i = 0; i < 2; i++) + foreach (Item item in Character.HeldItems) { - var hand = i == 0 ? InvSlotType.RightHand : InvSlotType.LeftHand; - Item item = Character.Inventory.GetItemInLimbSlot(hand); - if (item == null) { continue; } + if (item == null || !item.IsInteractable(Character)) { continue; } - if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List() { InvSlotType.Any }) && Character.Submarine?.TeamID == Character.TeamID ) + if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, CharacterInventory.AnySlot) && Character.Submarine?.TeamID == Character.TeamID) { if (item.AllowedSlots.Contains(InvSlotType.Bag) && Character.Inventory.TryPutItem(item, Character, new List() { InvSlotType.Bag })) { continue; } findItemState = FindItemState.OtherItem; @@ -1389,7 +1384,10 @@ namespace Barotrauma // Don't react to friendly enemy AI attacking other characters. E.g. husks attacking someone when whe are a cultist. continue; } - bool isWitnessing = otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) || otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull); + bool isWitnessing = + otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) || + otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull) || + otherCharacter.CanSeeTarget(attacker, seeThroughWindows: true); if (!isWitnessing) { //if the other character did not witness the attack, and the character is not within report range (or capable of reporting) @@ -1754,11 +1752,11 @@ namespace Barotrauma if (otherCharacter == character || otherCharacter.TeamID == character.TeamID || otherCharacter.IsDead || otherCharacter.Info?.Job == null || otherCharacter.AIController is not HumanAIController otherHumanAI || - !otherHumanAI.VisibleHulls.Contains(character.CurrentHull)) + Vector2.DistanceSquared(otherCharacter.WorldPosition, character.WorldPosition) > 1000.0f * 1000.0f) { continue; } - if (!otherCharacter.CanSeeTarget(character)) { continue; } + if (!otherCharacter.CanSeeTarget(character, seeThroughWindows: true)) { continue; } if (!otherHumanAI.structureDamageAccumulator.ContainsKey(character)) { otherHumanAI.structureDamageAccumulator.Add(character, 0.0f); } float prevAccumulatedDamage = otherHumanAI.structureDamageAccumulator[character]; @@ -1796,7 +1794,7 @@ namespace Barotrauma if (!TriggerSecurity(otherHumanAI, combatMode)) { // Else call the others - foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID).OrderByDescending(c => Vector2.DistanceSquared(character.WorldPosition, c.WorldPosition))) + foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID).OrderBy(c => Vector2.DistanceSquared(character.WorldPosition, c.WorldPosition))) { if (!TriggerSecurity(security.AIController as HumanAIController, combatMode)) { @@ -1840,13 +1838,13 @@ namespace Barotrauma foreach (Character otherCharacter in Character.CharacterList) { if (otherCharacter == thief || otherCharacter.TeamID == thief.TeamID || otherCharacter.IsIncapacitated || otherCharacter.Stun > 0.0f || - otherCharacter.Info?.Job == null || !(otherCharacter.AIController is HumanAIController otherHumanAI) || - !otherHumanAI.VisibleHulls.Contains(thief.CurrentHull)) + otherCharacter.Info?.Job == null || otherCharacter.AIController is not HumanAIController otherHumanAI || + Vector2.DistanceSquared(otherCharacter.WorldPosition, thief.WorldPosition) > 1000.0f * 1000.0f) { continue; } //if (!otherCharacter.IsFacing(thief.WorldPosition)) { continue; } - if (!otherCharacter.CanSeeTarget(thief)) { continue; } + if (!otherCharacter.CanSeeTarget(thief, seeThroughWindows: true)) { continue; } // Don't react if the player is taking an extinguisher and there's any fires on the sub, or diving gear when the sub is flooding // -> allow them to use the emergency items if (thief.Submarine != null) @@ -1857,16 +1855,11 @@ namespace Barotrauma } if (!someoneSpoke) { - if (!item.StolenDuringRound && - Level.Loaded?.Type == LevelData.LevelType.Outpost && - GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null) + if (!item.StolenDuringRound) { - var reputationLoss = MathHelper.Clamp( - (item.Prefab.GetMinPrice() ?? 0) * Reputation.ReputationLossPerStolenItemPrice, - Reputation.MinReputationLossPerStolenItem, Reputation.MaxReputationLossPerStolenItem); - GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation?.AddReputation(-reputationLoss); + ApplyStealingReputationLoss(item); + item.StolenDuringRound = true; } - item.StolenDuringRound = true; otherCharacter.Speak(TextManager.Get("dialogstealwarning").Value, null, Rand.Range(0.5f, 1.0f), "thief".ToIdentifier(), 10.0f); someoneSpoke = true; #if CLIENT @@ -1877,7 +1870,7 @@ namespace Barotrauma if (!TriggerSecurity(otherHumanAI)) { // Else call the others - foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID).OrderByDescending(c => Vector2.DistanceSquared(thief.WorldPosition, c.WorldPosition))) + foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID).OrderBy(c => Vector2.DistanceSquared(thief.WorldPosition, c.WorldPosition))) { if (TriggerSecurity(security.AIController as HumanAIController)) { @@ -1898,6 +1891,10 @@ namespace Barotrauma if (humanAI == null) { return false; } if (!humanAI.Character.IsSecurity) { return false; } if (humanAI.ObjectiveManager.IsCurrentObjective()) { return false; } + if (humanAI.ObjectiveManager.GetObjective() is { } findThieves) + { + findThieves.InspectEveryone(); + } humanAI.AddCombatObjective(AIObjectiveCombat.CombatMode.Arrest, thief, delay: GetReactionTime(), abortCondition: obj => thief.Inventory.FindItem(it => it != null && it.StolenDuringRound, true) == null, onAbort: () => @@ -1915,6 +1912,18 @@ namespace Barotrauma } } + public static void ApplyStealingReputationLoss(Item item) + { + if (Level.Loaded?.Type == LevelData.LevelType.Outpost && + GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null) + { + var reputationLoss = MathHelper.Clamp( + (item.Prefab.GetMinPrice() ?? 0) * Reputation.ReputationLossPerStolenItemPrice, + Reputation.MinReputationLossPerStolenItem, Reputation.MaxReputationLossPerStolenItem); + GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation?.AddReputation(-reputationLoss); + } + } + // 0.225 - 0.375 private static float GetReactionTime() => reactionTime * Rand.Range(0.75f, 1.25f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs index 884a07263..c9dd03d88 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -197,17 +197,6 @@ namespace Barotrauma } } - /// - /// This method allows multiple subobjectives of same type. Use with caution. - /// - public void AddSubObjectiveInQueue(AIObjective objective) - { - if (!subObjectives.Contains(objective)) - { - subObjectives.Add(objective); - } - } - public void RemoveSubObjective(ref T objective) where T : AIObjective { if (objective != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCheckStolenItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCheckStolenItems.cs new file mode 100644 index 000000000..cd63e7856 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCheckStolenItems.cs @@ -0,0 +1,160 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; + +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; + + public float FindStolenItemsProbability = 1.0f; + + enum State + { + GotoTarget, + Inspect, + Warn, + Done + } + + private float inspectDelay; + private float warnDelay; + + private State currentState; + + public readonly Character TargetCharacter; + + private AIObjectiveGoTo? goToObjective; + + private readonly List stolenItems = new List(); + + public AIObjectiveCheckStolenItems(Character character, Character targetCharacter, 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()); + } + + protected override bool CheckObjectiveSpecific() => false; + + protected override float GetPriority() + { + if (!Abandon && !IsCompleted && objectiveManager.IsOrder(this)) + { + Priority = objectiveManager.GetOrderPriority(this); + } + else + { + Priority = AIObjectiveManager.LowestOrderPriority - 1; + } + return Priority; + } + + public void ForceComplete() + { + IsCompleted = true; + } + + protected override void Act(float deltaTime) + { + switch (currentState) + { + case State.GotoTarget: + TryAddSubObjective(ref goToObjective, + constructor: () => + { + return new AIObjectiveGoTo(TargetCharacter, character, objectiveManager, repeat: 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); + }, + onAbandon: () => + { + Abandon = true; + }); + break; + case State.Inspect: + Inspect(deltaTime); + break; + case State.Warn: + Warn(deltaTime); + break; + } + } + + private void Inspect(float deltaTime) + { + if (inspectDelay > 0.0f) + { + character.SelectCharacter(TargetCharacter); + inspectDelay -= deltaTime; + return; + } + + if (stolenItems.Any() && + Rand.Range(0.0f, 1.0f, Rand.RandSync.Unsynced) < FindStolenItemsProbability) + { + character.Speak(TextManager.Get("dialogcheckstolenitems.warn").Value); + currentState = State.Warn; + } + else + { + character.Speak(TextManager.Get("dialogcheckstolenitems.nostolenitems").Value); + currentState = State.Done; + IsCompleted = true; + } + character.DeselectCharacter(); + } + + private void Warn(float deltaTime) + { + if (warnDelay > 0.0f) + { + warnDelay -= deltaTime; + return; + } + var stolenItemsOnCharacter = stolenItems.Where(it => it.GetRootInventoryOwner() == TargetCharacter); + if (stolenItemsOnCharacter.Any()) + { + character.Speak(TextManager.Get("dialogcheckstolenitems.arrest").Value); + HumanAIController.AddCombatObjective(AIObjectiveCombat.CombatMode.Arrest, TargetCharacter); + foreach (var stolenItem in stolenItemsOnCharacter) + { + HumanAIController.ApplyStealingReputationLoss(stolenItem); + } + } + else + { + character.Speak(TextManager.Get("dialogcheckstolenitems.comply").Value); + } + foreach (var item in stolenItems) + { + HumanAIController.ObjectiveManager.AddObjective(new AIObjectiveGetItem(character, item, objectiveManager, equip: false) + { + BasePriority = 10 + }); + } + currentState = State.Done; + IsCompleted = true; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index 612eaa7fe..9bbb3836b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -1070,7 +1070,8 @@ namespace Barotrauma { // Try reload ammunition from inventory static bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag(Tags.MobileRadio); - Item ammunition = character.Inventory.FindItem(i => i.HasIdentifierOrTags(ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i), recursive: true); + Item ammunition = character.Inventory.FindItem(i => + i.HasIdentifierOrTags(ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i) && i.IsInteractable(character), recursive: true); if (ammunition != null) { var container = Weapon.GetComponent(); @@ -1089,6 +1090,9 @@ namespace Barotrauma } else if (!HoldPosition && IsOffensiveOrArrest && seekAmmo && ammunitionIdentifiers != null) { + // Inventory not drawn = it's not interactable + // If the weapon is empty and the inventory is inaccessible, it can't be reloaded + if (!Weapon.OwnInventory.Container.DrawInventory) { return false; } SeekAmmunition(ammunitionIdentifiers); } return false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveEscapeHandcuffs.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveEscapeHandcuffs.cs index ed5408fb7..0a2a7839d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveEscapeHandcuffs.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveEscapeHandcuffs.cs @@ -14,7 +14,7 @@ namespace Barotrauma private int escapeProgress; private bool isBeingWatched; - private bool shouldSwitchTeams; + private readonly bool shouldSwitchTeams; const string EscapeTeamChangeIdentifier = "escape"; @@ -88,10 +88,12 @@ namespace Barotrauma escapeProgress += Rand.Range(2, 5); if (escapeProgress > 15) { - Item handcuffs = character.Inventory.FindItemByTag(Tags.HandLockerItem); - if (handcuffs != null) + foreach (var it in character.HeldItems) { - handcuffs.Drop(character); + if (it.HasTag(Tags.HandLockerItem) && it.IsInteractable(character)) + { + it.Drop(character); + } } } escapeTimer = EscapeIntervalTimer * Rand.Range(0.75f, 1.25f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindThieves.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindThieves.cs new file mode 100644 index 000000000..f6ac0bec8 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindThieves.cs @@ -0,0 +1,152 @@ +#nullable enable +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + class AIObjectiveFindThieves : AIObjectiveLoop + { + public override Identifier Identifier { get; set; } = "find thieves".ToIdentifier(); + protected override float IgnoreListClearInterval => 30; + public override bool IgnoreUnsafeHulls => true; + + protected override float TargetUpdateTimeMultiplier => 1.0f; + + const float DefaultInspectDistance = 200.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? overrideInspectProbability; + /// + /// 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 + { + get + { + if (overrideInspectProbability.HasValue) + { + return overrideInspectProbability.Value; + } + if (GameMain.GameSession?.Campaign is { } campaign) + { + if (campaign.Map?.CurrentLocation?.Reputation is { } reputation) + { + return MathHelper.Lerp( + campaign.Settings.MaxStolenItemInspectionProbability, + campaign.Settings.MinStolenItemInspectionProbability, + reputation.NormalizedValue); + } + } + + return 0.2f; + } + } + + /// + /// 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; + + public AIObjectiveFindThieves(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) + : base(character, objectiveManager, priorityModifier) { } + + protected override bool Filter(Character target) + { + if (!IsValidTarget(target, character)) { return false; } + if (Vector2.DistanceSquared(target.WorldPosition, character.WorldPosition) > InspectDistance * InspectDistance) { return false; } + if (lastInspectionTimes.TryGetValue(target, out double lastInspectionTime)) + { + if (Timing.TotalTime < lastInspectionTime + inspectionInterval) + { + return false; + } + } + return true; + } + + protected override IEnumerable GetList() => Character.CharacterList; + + protected override float TargetEvaluation() + { + return subObjectives.Any() ? 50 : 0; + } + + public void InspectEveryone() + { + lastInspectionTimes.Clear(); + overrideInspectProbability = 1.0f; + InspectDistance = DefaultInspectDistance * 2; + } + + protected override AIObjective ObjectiveConstructor(Character target) + { + var checkStolenItemsObjective = new AIObjectiveCheckStolenItems(character, target, objectiveManager); + if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Unsynced) >= InspectProbability) + { + checkStolenItemsObjective.ForceComplete(); + lastInspectionTimes[target] = Timing.TotalTime; + } + return checkStolenItemsObjective; + } + + private float checkVisibleStolenItemsTimer; + private const float CheckVisibleStolenItemsInterval = 5.0f; + + public override void Update(float deltaTime) + { + base.Update(deltaTime); + if (checkVisibleStolenItemsTimer > 0.0f) + { + checkVisibleStolenItemsTimer -= deltaTime; + return; + } + 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)) && + character.CanSeeTarget(target, seeThroughWindows: true)) + { + AIObjectiveCheckStolenItems? existingObjective = + objectiveManager.GetActiveObjectives().FirstOrDefault(o => o.TargetCharacter == target); + if (existingObjective == null) + { + objectiveManager.AddObjective(new AIObjectiveCheckStolenItems(character, target, objectiveManager)); + lastInspectionTimes[target] = Timing.TotalTime; + } + + } + } + checkVisibleStolenItemsTimer = CheckVisibleStolenItemsInterval; + } + + private bool IsValidTarget(Character target, Character character) + { + if (target == null || target.Removed) { return false; } + if (target.IsIncapacitated) { return false; } + if (target == character) { return false; } + if (target.Submarine == null) { return false; } + if (character.Submarine == null) { return false; } + if (target.CurrentHull == null) { return false; } + 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; } + return true; + } + + protected override void OnObjectiveCompleted(AIObjective objective, Character target) + { + lastInspectionTimes[target] = Timing.TotalTime; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index bd01ecebf..1e22487b1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -120,7 +120,7 @@ namespace Barotrauma // The intention behind this is to reduce unnecessary path finding calls in cases where the bot can't find a path. timerMargin += 0.5f; timerMargin = Math.Min(timerMargin, newTargetIntervalMin); - newTargetTimer = Math.Min(newTargetTimer, timerMargin); + newTargetTimer = Math.Max(newTargetTimer, timerMargin); } private void SetTargetTimerHigh() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs index 5419eedf7..2a939ab76 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs @@ -178,7 +178,7 @@ namespace Barotrauma if (!objectiveManager.IsOrder(this)) { // Battery or pump states cannot currently be reported (not implemented) and therefore we must ignore them -> the bots always know if they require attention. - bool ignore = this is AIObjectiveChargeBatteries || this is AIObjectivePumpWater; + bool ignore = this is AIObjectiveChargeBatteries || this is AIObjectivePumpWater || this is AIObjectiveFindThieves; if (!ignore && !ReportedTargets.Contains(target)) { continue; } } if (!Filter(target)) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index 5d1aa4a61..42c31a65c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -142,6 +142,7 @@ namespace Barotrauma prevIdleObjective.PreferredOutpostModuleTypes.ForEach(t => newIdleObjective.PreferredOutpostModuleTypes.Add(t)); } AddObjective(newIdleObjective); + int objectiveCount = Objectives.Count; foreach (var autonomousObjective in character.Info.Job.Prefab.AutonomousObjectives) { @@ -549,6 +550,9 @@ namespace Barotrauma case "escapehandcuffs": newObjective = new AIObjectiveEscapeHandcuffs(character, this, priorityModifier: priorityModifier); break; + case "findthieves": + newObjective = new AIObjectiveFindThieves(character, this, priorityModifier: priorityModifier); + break; case "prepareforexpedition": newObjective = new AIObjectivePrepare(character, this, order.GetTargetItems(order.Option), order.RequireItems) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index 21c5aeb35..04ee9ccad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -441,7 +441,8 @@ namespace Barotrauma } catch (NotImplementedException e) { - DebugConsole.LogError($"Error creating a new Order instance: unexpected target type \"{targetType}\".\n{e.StackTrace.CleanupStackTrace()}"); + DebugConsole.LogError($"Error creating a new Order instance: unexpected target type \"{targetType}\".\n{e.StackTrace.CleanupStackTrace()}", + contentPackage: ContentPackage); return null; } } @@ -663,6 +664,13 @@ namespace Barotrauma WallSectionIndex = wallSectionIndex ?? other.WallSectionIndex; UseController = useController ?? other.UseController; + +#if DEBUG + if (UseController && ConnectedController == null) + { + DebugConsole.ThrowError($"AI: Created an Order {Identifier} that's set to use a Controller, but a Controller was not specified.\n{Environment.StackTrace.CleanupStackTrace()}"); + } +#endif } public Order WithOption(Identifier option) @@ -712,7 +720,12 @@ namespace Barotrauma public Order WithItemComponent(Item item, ItemComponent component = null) { - return new Order(this, targetEntity: item, targetItemComponent: component ?? GetTargetItemComponent(item)); + Controller controller = null; + if (UseController) + { + controller = item?.FindController(tags: ControllerTags); + } + return new Order(this, targetEntity: item, targetItemComponent: component ?? GetTargetItemComponent(item), connectedController: controller); } public Order WithWallSection(Structure wall, int? sectionIndex) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs index 47e3ec73d..ad724f0bd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs @@ -552,7 +552,8 @@ namespace Barotrauma #if DEBUG if (handlePos[i].LengthSquared() > ArmLength) { - DebugConsole.AddWarning($"Aim position for the item {item.Name} may be incorrect (further than the length of the character's arm)"); + DebugConsole.AddWarning($"Aim position for the item {item.Name} may be incorrect (further than the length of the character's arm)", + item.Prefab.ContentPackage); } #endif HandIK( diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 91924e625..831287a4b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -10,6 +10,17 @@ namespace Barotrauma { class HumanoidAnimController : AnimController { + private const float SteepestWalkableSlopeAngleDegrees = 50f; + private const float SlowlyWalkableSlopeAngleDegrees = 30f; + + private static readonly float SteepestWalkableSlopeNormalX = + MathF.Sin(MathHelper.ToRadians(SteepestWalkableSlopeAngleDegrees)); + private static readonly float SlowlyWalkableSlopeNormalX = + MathF.Sin(MathHelper.ToRadians(SlowlyWalkableSlopeAngleDegrees)); + + private const float MaxSpeedOnStairs = 1.7f; + private const float SteepSlopePushMagnitude = MaxSpeedOnStairs; + public override RagdollParams RagdollParams { get { return HumanRagdollParams; } @@ -150,7 +161,7 @@ namespace Barotrauma private readonly float movementLerp; - private float cprAnimTimer,cprPump; + private float cprAnimTimer, cprPumpTimer; private float fallingProneAnimTimer; const float FallingProneAnimDuration = 1.0f; @@ -243,14 +254,17 @@ namespace Barotrauma if (MainLimb == null) { return; } levitatingCollider = !IsHanging; - if ((character.SelectedItem?.GetComponent()?.ControlCharacterPose ?? false) || - (character.SelectedSecondaryItem?.GetComponent()?.ControlCharacterPose ?? false) || - character.SelectedSecondaryItem?.GetComponent() != null || - (ForceSelectAnimationType != AnimationType.Crouch && ForceSelectAnimationType != AnimationType.NotDefined)) + if (onGround && character.CanMove) { - Crouching = false; + if ((character.SelectedItem?.GetComponent()?.ControlCharacterPose ?? false) || + (character.SelectedSecondaryItem?.GetComponent()?.ControlCharacterPose ?? false) || + character.SelectedSecondaryItem?.GetComponent() != null || + (ForceSelectAnimationType != AnimationType.Crouch && ForceSelectAnimationType != AnimationType.NotDefined)) + { + Crouching = false; + } + ColliderIndex = Crouching && !swimming ? 1 : 0; } - ColliderIndex = Crouching && !swimming ? 1 : 0; //stun (= disable the animations) if the ragdoll receives a large enough impact if (strongestImpact > 0.0f) @@ -276,7 +290,7 @@ namespace Barotrauma if (!character.CanMove) { - if (fallingProneAnimTimer < FallingProneAnimDuration) + if (fallingProneAnimTimer < FallingProneAnimDuration && onGround) { fallingProneAnimTimer += deltaTime; UpdateFallingProne(1.0f); @@ -285,7 +299,12 @@ namespace Barotrauma Collider.FarseerBody.FixedRotation = false; if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) { - Collider.Enabled = false; + if (Collider.Enabled) + { + //deactivating the collider -> make the main limb inherit the collider's velocity because it'll control the movement now + MainLimb.body.LinearVelocity = Collider.LinearVelocity; + Collider.Enabled = false; + } Collider.LinearVelocity = MainLimb.LinearVelocity; 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 @@ -386,6 +405,12 @@ namespace Barotrauma DragCharacter(character.SelectedCharacter, deltaTime); } + if (Anim != Animation.CPR) + { + cprAnimTimer = 0.0f; + cprPumpTimer = 0.0f; + } + switch (Anim) { case Animation.Climbing: @@ -487,10 +512,14 @@ namespace Barotrauma Limb leftLeg = GetLimb(LimbType.LeftLeg); Limb rightLeg = GetLimb(LimbType.RightLeg); + bool onSlopeThatMakesSlow = Math.Abs(floorNormal.X) > SlowlyWalkableSlopeNormalX; + bool slowedDownBySlope = onSlopeThatMakesSlow && Math.Sign(floorNormal.X) == -Math.Sign(TargetMovement.X); + bool onSlopeTooSteepToClimb = Math.Abs(floorNormal.X) > SteepestWalkableSlopeNormalX; + float walkCycleMultiplier = 1.0f; - if (Stairs != null) + if (Stairs != null || slowedDownBySlope) { - TargetMovement = new Vector2(MathHelper.Clamp(TargetMovement.X, -1.7f, 1.7f), TargetMovement.Y); + TargetMovement = new Vector2(MathHelper.Clamp(TargetMovement.X, -MaxSpeedOnStairs, MaxSpeedOnStairs), TargetMovement.Y); walkCycleMultiplier *= 1.5f; } @@ -572,6 +601,15 @@ namespace Barotrauma bool movingHorizontally = !MathUtils.NearlyEqual(TargetMovement.X, 0.0f); + if (Stairs == null && onSlopeTooSteepToClimb) + { + if (Math.Sign(targetMovement.X) != Math.Sign(floorNormal.X)) + { + targetMovement.X = Math.Sign(floorNormal.X) * SteepSlopePushMagnitude; + movement = targetMovement; + } + } + if (Stairs != null || onSlope) { torso.PullJointWorldAnchorB = new Vector2( @@ -648,14 +686,6 @@ namespace Barotrauma if (!onGround) { - Vector2 move = torso.PullJointWorldAnchorB - torso.SimPosition; - - foreach (Limb limb in Limbs) - { - if (limb.IsSevered) { continue; } - MoveLimb(limb, limb.SimPosition + move, 15.0f, true); - } - return; } @@ -758,7 +788,7 @@ namespace Barotrauma { footPos = new Vector2(colliderPos.X + stepSize.X * i * 0.2f, colliderPos.Y - 0.1f); } - if (Stairs == null) + if (Stairs == null && !onSlopeThatMakesSlow) { footPos.Y = Math.Max(Math.Min(FloorY, footPos.Y + 0.5f), footPos.Y); } @@ -1318,14 +1348,14 @@ namespace Barotrauma } } - void UpdateFallingProne(float strength) + void UpdateFallingProne(float strength, bool moveHands = true, bool moveTorso = true, bool moveLegs = true) { if (strength <= 0.0f) { return; } Limb head = GetLimb(LimbType.Head); Limb torso = GetLimb(LimbType.Torso); - if (head != null && head.LinearVelocity.LengthSquared() > 1.0f && !head.IsSevered) + if (moveHands && head != null && head.LinearVelocity.LengthSquared() > 1.0f && !head.IsSevered) { //if the head is moving, try to protect it with the hands Limb leftHand = GetLimb(LimbType.LeftHand); @@ -1347,7 +1377,7 @@ namespace Barotrauma //make the torso tip over //otherwise it tends to just drop straight down, pinning the characters legs in a weird pose - if (!InWater) + if (moveTorso && !InWater) { //prefer tipping over in the same direction the torso is rotating //or moving @@ -1358,27 +1388,30 @@ namespace Barotrauma } //attempt to make legs stay in a straight line with the torso to prevent the character from doing a split - for (int i = 0; i < 2; i++) + if (moveLegs) { - var thigh = i == 0 ? GetLimb(LimbType.LeftThigh) : GetLimb(LimbType.RightThigh); - if (thigh == null) { continue; } - if (thigh.IsSevered) { continue; } - float thighDiff = Math.Abs(MathUtils.GetShortestAngle(torso.Rotation, thigh.Rotation)); - float diff = torso.Rotation - thigh.Rotation; - if (MathUtils.IsValid(diff)) + for (int i = 0; i < 2; i++) { - float thighTorque = thighDiff * thigh.Mass * Math.Sign(diff) * 5.0f; - thigh.body.ApplyTorque(thighTorque * strength); - } + var thigh = i == 0 ? GetLimb(LimbType.LeftThigh) : GetLimb(LimbType.RightThigh); + if (thigh == null) { continue; } + if (thigh.IsSevered) { continue; } + float thighDiff = Math.Abs(MathUtils.GetShortestAngle(torso.Rotation, thigh.Rotation)); + float diff = torso.Rotation - thigh.Rotation; + if (MathUtils.IsValid(diff)) + { + float thighTorque = thighDiff * thigh.Mass * Math.Sign(diff) * 5.0f; + thigh.body.ApplyTorque(thighTorque * strength); + } - var leg = i == 0 ? GetLimb(LimbType.LeftLeg) : GetLimb(LimbType.RightLeg); - if (leg == null || leg.IsSevered) { continue; } - float legDiff = Math.Abs(MathUtils.GetShortestAngle(torso.Rotation, leg.Rotation)); - diff = torso.Rotation - leg.Rotation; - if (MathUtils.IsValid(diff)) - { - float legTorque = legDiff * leg.Mass * Math.Sign(diff) * 5.0f; - leg.body.ApplyTorque(legTorque * strength); + var leg = i == 0 ? GetLimb(LimbType.LeftLeg) : GetLimb(LimbType.RightLeg); + if (leg == null || leg.IsSevered) { continue; } + float legDiff = Math.Abs(MathUtils.GetShortestAngle(torso.Rotation, leg.Rotation)); + diff = torso.Rotation - leg.Rotation; + if (MathUtils.IsValid(diff)) + { + float legTorque = legDiff * leg.Mass * Math.Sign(diff) * 5.0f; + leg.body.ApplyTorque(legTorque * strength); + } } } } @@ -1398,7 +1431,8 @@ namespace Barotrauma Crouching = true; - Vector2 diff = target.SimPosition - character.SimPosition; + Vector2 offset = Vector2.UnitX * -Dir * 0.75f; + Vector2 diff = (target.SimPosition + offset) - character.SimPosition; Limb targetHead = target.AnimController.GetLimb(LimbType.Head); Limb targetTorso = target.AnimController.GetLimb(LimbType.Torso); if (targetTorso == null) @@ -1412,7 +1446,23 @@ namespace Barotrauma Vector2 headDiff = targetHead == null ? diff : targetHead.SimPosition - character.SimPosition; targetMovement = new Vector2(diff.X, 0.0f); + const float CloseEnough = 0.1f; + if (Math.Abs(targetMovement.X) < CloseEnough) + { + targetMovement.X = 0.0f; + } + TargetDir = headDiff.X > 0.0f ? Direction.Right : Direction.Left; + //if the target's in some weird pose, we may not be able to flip it so it's facing up, + //so let's only try it once so we don't end up constantly flipping it + if (cprAnimTimer <= 0.0f && target.AnimController.Direction == TargetDir) + { + target.AnimController.Flip(); + } + (target.AnimController as HumanoidAnimController)?.UpdateFallingProne(strength: 1.0f, moveHands: false, moveTorso: false); + + head.Disabled = true; + torso.Disabled = true; UpdateStanding(); @@ -1443,73 +1493,64 @@ namespace Barotrauma } } - //pump for 15 seconds (cprAnimTimer 0-15), then do mouth-to-mouth for 2 seconds (cprAnimTimer 15-17) - if (cprAnimTimer > 15.0f && targetHead != null && head != null) + //Serverside code + if (GameMain.NetworkMember is not { IsClient: true }) { - float yPos = (float)Math.Sin(cprAnimTimer) * 0.2f; - head.PullJointWorldAnchorB = new Vector2(targetHead.SimPosition.X, targetHead.SimPosition.Y + 0.3f + yPos); + if (target.Oxygen < -10.0f) + { + //stabilize the oxygen level but don't allow it to go positive and revive the character yet + float stabilizationAmount = skill * CPRSettings.Active.StabilizationPerSkill; + stabilizationAmount = MathHelper.Clamp(stabilizationAmount, CPRSettings.Active.StabilizationMin, CPRSettings.Active.StabilizationMax); + character.Oxygen -= 1.0f / stabilizationAmount * deltaTime; //Worse skill = more oxygen required + if (character.Oxygen > 0.0f) { target.Oxygen += stabilizationAmount * deltaTime; } //we didn't suffocate yet did we + } + } + + if (targetHead != null && head != null) + { + head.PullJointWorldAnchorB = new Vector2(targetHead.SimPosition.X, targetHead.SimPosition.Y + 0.8f); head.PullJointEnabled = true; - torso.PullJointWorldAnchorB = new Vector2(torso.SimPosition.X, colliderPos.Y + (TorsoPosition.Value - 0.2f)); - torso.PullJointEnabled = true; - - //Serverside code - if (GameMain.NetworkMember is not { IsClient: true }) - { - if (target.Oxygen < -10.0f) - { - //stabilize the oxygen level but don't allow it to go positive and revive the character yet - float stabilizationAmount = skill * CPRSettings.Active.StabilizationPerSkill; - stabilizationAmount = MathHelper.Clamp(stabilizationAmount, CPRSettings.Active.StabilizationMin, CPRSettings.Active.StabilizationMax); - character.Oxygen -= 1.0f / stabilizationAmount * deltaTime; //Worse skill = more oxygen required - if (character.Oxygen > 0.0f) { target.Oxygen += stabilizationAmount * deltaTime; } //we didn't suffocate yet did we - } - } } - else + + torso.PullJointWorldAnchorB = new Vector2(torso.SimPosition.X, colliderPos.Y + (TorsoPosition.Value - 0.1f)); + torso.PullJointEnabled = true; + + if (cprPumpTimer >= 1) { - if (targetHead != null && head != null) + torso.body.ApplyLinearImpulse(new Vector2(0, -20f), maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + targetTorso.body.ApplyLinearImpulse(new Vector2(0, -20f), maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + cprPumpTimer = 0; + + if (skill < CPRSettings.Active.DamageSkillThreshold) { - head.PullJointWorldAnchorB = new Vector2(targetHead.SimPosition.X, targetHead.SimPosition.Y + 0.8f); - head.PullJointEnabled = true; + target.LastDamageSource = null; + target.DamageLimb( + targetTorso.WorldPosition, targetTorso, + new[] { CPRSettings.Active.InsufficientSkillAffliction.Instantiate((CPRSettings.Active.DamageSkillThreshold - skill) * CPRSettings.Active.DamageSkillMultiplier, source: character) }, + stun: 0.0f, + playSound: true, + attackImpulse: Vector2.Zero, + attacker: null); } - - torso.PullJointWorldAnchorB = new Vector2(torso.SimPosition.X, colliderPos.Y + (TorsoPosition.Value - 0.1f)); - torso.PullJointEnabled = true; - - if (cprPump >= 1) + //need to CPR for at least a couple of seconds before the target can be revived + //(reviving the target when the CPR has barely started looks strange) + if (cprAnimTimer > 2.0f && GameMain.NetworkMember is not { IsClient: true }) { - torso.body.ApplyLinearImpulse(new Vector2(0, -20f), maxVelocity: NetConfig.MaxPhysicsBodyVelocity); - targetTorso.body.ApplyLinearImpulse(new Vector2(0, -20f), maxVelocity: NetConfig.MaxPhysicsBodyVelocity); - cprPump = 0; + float reviveChance = skill * CPRSettings.Active.ReviveChancePerSkill; + reviveChance = (float)Math.Pow(reviveChance, CPRSettings.Active.ReviveChanceExponent); + reviveChance = MathHelper.Clamp(reviveChance, CPRSettings.Active.ReviveChanceMin, CPRSettings.Active.ReviveChanceMax); + reviveChance *= 1f + cprBoost; - if (skill < CPRSettings.Active.DamageSkillThreshold) + if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) <= reviveChance) { - target.LastDamageSource = null; - target.DamageLimb( - targetTorso.WorldPosition, targetTorso, - new[] { CPRSettings.Active.InsufficientSkillAffliction.Instantiate((CPRSettings.Active.DamageSkillThreshold - skill) * CPRSettings.Active.DamageSkillMultiplier, source: character) }, - 0.0f, true, 0.0f, attacker: null); - } - if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) //Serverside code - { - float reviveChance = skill * CPRSettings.Active.ReviveChancePerSkill; - reviveChance = (float)Math.Pow(reviveChance, CPRSettings.Active.ReviveChanceExponent); - reviveChance = MathHelper.Clamp(reviveChance, CPRSettings.Active.ReviveChanceMin, CPRSettings.Active.ReviveChanceMax); - - reviveChance *= 1f + cprBoost; - - if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) <= reviveChance) - { - //increase oxygen and clamp it above zero - // -> the character should be revived if there are no major afflictions in addition to lack of oxygen - target.Oxygen = Math.Max(target.Oxygen + 10.0f, 10.0f); - } + //increase oxygen and clamp it above zero + // -> the character should be revived if there are no major afflictions in addition to lack of oxygen + target.Oxygen = Math.Max(target.Oxygen + 10.0f, 10.0f); } } - cprPump += deltaTime; } - - cprAnimTimer = (cprAnimTimer + deltaTime) % 17; + cprPumpTimer += deltaTime; + cprAnimTimer += deltaTime; //got the character back into a non-critical state, increase medical skill //BUT only if it has been more than 10 seconds since the character revived someone @@ -1519,7 +1560,7 @@ namespace Barotrauma target.CharacterHealth.CalculateVitality(); if (wasCritical && target.Vitality > 0.0f && Timing.TotalTime > lastReviveTime + 10.0f) { - character.Info?.IncreaseSkillLevel("medical".ToIdentifier(), SkillSettings.Current.SkillIncreasePerCprRevive); + character.Info?.ApplySkillGain(Tags.MedicalSkill, SkillSettings.Current.SkillIncreasePerCprRevive); SteamAchievementManager.OnCharacterRevived(target, character); lastReviveTime = (float)Timing.TotalTime; #if SERVER @@ -1724,7 +1765,23 @@ namespace Barotrauma targetAnchor += target.Submarine.SimPosition; } } - pullLimb.PullJointWorldAnchorB = pullLimbAnchor; + if (Vector2.DistanceSquared(pullLimb.PullJointWorldAnchorA, pullLimbAnchor) > 50.0f * 50.0f) + { + //there's a similar error check in the PullJointWorldAnchorB setter, but we seem to be getting quite a lot of + //errors specifically from this method, so let's use a more consistent error message here to prevent clogging GA with + //different error messages that all include a different coordinate + string errorMsg = + $"Attempted to move the anchor B of a limb's pull joint extremely far from the limb in {nameof(DragCharacter)}. " + + $"Character in sub: {character.Submarine != null}, target in sub: {target.Submarine != null}."; + GameAnalyticsManager.AddErrorEventOnce("DragCharacter:PullJointTooFar", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); +#if DEBUG + DebugConsole.ThrowError(errorMsg); +#endif + } + else + { + pullLimb.PullJointWorldAnchorB = pullLimbAnchor; + } } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index c9ac1f88b..ffebedae2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -437,7 +437,18 @@ namespace Barotrauma foreach (var huskAppendage in mainElement.GetChildElements("huskappendage")) { if (!inEditor && huskAppendage.GetAttributeBool("onlyfromafflictions", false)) { continue; } - AfflictionHusk.AttachHuskAppendage(character, huskAppendage.GetAttributeIdentifier("affliction", Identifier.Empty), huskAppendage, ragdoll: this); + + Identifier afflictionIdentifier = huskAppendage.GetAttributeIdentifier("affliction", Identifier.Empty); + if (!AfflictionPrefab.Prefabs.TryGet(afflictionIdentifier, out AfflictionPrefab affliction) || + affliction is not AfflictionPrefabHusk matchingAffliction) + { + DebugConsole.ThrowError($"Could not find an affliction of type 'huskinfection' that matches the affliction '{afflictionIdentifier}'!", + contentPackage: huskAppendage.ContentPackage); + } + else + { + AfflictionHusk.AttachHuskAppendage(character, matchingAffliction, huskAppendage, ragdoll: this); + } } } } @@ -561,6 +572,10 @@ namespace Barotrauma public void AddJoint(JointParams jointParams) { + if (!checkLimbIndex(jointParams.Limb2, "Limb1") || !checkLimbIndex(jointParams.Limb2, "Limb2")) + { + return; + } LimbJoint joint = new LimbJoint(Limbs[jointParams.Limb1], Limbs[jointParams.Limb2], jointParams, this); GameMain.World.Add(joint.Joint); for (int i = 0; i < LimbJoints.Length; i++) @@ -571,6 +586,21 @@ namespace Barotrauma } Array.Resize(ref LimbJoints, LimbJoints.Length + 1); LimbJoints[LimbJoints.Length - 1] = joint; + + bool checkLimbIndex(int index, string debugName) + { + if (index < 0 || index >= limbs.Length) + { + string errorMsg = $"Failed to add a joint to character {character.Name}. {debugName} out of bounds (index: {index}, limbs: {limbs.Length}."; + DebugConsole.ThrowError(errorMsg, contentPackage: jointParams.Element?.ContentPackage); + if (jointParams.Element?.ContentPackage == GameMain.VanillaContent) + { + GameAnalyticsManager.AddErrorEventOnce("Ragdoll.AddJoint:IndexOutOfRange", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + } + return false; + } + return true; + } } protected void AddLimb(LimbParams limbParams) @@ -657,6 +687,13 @@ namespace Barotrauma } } + private enum LimbStairCollisionResponse + { + DontClimbStairs, + ClimbWithoutLimbCollision, + ClimbWithLimbCollision + } + public bool OnLimbCollision(Fixture f1, Fixture f2, Contact contact) { if (f2.Body.UserData is Submarine && character.Submarine == (Submarine)f2.Body.UserData) { return false; } @@ -699,37 +736,53 @@ namespace Barotrauma } else if (structure.StairDirection != Direction.None) { - Stairs = null; - - //don't collider with stairs if - - //1. bottom of the collider is at the bottom of the stairs and the character isn't trying to move upwards - float stairBottomPos = ConvertUnits.ToSimUnits(structure.Rect.Y - structure.Rect.Height + 10); - if (colliderBottom.Y < stairBottomPos && targetMovement.Y < 0.5f) { return false; } - - //2. bottom of the collider is at the top of the stairs and the character isn't trying to move downwards - if (targetMovement.Y >= 0.0f && colliderBottom.Y >= ConvertUnits.ToSimUnits(structure.Rect.Y - Submarine.GridSize.Y * 5)) { return false; } - - //3. collided with the stairs from below - if (contact.Manifold.LocalNormal.Y < 0.0f) { return false; } - - //4. contact points is above the bottom half of the collider - contact.GetWorldManifold(out Vector2 normal, out FarseerPhysics.Common.FixedArray2 points); - if (points[0].Y > Collider.SimPosition.Y) { return false; } - - //5. in water - if (inWater && targetMovement.Y < 0.5f) { return false; } - - //--------------- - - //set stairs to that of the one dragging us if (character.SelectedBy != null) + { Stairs = character.SelectedBy.AnimController.Stairs; + } else - Stairs = structure; + { + var collisionResponse = handleLimbStairCollision(); + if (collisionResponse == LimbStairCollisionResponse.ClimbWithLimbCollision) + { + Stairs = structure; + } + else + { + if (collisionResponse == LimbStairCollisionResponse.DontClimbStairs) { Stairs = null; } - if (Stairs == null) - return false; + return false; + } + } + + LimbStairCollisionResponse handleLimbStairCollision() + { + //don't collide with stairs if + + //1. bottom of the collider is at the bottom of the stairs and the character isn't trying to move upwards + float stairBottomPos = ConvertUnits.ToSimUnits(structure.Rect.Y - structure.Rect.Height + 10); + if (colliderBottom.Y < stairBottomPos && targetMovement.Y < 0.5f) { return LimbStairCollisionResponse.DontClimbStairs; } + + //2. bottom of the collider is at the top of the stairs and the character isn't trying to move downwards + if (targetMovement.Y >= 0.0f && colliderBottom.Y >= ConvertUnits.ToSimUnits(structure.Rect.Y - Submarine.GridSize.Y * 5)) { return LimbStairCollisionResponse.DontClimbStairs; } + + //3. collided with the stairs from below + if (contact.Manifold.LocalNormal.Y < 0.0f) + { + return Stairs != structure + ? LimbStairCollisionResponse.DontClimbStairs + : LimbStairCollisionResponse.ClimbWithoutLimbCollision; + } + + //4. contact points is above the bottom half of the collider + contact.GetWorldManifold(out _, out FarseerPhysics.Common.FixedArray2 points); + if (points[0].Y > Collider.SimPosition.Y) { return LimbStairCollisionResponse.DontClimbStairs; } + + //5. in water + if (inWater && targetMovement.Y < 0.5f) { return LimbStairCollisionResponse.DontClimbStairs; } + + return LimbStairCollisionResponse.ClimbWithLimbCollision; + } } lock (impactQueue) @@ -1324,17 +1377,28 @@ namespace Barotrauma if (onGround && Collider.LinearVelocity.Y > -ImpactTolerance) { float targetY = standOnFloorY + ((float)Math.Abs(Math.Cos(Collider.Rotation)) * Collider.Height * 0.5f) + Collider.Radius + ColliderHeightFromFloor; - if (Math.Abs(Collider.SimPosition.Y - targetY) > 0.01f) + + const float LevitationSpeedMultiplier = 5f; + + // If the character is walking down a slope, target a position that moves along it + float slopePull = 0f; + if (floorNormal.Y is > 0f and < 1f + && Math.Sign(movement.X) == Math.Sign(floorNormal.X)) { - if (Stairs != null) + slopePull = Math.Abs(movement.X * floorNormal.X / floorNormal.Y) / LevitationSpeedMultiplier; + } + + if (Math.Abs(Collider.SimPosition.Y - targetY - slopePull) > 0.01f) + { + float yVelocity = (targetY - Collider.SimPosition.Y) * LevitationSpeedMultiplier; + if (Stairs != null && targetY < Collider.SimPosition.Y) { - Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, - (targetY < Collider.SimPosition.Y ? Math.Sign(targetY - Collider.SimPosition.Y) : (targetY - Collider.SimPosition.Y)) * 5.0f); - } - else - { - Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, (targetY - Collider.SimPosition.Y) * 5.0f); + yVelocity = Math.Sign(yVelocity); } + + yVelocity -= slopePull * LevitationSpeedMultiplier; + + Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, yVelocity); } } else @@ -1582,10 +1646,10 @@ namespace Barotrauma // Force check floor y at least once a second so that we'll drop through gaps that we are standing upon. private const float FloorYStaleTime = 1; private float floorYCheckTimer; - private void RefreshFloorY(float deltaTime, Limb refLimb = null, bool ignoreStairs = false) + private void RefreshFloorY(float deltaTime, bool ignoreStairs = false) { floorYCheckTimer -= deltaTime; - PhysicsBody refBody = refLimb == null ? Collider : refLimb.body; + PhysicsBody refBody = Collider; if (floorYCheckTimer < 0 || lastFloorCheckIgnoreStairs != ignoreStairs || lastFloorCheckIgnorePlatforms != IgnorePlatforms || @@ -1610,7 +1674,7 @@ namespace Barotrauma if (HeadPosition.HasValue && MathUtils.IsValid(HeadPosition.Value)) { height = Math.Max(height, HeadPosition.Value); } if (TorsoPosition.HasValue && MathUtils.IsValid(TorsoPosition.Value)) { height = Math.Max(height, TorsoPosition.Value); } - Vector2 rayEnd = rayStart - new Vector2(0.0f, height); + Vector2 rayEnd = rayStart - new Vector2(0.0f, height * 2f); Vector2 colliderBottomDisplay = ConvertUnits.ToDisplayUnits(GetColliderBottom()); Fixture standOnFloorFixture = null; @@ -1689,7 +1753,7 @@ namespace Barotrauma } } - if (closestFraction == 1) //raycast didn't hit anything + if (closestFraction >= 1) //raycast didn't hit anything { floorNormal = Vector2.UnitY; if (CurrentHull == null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index f3397c089..8bb5d51ff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -1,11 +1,12 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; -using Barotrauma.Items.Components; namespace Barotrauma -{ +{ public enum HitDetection { Distance, @@ -391,7 +392,8 @@ namespace Barotrauma element.GetAttribute("burndamage") != null || element.GetAttribute("bleedingdamage") != null) { - DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Define damage as afflictions instead of using the damage attribute (e.g. )."); + DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Define damage as afflictions instead of using the damage attribute (e.g. ).", + contentPackage: element.ContentPackage); } //if level wall damage is not defined, default to the structure damage @@ -414,12 +416,14 @@ namespace Barotrauma AfflictionPrefab afflictionPrefab; if (subElement.GetAttribute("name") != null) { - DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - define afflictions using identifiers instead of names."); + DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - define afflictions using identifiers instead of names.", + contentPackage: element.ContentPackage); string afflictionName = subElement.GetAttributeString("name", "").ToLowerInvariant(); afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Name.Equals(afflictionName, System.StringComparison.OrdinalIgnoreCase)); if (afflictionPrefab == null) { - DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionName + "\" not found."); + DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionName + "\" not found.", + contentPackage: element.ContentPackage); continue; } } @@ -428,7 +432,8 @@ namespace Barotrauma Identifier afflictionIdentifier = subElement.GetAttributeIdentifier("identifier", ""); if (!AfflictionPrefab.Prefabs.TryGet(afflictionIdentifier, out afflictionPrefab)) { - DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionIdentifier + "\" not found."); + DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionIdentifier + "\" not found.", + contentPackage: element.ContentPackage); continue; } } @@ -441,7 +446,7 @@ namespace Barotrauma } partial void InitProjSpecific(ContentXElement element); - public void ReloadAfflictions(XElement element, string parentDebugName) + public void ReloadAfflictions(ContentXElement element, string parentDebugName) { Afflictions.Clear(); foreach (var subElement in element.GetChildElements("affliction")) @@ -450,13 +455,14 @@ namespace Barotrauma Identifier afflictionIdentifier = subElement.GetAttributeIdentifier("identifier", ""); if (!AfflictionPrefab.Prefabs.TryGet(afflictionIdentifier, out AfflictionPrefab afflictionPrefab)) { - DebugConsole.ThrowError($"Error in an Attack defined in \"{parentDebugName}\" - could not find an affliction with the identifier \"{afflictionIdentifier}\"."); + DebugConsole.ThrowError($"Error in an Attack defined in \"{parentDebugName}\" - could not find an affliction with the identifier \"{afflictionIdentifier}\".", + contentPackage: element.ContentPackage); continue; } affliction = afflictionPrefab.Instantiate(0.0f); affliction.Deserialize(subElement); //backwards compatibility - if (subElement.Attribute("amount") != null && subElement.Attribute("strength") == null) + if (subElement.GetAttribute("amount") != null && subElement.GetAttribute("strength") == null) { affliction.Strength = subElement.GetAttributeFloat("amount", 0.0f); } @@ -465,7 +471,7 @@ namespace Barotrauma } } - public void Serialize(XElement element) + public void Serialize(ContentXElement element) { SerializableProperty.SerializeProperties(this, element, true); foreach (var affliction in Afflictions) @@ -477,7 +483,7 @@ namespace Barotrauma } } - public void Deserialize(XElement element, string parentDebugName) + public void Deserialize(ContentXElement element, string parentDebugName) { SerializableProperties = SerializableProperty.DeserializeProperties(this, element); ReloadAfflictions(element, parentDebugName); @@ -497,8 +503,9 @@ namespace Barotrauma SetUser(attacker); DamageParticles(deltaTime, worldPosition); - - var attackResult = target?.AddDamage(attacker, worldPosition, this, deltaTime, playSound) ?? new AttackResult(); + + Vector2 impulseDirection = GetImpulseDirection(target as ISpatialEntity, worldPosition, SourceItem); + var attackResult = target?.AddDamage(attacker, worldPosition, this, impulseDirection, deltaTime, playSound) ?? new AttackResult(); var conditionalEffectType = attackResult.Damage > 0.0f ? ActionType.OnSuccess : ActionType.OnFailure; var additionalEffectType = ActionType.OnUse; if (targetCharacter != null && targetCharacter.IsDead) @@ -606,7 +613,7 @@ namespace Barotrauma float penetration = Penetration; RangedWeapon weapon = - SourceItem?.GetComponent() ?? + SourceItem?.GetComponent() ?? SourceItem?.GetComponent()?.Launcher?.GetComponent(); float? penetrationValue = weapon?.Penetration; if (penetrationValue.HasValue) @@ -614,7 +621,8 @@ namespace Barotrauma penetration += penetrationValue.Value; } - var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, playSound, targetLimb, penetration); + Vector2 impulseDirection = GetImpulseDirection(targetLimb, worldPosition, SourceItem); + var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, impulseDirection, playSound, targetLimb, penetration); var conditionalEffectType = attackResult.Damage > 0.0f ? ActionType.OnSuccess : ActionType.OnFailure; foreach (StatusEffect effect in statusEffects) @@ -666,6 +674,34 @@ namespace Barotrauma return attackResult; } + private Vector2 GetImpulseDirection(ISpatialEntity target, Vector2 sourceWorldPosition, Item sourceItem) + { + Vector2 impulseDirection = Vector2.Zero; + if (target != null) + { + impulseDirection = target.WorldPosition - sourceWorldPosition; + } + + if (sourceItem?.body != null && sourceItem.body.Enabled && sourceItem.body.LinearVelocity.LengthSquared() > 0.0f) + { + impulseDirection = sourceItem.body.LinearVelocity; + } + else + { + var projectileComponent = sourceItem?.GetComponent(); + if (projectileComponent != null) + { + impulseDirection = new Vector2(MathF.Cos(SourceItem.Rotation), MathF.Sin(SourceItem.Rotation)); + } + } + + if (impulseDirection.LengthSquared() > 0.0001f) + { + impulseDirection = Vector2.Normalize(impulseDirection); + } + return impulseDirection; + } + public float AttackTimer { get; private set; } public float CoolDownTimer { get; set; } public float CurrentRandomCoolDown { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 679e76a1b..21ce51013 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -67,10 +67,18 @@ namespace Barotrauma } foreach (var item in HeldItems) { - if (item.body != null) + if (item.body == null) { continue; } + if (!enabled) { - item.body.Enabled = enabled; + item.body.Enabled = false; } + else if (item.GetComponent() is { IsActive: true }) + { + //held items includes all items in hand slots + //we only want to enable the physics body if it's an actual holdable item, not e.g. a wearable item like handcuffs + item.body.Enabled = true; + } + } AnimController.Collider.Enabled = value; } @@ -936,10 +944,16 @@ namespace Barotrauma { var prevSelectedItem = _selectedItem; _selectedItem = value; + if (value is not null) + { + CheckTalents(AbilityEffectType.OnItemSelected, new AbilityItemSelected(value)); + } #if CLIENT HintManager.OnSetSelectedItem(this, prevSelectedItem, _selectedItem); if (Controlled == this) { + _selectedItem?.GetComponent()?.RefreshSelectedItem(); + if (_selectedItem == null) { GameMain.GameSession?.CrewManager?.ResetCrewList(); @@ -1098,6 +1112,15 @@ namespace Barotrauma set { CharacterHealth.Unkillable = value; } } + /// + /// Is the health interface available on this character? Can be used by status effects + /// + public bool UseHealthWindow + { + get { return CharacterHealth.UseHealthWindow; } + set { CharacterHealth.UseHealthWindow = value; } + } + public CampaignMode.InteractionType CampaignInteractionType; public Identifier MerchantIdentifier; @@ -1278,7 +1301,8 @@ namespace Barotrauma { if (!VariantOf.IsEmpty) { - DebugConsole.ThrowError("The variant system does not yet support humans, sorry. It does support other humanoids though!"); + DebugConsole.ThrowError("The variant system does not yet support humans, sorry. It does support other humanoids though!", + contentPackage: Prefab.ContentPackage); } if (characterInfo == null) { @@ -1402,7 +1426,8 @@ namespace Barotrauma if (matchingAffliction == null || nonHuskedSpeciesName.IsEmpty) { DebugConsole.ThrowError($"Cannot find a husk infection that matches {speciesName}! Please make sure that the speciesname is added as 'targets' in the husk affliction prefab definition!\n" - + "Note that all the infected speciesnames and files must stick the following pattern: [nonhuskedspeciesname][huskedspeciesname]. E.g. Humanhusk, Crawlerhusk, or Humancustomhusk, or Crawlerzombie. Not \"Customhumanhusk!\" or \"Zombiecrawler\""); + + "Note that all the infected speciesnames and files must stick the following pattern: [nonhuskedspeciesname][huskedspeciesname]. E.g. Humanhusk, Crawlerhusk, or Humancustomhusk, or Crawlerzombie. Not \"Customhumanhusk!\" or \"Zombiecrawler\"", + contentPackage: Prefab.ContentPackage); // Crashes if we fail to create a ragdoll -> Let's just use some ragdoll so that the user sees the error msg. nonHuskedSpeciesName = IsHumanoid ? CharacterPrefab.HumanSpeciesName : "crawler".ToIdentifier(); speciesName = nonHuskedSpeciesName; @@ -1683,31 +1708,21 @@ namespace Barotrauma info.Job?.GiveJobItems(this, spawnPoint); } - - public void GiveIdCardTags(WayPoint spawnPoint, bool requireSpawnPointTagsNotGiven = true, bool createNetworkEvent = false) + public void GiveIdCardTags(WayPoint spawnPoint, bool createNetworkEvent = false) { - GiveIdCardTags(spawnPoint.ToEnumerable(), requireSpawnPointTagsNotGiven, createNetworkEvent); - } - - public void GiveIdCardTags(IEnumerable spawnPoints, bool requireSpawnPointTagsNotGiven = true, bool createNetworkEvent = false) - { - if (info?.Job == null || spawnPoints == null) { return; } + if (info?.Job == null || spawnPoint == null) { return; } foreach (Item item in Inventory.AllItems) { - if (item?.GetComponent() is not IdCard idCard) { continue; } - if (requireSpawnPointTagsNotGiven) + var idCard = item?.GetComponent(); + if (idCard == null) { continue; } + //if the card belongs to someone else, don't add any tags. + //otherwise you can gain access to places you shouldn't by temporarily giving the card to someone (e.g. a captain bot) at the end of the round + if (idCard.OwnerName != info.Name) { continue; } + foreach (string s in spawnPoint.IdCardTags) { - if (idCard.SpawnPointTagsGiven) { continue; } + item.AddTag(s); } - foreach (var spawnPoint in spawnPoints) - { - foreach (string s in spawnPoint.IdCardTags) - { - item.AddTag(s); - } - } - idCard.SpawnPointTagsGiven = true; if (createNetworkEvent && GameMain.NetworkMember is { IsServer: true }) { GameMain.NetworkMember.CreateEntityEvent(item, new Item.ChangePropertyEventData(item.SerializableProperties[nameof(item.Tags).ToIdentifier()], item)); @@ -2292,40 +2307,40 @@ namespace Barotrauma return AnimController.GetLimb(LimbType.Head) ?? AnimController.GetLimb(LimbType.Torso) ?? AnimController.MainLimb; } - public bool CanSeeTarget(ISpatialEntity target, ISpatialEntity seeingEntity = null, bool checkFacing = false) + public bool CanSeeTarget(ISpatialEntity target, ISpatialEntity seeingEntity = null, bool seeThroughWindows = false, bool checkFacing = false) { seeingEntity ??= AnimController.SimplePhysicsEnabled ? this : GetSeeingLimb(); if (target is Character targetCharacter) { - return IsCharacterVisible(targetCharacter, seeingEntity, checkFacing); + return IsCharacterVisible(targetCharacter, seeingEntity, seeThroughWindows, checkFacing); } else { - return CheckVisibility(target, seeingEntity, checkFacing); + return CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing); } } - public static bool IsTargetVisible(ISpatialEntity target, ISpatialEntity seeingEntity, bool checkFacing = false) + public static bool IsTargetVisible(ISpatialEntity target, ISpatialEntity seeingEntity, bool seeThroughWindows = false, bool checkFacing = false) { if (seeingEntity is Character seeingCharacter) { - return seeingCharacter.CanSeeTarget(target, checkFacing: checkFacing); + return seeingCharacter.CanSeeTarget(target, seeThroughWindows: seeThroughWindows, checkFacing: checkFacing); } if (target is Character targetCharacter) { - return IsCharacterVisible(targetCharacter, seeingEntity, checkFacing); + return IsCharacterVisible(targetCharacter, seeingEntity, seeThroughWindows, checkFacing); } else { - return CheckVisibility(target, seeingEntity, checkFacing); + return CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing); } } - private static bool IsCharacterVisible(Character target, ISpatialEntity seeingEntity, bool checkFacing = false) + private static bool IsCharacterVisible(Character target, ISpatialEntity seeingEntity, bool seeThroughWindows = false, bool checkFacing = false) { System.Diagnostics.Debug.Assert(target != null); if (target == null || target.Removed) { return false; } - if (CheckVisibility(target, seeingEntity, checkFacing)) { return true; } + if (CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing)) { return true; } if (!target.AnimController.SimplePhysicsEnabled) { //find the limbs that are furthest from the target's position (from the viewer's point of view) @@ -2354,13 +2369,13 @@ namespace Barotrauma continue; } } - if (leftExtremity != null && CheckVisibility(leftExtremity, seeingEntity, checkFacing)) { return true; } - if (rightExtremity != null && CheckVisibility(rightExtremity, seeingEntity, checkFacing)) { return true; } + if (leftExtremity != null && CheckVisibility(leftExtremity, seeingEntity, seeThroughWindows, checkFacing)) { return true; } + if (rightExtremity != null && CheckVisibility(rightExtremity, seeingEntity, seeThroughWindows, checkFacing)) { return true; } } return false; } - private static bool CheckVisibility(ISpatialEntity target, ISpatialEntity seeingEntity, bool checkFacing = false) + private static bool CheckVisibility(ISpatialEntity target, ISpatialEntity seeingEntity, bool seeThroughWindows = true, bool checkFacing = false) { System.Diagnostics.Debug.Assert(target != null); if (target == null) { return false; } @@ -2371,38 +2386,41 @@ namespace Barotrauma { if (Math.Sign(diff.X) != seeingCharacter.AnimController.Dir) { return false; } } - Body closestBody; //both inside the same sub (or both outside) //OR the we're inside, the other character outside if (target.Submarine == seeingEntity.Submarine || target.Submarine == null) { - closestBody = Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff); + return Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff, blocksVisibilityPredicate: IsBlocking) == null; } //we're outside, the other character inside else if (seeingEntity.Submarine == null) { - closestBody = Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff); + return Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff, blocksVisibilityPredicate: IsBlocking) == null; } //both inside different subs else { - closestBody = Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff); - if (!IsBlocking(closestBody)) - { - closestBody = Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff); - } + return + Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff, blocksVisibilityPredicate: IsBlocking) == null && + Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff, blocksVisibilityPredicate: IsBlocking) == null; } - return !IsBlocking(closestBody); - bool IsBlocking(Body body) + bool IsBlocking(Fixture f) { + var body = f.Body; if (body == null) { return false; } - if (body.UserData is Structure wall && wall.CastShadow) + if (body.UserData is Structure wall) { + if (!wall.CastShadow && seeThroughWindows) { return false; } return wall != target; } else if (body.UserData is Item item) { + if (item.GetComponent() is { HasWindow: true } door && seeThroughWindows) + { + if (door.IsPositionOnWindow(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition))) { return false; } + } + return item != target; } return true; @@ -2497,9 +2515,21 @@ namespace Barotrauma if (inventory.Owner is Item item) { - if (!CanInteractWith(item) && !item.linkedTo.Any(lt => lt is Item item && item.DisplaySideBySideWhenLinked && CanInteractWith(item))) { return false; } - ItemContainer container = item.GetComponents().FirstOrDefault(ic => ic.Inventory == inventory); - if (container != null && !container.HasRequiredItems(this, addMessage: false)) { return false; } + if (!CanInteractWith(item)) + { + //could be simplified with LINQ, but that'd require capturing variables which we shouldn't do in a method that's called as frequently as this + foreach (var linkedEntity in item.linkedTo) + { + if (linkedEntity is Item linkedItem && linkedItem.DisplaySideBySideWhenLinked && CanInteractWith(linkedItem)) { return true; } + } + return false; + } + ItemContainer container = (inventory as ItemInventory)?.Container; + if (container != null) + { + if (!container.HasRequiredItems(this, addMessage: false)) { return false; } + if (!container.DrawInventory) { return false; } + } } return true; } @@ -2764,9 +2794,17 @@ namespace Barotrauma if (!item.Prefab.InteractThroughWalls && Screen.Selected != GameMain.SubEditorScreen && !insideTrigger) { var body = Submarine.CheckVisibility(SimPosition, itemPosition, ignoreLevel: true); - if (body != null && body.UserData as Item != item && (body.UserData as ItemComponent)?.Item != item && Submarine.LastPickedFixture?.UserData as Item != item) - { - return false; + if (body != null) + { + 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; + } } } @@ -2793,7 +2831,12 @@ namespace Barotrauma public void DeselectCharacter() { if (SelectedCharacter == null) { return; } - SelectedCharacter.AnimController?.ResetPullJoints(); + if (!SelectedCharacter.AllowInput) + { + //we cannot reset the pull joints if the target is conscious (moving on its own), + //that'd interfere with its animations + SelectedCharacter.AnimController?.ResetPullJoints(); + } SelectedCharacter = null; } @@ -3297,10 +3340,7 @@ namespace Barotrauma IsRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves if (wasRagdolled != IsRagdolled) { ragdollingLockTimer = 0.2f; } } - if (IsRagdolled) - { - SetInput(InputType.Ragdoll, false, true); - } + SetInput(InputType.Ragdoll, false, IsRagdolled); } if (!wasRagdolled && IsRagdolled) { @@ -3558,6 +3598,8 @@ namespace Barotrauma private void Despawn(bool createNetworkEvents = true) { + if (!EnableDespawn) { return; } + Identifier despawnContainerId = IsHuman ? "despawncontainer".ToIdentifier() : @@ -3639,10 +3681,12 @@ namespace Barotrauma float massFactor = (float)Math.Sqrt(Mass / 20); float targetRange = Math.Min(minRange + massFactor * AnimController.Collider.LinearVelocity.Length() * 2 * Visibility, maxAIRange); float newRange = MathHelper.SmoothStep(aiTarget.SightRange, targetRange, deltaTime * aiTargetChangeSpeed); + newRange *= 1.0f + GetStatValue(StatTypes.SightRangeMultiplier); if (!float.IsNaN(newRange)) { aiTarget.SightRange = newRange; } + } private void UpdateSoundRange(float deltaTime) @@ -3657,6 +3701,7 @@ namespace Barotrauma float massFactor = (float)Math.Sqrt(Mass / 10); float targetRange = Math.Min(massFactor * AnimController.Collider.LinearVelocity.Length() * 2 * Noise, maxAIRange); float newRange = MathHelper.SmoothStep(aiTarget.SoundRange, targetRange, deltaTime * aiTargetChangeSpeed); + newRange *= 1.0f + GetStatValue(StatTypes.SoundRangeMultiplier); if (!float.IsNaN(newRange)) { aiTarget.SoundRange = newRange; @@ -3976,15 +4021,15 @@ namespace Barotrauma CharacterHealth.SetAllDamage(damageAmount, bleedingDamageAmount, burnDamageAmount); } - public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = true) + public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, Vector2 impulseDirection, float deltaTime, bool playSound = true) { - return ApplyAttack(attacker, worldPosition, attack, deltaTime, playSound, null); + return ApplyAttack(attacker, worldPosition, attack, deltaTime, impulseDirection, playSound); } /// /// Apply the specified attack to this character. If the targetLimb is not specified, the limb closest to worldPosition will receive the damage. /// - public AttackResult ApplyAttack(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = false, Limb targetLimb = null, float penetration = 0f) + public AttackResult ApplyAttack(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, Vector2 impulseDirection, bool playSound = false, Limb targetLimb = null, float penetration = 0f) { if (Removed) { @@ -3996,7 +4041,16 @@ namespace Barotrauma Limb limbHit = targetLimb; - float attackImpulse = attack.TargetImpulse + attack.TargetForce * attack.ImpactMultiplier * deltaTime; + float impulseMagnitude = (attack.TargetImpulse + attack.TargetForce * attack.ImpactMultiplier) * deltaTime; + + Vector2 attackImpulse = Vector2.Zero; + if (Math.Abs(impulseMagnitude) > 0.0f) + { + impulseDirection = impulseDirection.LengthSquared() > 0.0001f ? + Vector2.Normalize(impulseDirection) : + Vector2.UnitX; + attackImpulse = impulseDirection * impulseMagnitude; + } AbilityAttackData attackData = new AbilityAttackData(attack, this, attacker); IEnumerable attackAfflictions; @@ -4125,12 +4179,12 @@ namespace Barotrauma } } - public AttackResult AddDamage(Vector2 worldPosition, IEnumerable afflictions, float stun, bool playSound, float attackImpulse = 0.0f, Character attacker = null, float damageMultiplier = 1f) + public AttackResult AddDamage(Vector2 worldPosition, IEnumerable afflictions, float stun, bool playSound, Vector2? attackImpulse = null, Character attacker = null, float damageMultiplier = 1f) { - return AddDamage(worldPosition, afflictions, stun, playSound, attackImpulse, out _, attacker, damageMultiplier: damageMultiplier); + return AddDamage(worldPosition, afflictions, stun, playSound, attackImpulse ?? Vector2.Zero, out _, attacker, damageMultiplier: damageMultiplier); } - public AttackResult AddDamage(Vector2 worldPosition, IEnumerable afflictions, float stun, bool playSound, float attackImpulse, out Limb hitLimb, Character attacker = null, float damageMultiplier = 1) + public AttackResult AddDamage(Vector2 worldPosition, IEnumerable afflictions, float stun, bool playSound, Vector2 attackImpulse, out Limb hitLimb, Character attacker = null, float damageMultiplier = 1) { hitLimb = null; @@ -4163,7 +4217,7 @@ namespace Barotrauma CreatureMetrics.RecordKill(target.SpeciesName); } - public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable afflictions, float stun, bool playSound, float attackImpulse, Character attacker = null, float damageMultiplier = 1, bool allowStacking = true, float penetration = 0f, bool shouldImplode = false) + public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable afflictions, float stun, bool playSound, Vector2 attackImpulse, Character attacker = null, float damageMultiplier = 1, bool allowStacking = true, float penetration = 0f, bool shouldImplode = false) { if (Removed) { return new AttackResult(); } @@ -4196,18 +4250,17 @@ namespace Barotrauma } Vector2 dir = hitLimb.WorldPosition - worldPosition; - if (Math.Abs(attackImpulse) > 0.0f) + if (attackImpulse.LengthSquared() > 0.0f) { Vector2 diff = dir; if (diff == Vector2.Zero) { diff = Rand.Vector(1.0f); } - Vector2 impulse = Vector2.Normalize(diff) * attackImpulse; Vector2 hitPos = hitLimb.SimPosition + ConvertUnits.ToSimUnits(diff); - hitLimb.body.ApplyLinearImpulse(impulse, hitPos, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.5f); + hitLimb.body.ApplyLinearImpulse(attackImpulse, hitPos, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.5f); var mainLimb = hitLimb.character.AnimController.MainLimb; if (hitLimb != mainLimb) { // Always add force to mainlimb - mainLimb.body.ApplyLinearImpulse(impulse, hitPos, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + mainLimb.body.ApplyLinearImpulse(attackImpulse, hitPos, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); } } bool wasDead = IsDead; @@ -4306,19 +4359,16 @@ namespace Barotrauma } if (medicalDamage > 0) { - IncreaseSkillLevel("medical".ToIdentifier(), medicalDamage); + IncreaseSkillLevel(Tags.MedicalSkill, medicalDamage); } if (weaponDamage > 0) { - IncreaseSkillLevel("weapons".ToIdentifier(), weaponDamage); + IncreaseSkillLevel(Tags.WeaponsSkill, weaponDamage); } void IncreaseSkillLevel(Identifier skill, float damage) { - float attackerSkillLevel = attacker.GetSkillLevel(skill); - // The formula is too generous on low skill levels, hence the minimum divider. - float minSkillDivider = 15f; - attacker.Info?.IncreaseSkillLevel(skill, damage * SkillSettings.Current.SkillIncreasePerHostileDamage / Math.Max(attackerSkillLevel, minSkillDivider)); + attacker.Info?.ApplySkillGain(skill, damage * SkillSettings.Current.SkillIncreasePerHostileDamage, false, 1f); } } @@ -4332,12 +4382,10 @@ namespace Barotrauma { medicalGain += affliction.Strength * affliction.Prefab.MedicalSkillGain; } - if (medicalGain <= 0) { return; } - Identifier skill = new Identifier("medical"); - float attackerSkillLevel = healer.GetSkillLevel(skill); - // The formula is too generous on low skill levels, hence the minimum divider. - float minSkillDivider = 15f; - healer.Info?.IncreaseSkillLevel(skill, medicalGain * SkillSettings.Current.SkillIncreasePerFriendlyHealed / Math.Max(attackerSkillLevel, minSkillDivider)); + if (medicalGain > 0) + { + healer.Info?.ApplySkillGain(Tags.MedicalItem, medicalGain * SkillSettings.Current.SkillIncreasePerFriendlyHealed); + } } /// @@ -4971,8 +5019,10 @@ namespace Barotrauma private readonly List visibleHulls = new List(); private readonly HashSet tempList = new HashSet(); + /// - /// Returns hulls that are visible to the player, including the current hull. + /// Returns hulls that are visible to the character, including the current hull. + /// Note that this is not an accurate visibility check, it only checks for open gaps between the adjacent and linked hulls. /// Can be heavy if used every frame. /// public List GetVisibleHulls() @@ -4986,7 +5036,9 @@ namespace Barotrauma float maxDistance = 1000f; foreach (var hull in adjacentHulls) { - if (hull.ConnectedGaps.Any(g => g.Open > 0.9f && g.linkedTo.Contains(CurrentHull) && + if (hull.ConnectedGaps.Any(g => + g.Open > 0.9f && + g.linkedTo.Contains(CurrentHull) && Vector2.DistanceSquared(g.WorldPosition, WorldPosition) < Math.Pow(maxDistance / 2, 2))) { if (Vector2.DistanceSquared(hull.WorldPosition, WorldPosition) < Math.Pow(maxDistance, 2)) @@ -5027,7 +5079,7 @@ namespace Barotrauma public bool IsEngineer => HasJob("engineer"); public bool IsMechanic => HasJob("mechanic"); public bool IsMedic => HasJob("medicaldoctor"); - public bool IsSecurity => HasJob("securityofficer") || HasJob("vipsecurityofficer"); + public bool IsSecurity => HasJob("securityofficer") || HasJob("vipsecurityofficer") || HasJob("outpostsecurityofficer"); public bool IsAssistant => HasJob("assistant"); public bool IsWatchman => HasJob("watchman"); public bool IsVip => HasJob("prisoner"); @@ -5546,4 +5598,12 @@ namespace Barotrauma public Character Character { get; set; } } + class AbilityItemSelected : AbilityObject, IAbilityItem + { + public AbilityItemSelected(Item item) + { + Item = item; + } + public Item Item { get; set; } + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index 61f4df9c7..2b8466406 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -767,7 +767,7 @@ namespace Barotrauma } // Used for loading the data - public CharacterInfo(XElement infoElement, Identifier npcIdentifier = default) + public CharacterInfo(ContentXElement infoElement, Identifier npcIdentifier = default) { ID = idCounter; idCounter++; @@ -1214,6 +1214,21 @@ namespace Barotrauma return (int)(salary * Job.Prefab.PriceMultiplier); } + /// + /// Increases the characters skill at a rate proportional to their current skill. + /// If you want to increase the skill level by a specific amount instead, use + /// + public void ApplySkillGain(Identifier skillIdentifier, float baseGain, bool gainedFromAbility = false, float maxGain = 2f) + { + float skillLevel = Job.GetSkillLevel(skillIdentifier); + // The formula is too generous on low skill levels, hence the minimum divider. + float skillDivider = MathF.Pow(Math.Max(skillLevel, 15f), SkillSettings.Current.SkillIncreaseExponent); + IncreaseSkillLevel(skillIdentifier, Math.Min(baseGain / skillDivider, maxGain), gainedFromAbility); + } + + /// + /// Increase the skill by a specific amount. Talents may affect the actual, final skill increase. + /// public void IncreaseSkillLevel(Identifier skillIdentifier, float increase, bool gainedFromAbility = false) { if (Job == null || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) || Character == null) { return; } @@ -1222,9 +1237,7 @@ namespace Barotrauma { increase *= SkillSettings.Current.AssistantSkillIncreaseMultiplier; } - increase *= 1f + Character.GetStatValue(StatTypes.SkillGainSpeed); - increase = GetSkillSpecificGain(increase, skillIdentifier); float prevLevel = Job.GetSkillLevel(skillIdentifier); @@ -1311,12 +1324,12 @@ namespace Barotrauma OnExperienceChanged(prevAmount, ExperiencePoints); } - const int BaseExperienceRequired = -50; + const int BaseExperienceRequired = 450; const int AddedExperienceRequiredPerLevel = 500; public int GetTotalTalentPoints() { - return GetCurrentLevel() + AdditionalTalentPoints - 1; + return GetCurrentLevel() + AdditionalTalentPoints; } public int GetAvailableTalentPoints() @@ -1342,16 +1355,19 @@ namespace Barotrauma return experienceRequired + ExperienceRequiredPerLevel(level); } + /// + /// How much more experience does the character need to reach the specified level? + /// public int GetExperienceRequiredForLevel(int level) { - int currentLevel = GetCurrentLevel(out int experienceRequired); + int currentLevel = GetCurrentLevel(); if (currentLevel >= level) { return 0; } - int required = experienceRequired; - for (int i = currentLevel + 1; i <= level; i++) + int required = 0; + for (int i = 0; i < level; i++) { required += ExperienceRequiredPerLevel(i); } - return required; + return required - ExperiencePoints; } public int GetCurrentLevel() @@ -1361,7 +1377,7 @@ namespace Barotrauma private int GetCurrentLevel(out int experienceRequired) { - int level = 1; + int level = 0; experienceRequired = 0; while (experienceRequired + ExperienceRequiredPerLevel(level) <= ExperiencePoints) { @@ -1899,13 +1915,30 @@ namespace Barotrauma } } + /// + /// Get the combined stat value of the identifier "all" and the specified identifier. + /// + /// + /// The "all" identifier works like the "any" identifier in outpost modules where it doesn't literally mean everything but + /// is an unique identifier that indicates that it should target everything. For example if we wanted to make a talent + /// that increases the fabrication quality of every single item we could use something like: + /// + /// (Granted IncreaseFabricationQuality doesn't support the "all" identifier so if we need this in vanilla it needs to be implemented in code) + /// + public float GetSavedStatValueWithAll(StatTypes statType, Identifier statIdentifier) + => GetSavedStatValue(statType, Tags.StatIdentifierTargetAll) + + GetSavedStatValue(statType, statIdentifier); + public float GetSavedStatValueWithBotsInMp(StatTypes statType, Identifier statIdentifier) + => GetSavedStatValueWithBotsInMp(statType, statIdentifier, GameSession.GetSessionCrewCharacters(CharacterType.Bot)); + + public float GetSavedStatValueWithBotsInMp(StatTypes statType, Identifier statIdentifier, IReadOnlyCollection bots) { float statValue = GetSavedStatValue(statType, statIdentifier); if (GameMain.NetworkMember is null) { return statValue; } - foreach (Character bot in GameSession.GetSessionCrewCharacters(CharacterType.Bot)) + foreach (Character bot in bots) { int botStatValue = (int)bot.Info.GetSavedStatValue(statType, statIdentifier); statValue = Math.Max(statValue, botStatValue); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs index 2dfb6ecf5..acae26e4a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs @@ -95,7 +95,8 @@ namespace Barotrauma name = ParseName(mainElement, file); if (name == Identifier.Empty) { - DebugConsole.ThrowError($"No species name defined for: {file.Path}"); + DebugConsole.ThrowError($"No species name defined for: {file.Path}", + contentPackage: file.ContentPackage); return false; } return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index 20a19d64f..4136b6381 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -75,7 +75,8 @@ namespace Barotrauma HuskPrefab = prefab as AfflictionPrefabHusk; if (HuskPrefab == null) { - DebugConsole.ThrowError("Error in husk affliction definition: the prefab is of wrong type!"); + DebugConsole.ThrowError("Error in husk affliction definition: the prefab is of wrong type!", + contentPackage: prefab.ContentPackage); } } @@ -197,7 +198,7 @@ namespace Barotrauma huskInfection.Add(AfflictionPrefab.InternalDamage.Instantiate(random * 10 * deltaTime / limbCount)); character.LastDamageSource = null; float force = applyForce ? random * 0.5f * limb.Mass : 0; - character.DamageLimb(limb.WorldPosition, limb, huskInfection, 0, false, force); + character.DamageLimb(limb.WorldPosition, limb, huskInfection, 0, false, Rand.Vector(force)); } } @@ -205,7 +206,7 @@ namespace Barotrauma { if (huskAppendage == null && character.Params.UseHuskAppendage) { - huskAppendage = AttachHuskAppendage(character, Prefab.Identifier); + huskAppendage = AttachHuskAppendage(character, Prefab as AfflictionPrefabHusk); } if (Prefab is AfflictionPrefabHusk { NeedsAir: false }) @@ -285,13 +286,14 @@ namespace Barotrauma if (prefab == null) { - DebugConsole.ThrowError("Failed to turn character \"" + character.Name + "\" into a husk - husk config file not found."); + DebugConsole.ThrowError("Failed to turn character \"" + character.Name + "\" into a husk - husk config file not found.", + contentPackage: Prefab.ContentPackage); yield return CoroutineStatus.Success; } XElement parentElement = new XElement("CharacterInfo"); XElement infoElement = character.Info?.Save(parentElement); - CharacterInfo huskCharacterInfo = infoElement == null ? null : new CharacterInfo(infoElement); + CharacterInfo huskCharacterInfo = infoElement == null ? null : new CharacterInfo(new ContentXElement(Prefab.ContentPackage, infoElement)); if (huskCharacterInfo != null) { @@ -371,31 +373,28 @@ namespace Barotrauma yield return CoroutineStatus.Success; } - public static List AttachHuskAppendage(Character character, Identifier afflictionIdentifier, ContentXElement appendageDefinition = null, Ragdoll ragdoll = null) + public static List AttachHuskAppendage(Character character, AfflictionPrefabHusk matchingAffliction, ContentXElement appendageDefinition = null, Ragdoll ragdoll = null) { var appendage = new List(); - if (!(AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier == afflictionIdentifier) is AfflictionPrefabHusk matchingAffliction)) - { - DebugConsole.ThrowError($"Could not find an affliction of type 'huskinfection' that matches the affliction '{afflictionIdentifier}'!"); - return appendage; - } Identifier nonhuskedSpeciesName = GetNonHuskedSpeciesName(character.SpeciesName, matchingAffliction); Identifier huskedSpeciesName = GetHuskedSpeciesName(nonhuskedSpeciesName, matchingAffliction); CharacterPrefab huskPrefab = CharacterPrefab.FindBySpeciesName(huskedSpeciesName); if (huskPrefab?.ConfigElement == null) { - DebugConsole.ThrowError($"Failed to find the config file for the husk infected species with the species name '{huskedSpeciesName}'!"); + DebugConsole.ThrowError($"Failed to find the config file for the husk infected species with the species name '{huskedSpeciesName}'!", + contentPackage: matchingAffliction.ContentPackage); return appendage; } var mainElement = huskPrefab.ConfigElement; var element = appendageDefinition; if (element == null) { - element = mainElement.GetChildElements("huskappendage").FirstOrDefault(e => e.GetAttributeIdentifier("affliction", Identifier.Empty) == afflictionIdentifier); + element = mainElement.GetChildElements("huskappendage").FirstOrDefault(e => e.GetAttributeIdentifier("affliction", Identifier.Empty) == matchingAffliction.Identifier); } if (element == null) { - DebugConsole.ThrowError($"Error in '{huskPrefab.FilePath}': Failed to find a huskappendage that matches the affliction with an identifier '{afflictionIdentifier}'!"); + DebugConsole.ThrowError($"Error in '{huskPrefab.FilePath}': Failed to find a huskappendage that matches the affliction with an identifier '{matchingAffliction.Identifier}'!", + contentPackage: matchingAffliction.ContentPackage); return appendage; } ContentPath pathToAppendage = element.GetAttributeContentPath("path") ?? ContentPath.Empty; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 5d46b8207..035a8c9ae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -170,11 +170,13 @@ namespace Barotrauma if (DormantThreshold > ActiveThreshold) { - DebugConsole.ThrowError($"Error in \"{Identifier}\": {nameof(DormantThreshold)} is greater than {nameof(ActiveThreshold)} ({DormantThreshold} > {ActiveThreshold})"); + DebugConsole.ThrowError($"Error in \"{Identifier}\": {nameof(DormantThreshold)} is greater than {nameof(ActiveThreshold)} ({DormantThreshold} > {ActiveThreshold})", + contentPackage: element.ContentPackage); } if (ActiveThreshold > TransitionThreshold) { - DebugConsole.ThrowError($"Error in \"{Identifier}\": {nameof(ActiveThreshold)} is greater than {nameof(TransitionThreshold)} ({ActiveThreshold} > {TransitionThreshold})"); + DebugConsole.ThrowError($"Error in \"{Identifier}\": {nameof(ActiveThreshold)} is greater than {nameof(TransitionThreshold)} ({ActiveThreshold} > {TransitionThreshold})", + contentPackage: element.ContentPackage); } TransformThresholdOnDeath = element.GetAttributeFloat("transformthresholdondeath", ActiveThreshold); @@ -440,13 +442,15 @@ namespace Barotrauma AbilityFlags flagType = subElement.GetAttributeEnum("flagtype", AbilityFlags.None); if (flagType is AbilityFlags.None) { - DebugConsole.ThrowError($"Error in affliction \"{parentDebugName}\" - invalid ability flag type \"{subElement.GetAttributeString("flagtype", "")}\"."); + DebugConsole.ThrowError($"Error in affliction \"{parentDebugName}\" - invalid ability flag type \"{subElement.GetAttributeString("flagtype", "")}\".", + contentPackage: element.ContentPackage); continue; } AfflictionAbilityFlags |= flagType; break; case "affliction": - DebugConsole.AddWarning($"Error in affliction \"{parentDebugName}\" - additional afflictions caused by the affliction should be configured inside status effects."); + DebugConsole.AddWarning($"Error in affliction \"{parentDebugName}\" - additional afflictions caused by the affliction should be configured inside status effects.", + contentPackage: element.ContentPackage); break; } } @@ -537,14 +541,16 @@ namespace Barotrauma } else if (TextTag.IsEmpty) { - DebugConsole.ThrowError($"Error in affliction \"{affliction.Identifier}\" - no text defined for one of the descriptions."); + DebugConsole.ThrowError($"Error in affliction \"{affliction.Identifier}\" - no text defined for one of the descriptions.", + contentPackage: element.ContentPackage); } MinStrength = element.GetAttributeFloat(nameof(MinStrength), 0.0f); MaxStrength = element.GetAttributeFloat(nameof(MaxStrength), 100.0f); if (MinStrength >= MaxStrength) { - DebugConsole.ThrowError($"Error in affliction \"{affliction.Identifier}\" - max strength is not larger than min."); + DebugConsole.ThrowError($"Error in affliction \"{affliction.Identifier}\" - max strength is not larger than min.", + contentPackage: element.ContentPackage); } Target = element.GetAttributeEnum(nameof(Target), TargetType.Any); } @@ -953,7 +959,8 @@ namespace Barotrauma AfflictionOverlay = new Sprite(subElement); break; case "statvalue": - DebugConsole.ThrowError($"Error in affliction \"{Identifier}\" - stat values should be configured inside the affliction's effects."); + DebugConsole.ThrowError($"Error in affliction \"{Identifier}\" - stat values should be configured inside the affliction's effects.", + contentPackage: element.ContentPackage); break; case "effect": case "periodiceffect": @@ -962,7 +969,8 @@ namespace Barotrauma descriptions.Add(new Description(subElement, this)); break; default: - DebugConsole.AddWarning($"Unrecognized element in affliction \"{Identifier}\" ({subElement.Name})"); + DebugConsole.AddWarning($"Unrecognized element in affliction \"{Identifier}\" ({subElement.Name})", + contentPackage: element.ContentPackage); break; } } @@ -1018,6 +1026,10 @@ namespace Barotrauma } } + /// + /// Removes all the effects of the prefab (including the sounds and other assets defined in them). + /// Note that you need to call LoadAllEffectsAndTreatmentSuitabilities before trying to use the affliction again! + /// public static void ClearAllEffects() { Prefabs.ForEach(p => p.ClearEffects()); @@ -1046,7 +1058,8 @@ namespace Barotrauma var b = effects[j]; if (a.MinStrength < b.MaxStrength && b.MinStrength < a.MaxStrength) { - DebugConsole.AddWarning($"Affliction \"{Identifier}\" contains effects with overlapping strength ranges. Only one effect can be active at a time, meaning one of the effects won't work."); + DebugConsole.AddWarning($"Affliction \"{Identifier}\" contains effects with overlapping strength ranges. Only one effect can be active at a time, meaning one of the effects won't work.", + ContentPackage); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index d1092d476..7e813b74a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -49,7 +49,8 @@ namespace Barotrauma case "vitalitymultiplier": if (subElement.GetAttribute("name") != null) { - DebugConsole.ThrowError("Error in character health config (" + characterHealth.Character.Name + ") - define vitality multipliers using affliction identifiers or types instead of names."); + DebugConsole.ThrowError("Error in character health config (" + characterHealth.Character.Name + ") - define vitality multipliers using affliction identifiers or types instead of names.", + contentPackage: element.ContentPackage); continue; } var vitalityMultipliers = subElement.GetAttributeIdentifierArray("identifier", null) ?? subElement.GetAttributeIdentifierArray("identifiers", null); @@ -61,7 +62,8 @@ namespace Barotrauma VitalityMultipliers.Add(vitalityMultiplier, multiplier); if (AfflictionPrefab.Prefabs.None(p => p.Identifier == vitalityMultiplier)) { - DebugConsole.AddWarning($"Potentially incorrectly defined vitality multiplier in \"{characterHealth.Character.Name}\". Could not find any afflictions with the identifier \"{vitalityMultiplier}\". Did you mean to define the afflictions by type instead?"); + DebugConsole.AddWarning($"Potentially incorrectly defined vitality multiplier in \"{characterHealth.Character.Name}\". Could not find any afflictions with the identifier \"{vitalityMultiplier}\". Did you mean to define the afflictions by type instead?", + contentPackage: element.ContentPackage); } } } @@ -74,13 +76,15 @@ namespace Barotrauma VitalityTypeMultipliers.Add(vitalityTypeMultiplier, multiplier); if (AfflictionPrefab.Prefabs.None(p => p.AfflictionType == vitalityTypeMultiplier)) { - DebugConsole.AddWarning($"Potentially incorrectly defined vitality multiplier in \"{characterHealth.Character.Name}\". Could not find any afflictions of the type \"{vitalityTypeMultiplier}\". Did you mean to define the afflictions by identifier instead?"); + DebugConsole.AddWarning($"Potentially incorrectly defined vitality multiplier in \"{characterHealth.Character.Name}\". Could not find any afflictions of the type \"{vitalityTypeMultiplier}\". Did you mean to define the afflictions by identifier instead?", + contentPackage: element.ContentPackage); } } } if (vitalityMultipliers == null && VitalityTypeMultipliers == null) { - DebugConsole.ThrowError($"Error in character health config {characterHealth.Character.Name}: affliction identifier(s) or type(s) not defined in the \"VitalityMultiplier\" elements!"); + DebugConsole.ThrowError($"Error in character health config {characterHealth.Character.Name}: affliction identifier(s) or type(s) not defined in the \"VitalityMultiplier\" elements!", + contentPackage: element.ContentPackage); } break; } @@ -1309,6 +1313,8 @@ namespace Barotrauma public void Remove() { RemoveProjSpecific(); + afflictionsToRemove.Clear(); + afflictionsToUpdate.Clear(); } partial void RemoveProjSpecific(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs index 03fb3ad7d..dcdd8ddb4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs @@ -79,12 +79,13 @@ namespace Barotrauma public ref readonly ImmutableArray ParsedAfflictionTypes => ref parsedAfflictionTypes; - public DamageModifier(XElement element, string parentDebugName, bool checkErrors = true) + public DamageModifier(ContentXElement element, string parentDebugName, bool checkErrors = true) { Deserialize(element); - if (element.Attribute("afflictionnames") != null) + if (element.GetAttribute("afflictionnames") != null) { - DebugConsole.ThrowError("Error in DamageModifier config (" + parentDebugName + ") - define afflictions using identifiers or types instead of names."); + DebugConsole.ThrowError("Error in DamageModifier config (" + parentDebugName + ") - define afflictions using identifiers or types instead of names.", + contentPackage: element.ContentPackage); } if (checkErrors) { @@ -108,12 +109,12 @@ namespace Barotrauma } } - static void createWarningOrError(string msg) + void createWarningOrError(string msg) { #if DEBUG - DebugConsole.ThrowError(msg); + DebugConsole.ThrowError(msg, contentPackage: element.ContentPackage); #else - DebugConsole.AddWarning(msg); + DebugConsole.AddWarning(msg, contentPackage: element.ContentPackage); #endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index d5c10cbef..630e18265 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs @@ -117,8 +117,8 @@ namespace Barotrauma public XElement Element { get; protected set; } - public readonly List<(XElement element, float commonness)> ItemSets = new List<(XElement element, float commonness)>(); - public readonly List<(XElement element, float commonness)> CustomCharacterInfos = new List<(XElement element, float commonness)>(); + public readonly List<(ContentXElement element, float commonness)> ItemSets = new List<(ContentXElement element, float commonness)>(); + public readonly List<(ContentXElement element, float commonness)> CustomCharacterInfos = new List<(ContentXElement element, float commonness)>(); public readonly Identifier NpcSetIdentifier; @@ -196,7 +196,7 @@ namespace Barotrauma var spawnItems = ToolBox.SelectWeightedRandom(ItemSets, it => it.commonness, randSync).element; if (spawnItems != null) { - foreach (XElement itemElement in spawnItems.GetChildElements("item")) + foreach (ContentXElement itemElement in spawnItems.GetChildElements("item")) { int amount = itemElement.GetAttributeInt("amount", 1); for (int i = 0; i < amount; i++) @@ -239,14 +239,15 @@ namespace Barotrauma return characterInfo; } - public static void InitializeItem(Character character, XElement itemElement, Submarine submarine, HumanPrefab humanPrefab, WayPoint spawnPoint = null, Item parentItem = null, bool createNetworkEvents = true) + public static void InitializeItem(Character character, ContentXElement itemElement, Submarine submarine, HumanPrefab humanPrefab, WayPoint spawnPoint = null, Item parentItem = null, bool createNetworkEvents = true) { ItemPrefab itemPrefab; string itemIdentifier = itemElement.GetAttributeString("identifier", ""); itemPrefab = MapEntityPrefab.FindByIdentifier(itemIdentifier.ToIdentifier()) as ItemPrefab; if (itemPrefab == null) { - DebugConsole.ThrowError("Tried to spawn \"" + humanPrefab?.Identifier + "\" with the item \"" + itemIdentifier + "\". Matching item prefab not found."); + DebugConsole.ThrowError("Tried to spawn \"" + humanPrefab?.Identifier + "\" with the item \"" + itemIdentifier + "\". Matching item prefab not found.", + contentPackage: itemElement?.ContentPackage); return; } Item item = new Item(itemPrefab, character.Position, null); @@ -301,7 +302,7 @@ namespace Barotrauma wifiComponent.TeamID = character.TeamID; } parentItem?.Combine(item, user: null); - foreach (XElement childItemElement in itemElement.Elements()) + foreach (ContentXElement childItemElement in itemElement.Elements()) { int amount = childItemElement.GetAttributeInt("amount", 1); for (int i = 0; i < amount; i++) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs index 887743c78..411ff78b0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs @@ -47,13 +47,14 @@ namespace Barotrauma } } - public Job(XElement element) + public Job(ContentXElement element) { Identifier identifier = element.GetAttributeIdentifier("identifier", ""); JobPrefab p; if (!JobPrefab.Prefabs.ContainsKey(identifier)) { - DebugConsole.ThrowError($"Could not find the job {identifier}. Giving the character a random job."); + DebugConsole.ThrowError($"Could not find the job {identifier}. Giving the character a random job.", + contentPackage: element.ContentPackage); p = JobPrefab.Random(Rand.RandSync.Unsynced); } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs index e311800bc..e93e38566 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs @@ -43,7 +43,8 @@ namespace Barotrauma Priority = element.GetAttributeFloat("priority", -1f); if (Priority < 0) { - DebugConsole.AddWarning($"The 'priority' attribute is missing from the the item repair priorities definition in {element} of {file.Path}."); + DebugConsole.AddWarning($"The 'priority' attribute is missing from the the item repair priorities definition in {element} of {file.Path}.", + ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs index f2859aaa2..d88356d60 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs @@ -266,7 +266,8 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"[AnimationParams] Failed to load an animation {a} at {selectedFile} of type {animType} for the character {speciesName}"); + DebugConsole.ThrowError($"[AnimationParams] Failed to load an animation {a} at {selectedFile} of type {animType} for the character {speciesName}", + contentPackage: characterPrefab.ContentPackage); } return a; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs index e902ba353..6a5c7aab6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs @@ -63,7 +63,8 @@ namespace Barotrauma { if (!character.AnimController.CanWalk) { - DebugConsole.ThrowError($"{character.SpeciesName} cannot use run animations!"); + DebugConsole.ThrowError($"{character.SpeciesName} cannot use run animations!", + contentPackage: character.Prefab.ContentPackage); return false; } return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index c3042739a..7da09bc6b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -136,6 +136,12 @@ namespace Barotrauma public readonly List DamageEmitters = new List(); public readonly List Inventories = new List(); public HealthParams Health { get; private set; } + /// + /// Parameters for EnemyAIController. Not used by HumanAIController. + /// + /// + /// AIParams or null. Use , if you don't expect nulls. + /// public AIParams AI { get; private set; } public CharacterParams(CharacterFile file) @@ -559,7 +565,8 @@ namespace Barotrauma DebugConsole.AddWarning($"Character \"{character.SpeciesName}\" has a negative crush depth. "+ "Previously the crush depths were defined as display units (e.g. -30000 would correspond to 300 meters below the level), "+ "but now they're in meters (e.g. 3000 would correspond to a depth of 3000 meters displayed on the nav terminal). "+ - $"Changing the crush depth from {CrushDepth} to {newCrushDepth}."); + $"Changing the crush depth from {CrushDepth} to {newCrushDepth}.", + element.ContentPackage); CrushDepth = newCrushDepth; } } @@ -602,7 +609,8 @@ namespace Barotrauma public void AddItem(string identifier = null) { - identifier = identifier ?? ""; + if (Element == null) { return; } + identifier ??= ""; var element = CreateElement("item", new XAttribute("identifier", identifier)); Element.Add(element); var item = new InventoryItem(element, Character); @@ -711,7 +719,8 @@ namespace Barotrauma if (HasTag(tag)) { target = null; - DebugConsole.AddWarning($"Trying to add multiple targets with the same tag ('{tag}') defined! Only the first will be used!"); + DebugConsole.AddWarning($"Trying to add multiple targets with the same tag ('{tag}') defined! Only the first will be used!", + targetElement.ContentPackage); return false; } else @@ -730,6 +739,11 @@ namespace Barotrauma public bool TryAddNewTarget(Identifier tag, AIState state, float priority, out TargetParams targetParams) { + if (Element == null) + { + targetParams = null; + return false; + } var element = TargetParams.CreateNewElement(Character, tag, state, priority); if (TryAddTarget(element, out targetParams)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs index 03f56b285..72ad781a8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs @@ -84,12 +84,14 @@ namespace Barotrauma doc = XMLExtensions.TryLoadXml(Path); if (doc == null) { - DebugConsole.ThrowError("[EditableParams] The document is null! Failed to load the parameters."); + DebugConsole.ThrowError("[EditableParams] The document is null! Failed to load the parameters.", + contentPackage: file.ContentPackage); return false; } if (MainElement == null) { - DebugConsole.ThrowError("[EditableParams] The main element is null! Failed to load the parameters."); + DebugConsole.ThrowError("[EditableParams] The main element is null! Failed to load the parameters.", + contentPackage: file.ContentPackage); return false; } IsLoaded = Deserialize(MainElement); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs index 9187430ae..26e8ccfec 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs @@ -106,7 +106,8 @@ namespace Barotrauma CharacterPrefab prefab = CharacterPrefab.Find(p => p.Identifier == speciesName && (contentPackage == null || p.ContentFile.ContentPackage == contentPackage)); if (prefab?.ConfigElement == null) { - DebugConsole.ThrowError($"Failed to find config file for '{speciesName}' (content package {contentPackage?.Name ?? "null"})"); + DebugConsole.ThrowError($"Failed to find config file for '{speciesName}'", + contentPackage: contentPackage); return string.Empty; } return GetFolder(prefab.ConfigElement, prefab.ContentFile.Path.Value); @@ -183,7 +184,8 @@ namespace Barotrauma } if (error != null) { - DebugConsole.ThrowError(error); + DebugConsole.ThrowError(error, + contentPackage: prefab?.ContentPackage); } } if (selectedFile == null) @@ -444,7 +446,8 @@ namespace Barotrauma { if (source.MainElement == null) { - DebugConsole.ThrowError("[RagdollParams] The source XML Element of the given RagdollParams is null!"); + DebugConsole.ThrowError("[RagdollParams] The source XML Element of the given RagdollParams is null!", + contentPackage: source.MainElement?.ContentPackage); return; } Deserialize(source.MainElement, alsoChildren: false); @@ -453,7 +456,8 @@ namespace Barotrauma // TODO: cannot currently undo joint/limb deletion. if (sourceSubParams.Count != subParams.Count) { - DebugConsole.ThrowError("[RagdollParams] The count of the sub params differs! Failed to revert to the previous snapshot! Please reset the ragdoll to undo the changes."); + DebugConsole.ThrowError("[RagdollParams] The count of the sub params differs! Failed to revert to the previous snapshot! Please reset the ragdoll to undo the changes.", + contentPackage: source.MainElement?.ContentPackage); return; } for (int i = 0; i < subParams.Count; i++) @@ -461,7 +465,8 @@ namespace Barotrauma var subSubParams = subParams[i].SubParams; if (subSubParams.Count != sourceSubParams[i].SubParams.Count) { - DebugConsole.ThrowError("[RagdollParams] The count of the sub sub params differs! Failed to revert to the previous snapshot! Please reset the ragdoll to undo the changes."); + DebugConsole.ThrowError("[RagdollParams] The count of the sub sub params differs! Failed to revert to the previous snapshot! Please reset the ragdoll to undo the changes.", + contentPackage: source.MainElement?.ContentPackage); return; } subParams[i].Deserialize(sourceSubParams[i].Element, recursive: false); @@ -890,14 +895,14 @@ namespace Barotrauma #if CLIENT public DecorativeSprite DecorativeSprite { get; private set; } - public override bool Deserialize(XElement element = null, bool recursive = true) + public override bool Deserialize(ContentXElement element = null, bool recursive = true) { base.Deserialize(element, recursive); DecorativeSprite.SerializableProperties = SerializableProperty.DeserializeProperties(DecorativeSprite, element ?? Element); return SerializableProperties != null; } - public override bool Serialize(XElement element = null, bool recursive = true) + public override bool Serialize(ContentXElement element = null, bool recursive = true) { base.Serialize(element, recursive); SerializableProperty.SerializeProperties(DecorativeSprite, element ?? Element); @@ -985,7 +990,8 @@ namespace Barotrauma deformation = new PositionalDeformationParams(deformationElement); break; default: - DebugConsole.ThrowError($"SpriteDeformationParams not implemented: '{typeName}'"); + DebugConsole.ThrowError($"SpriteDeformationParams not implemented: '{typeName}'", + contentPackage: element.ContentPackage); break; } if (deformation != null) @@ -1000,14 +1006,14 @@ namespace Barotrauma #if CLIENT public Dictionary Deformations { get; private set; } - public override bool Deserialize(XElement element = null, bool recursive = true) + public override bool Deserialize(ContentXElement element = null, bool recursive = true) { base.Deserialize(element, recursive); Deformations.ForEach(d => d.Key.SerializableProperties = SerializableProperty.DeserializeProperties(d.Key, d.Value)); return SerializableProperties != null; } - public override bool Serialize(XElement element = null, bool recursive = true) + public override bool Serialize(ContentXElement element = null, bool recursive = true) { base.Serialize(element, recursive); Deformations.ForEach(d => SerializableProperty.SerializeProperties(d.Key, d.Value)); @@ -1098,14 +1104,14 @@ namespace Barotrauma } #if CLIENT - public override bool Deserialize(XElement element = null, bool recursive = true) + public override bool Deserialize(ContentXElement element = null, bool recursive = true) { base.Deserialize(element, recursive); LightSource.Deserialize(element ?? Element); return SerializableProperties != null; } - public override bool Serialize(XElement element = null, bool recursive = true) + public override bool Serialize(ContentXElement element = null, bool recursive = true) { base.Serialize(element, recursive); LightSource.Serialize(element ?? Element); @@ -1130,14 +1136,14 @@ namespace Barotrauma Attack = new Attack(element, ragdoll.SpeciesName.Value); } - public override bool Deserialize(XElement element = null, bool recursive = true) + public override bool Deserialize(ContentXElement element = null, bool recursive = true) { base.Deserialize(element, recursive); Attack.Deserialize(element ?? Element, parentDebugName: Ragdoll?.SpeciesName.ToString() ?? "null"); return SerializableProperties != null; } - public override bool Serialize(XElement element = null, bool recursive = true) + public override bool Serialize(ContentXElement element = null, bool recursive = true) { base.Serialize(element, recursive); Attack.Serialize(element ?? Element); @@ -1182,14 +1188,14 @@ namespace Barotrauma DamageModifier = new DamageModifier(element, ragdoll.SpeciesName.Value); } - public override bool Deserialize(XElement element = null, bool recursive = true) + public override bool Deserialize(ContentXElement element = null, bool recursive = true) { base.Deserialize(element, recursive); DamageModifier.Deserialize(element ?? Element); return SerializableProperties != null; } - public override bool Serialize(XElement element = null, bool recursive = true) + public override bool Serialize(ContentXElement element = null, bool recursive = true) { base.Serialize(element, recursive); DamageModifier.Serialize(element ?? Element); @@ -1218,7 +1224,7 @@ namespace Barotrauma public virtual string Name { get; set; } public Dictionary SerializableProperties { get; private set; } public ContentXElement Element { get; set; } - public XElement OriginalElement { get; protected set; } + public ContentXElement OriginalElement { get; protected set; } public List SubParams { get; set; } = new List(); public RagdollParams Ragdoll { get; private set; } @@ -1230,14 +1236,14 @@ namespace Barotrauma public SubParam(ContentXElement element, RagdollParams ragdoll) { Element = element; - OriginalElement = new XElement(element); + OriginalElement = new ContentXElement(element.ContentPackage, element); Ragdoll = ragdoll; SerializableProperties = SerializableProperty.DeserializeProperties(this, element); } - public virtual bool Deserialize(XElement element = null, bool recursive = true) + public virtual bool Deserialize(ContentXElement element = null, bool recursive = true) { - element = element ?? Element; + element ??= Element; SerializableProperties = SerializableProperty.DeserializeProperties(this, element); if (recursive) { @@ -1246,9 +1252,9 @@ namespace Barotrauma return SerializableProperties != null; } - public virtual bool Serialize(XElement element = null, bool recursive = true) + public virtual bool Serialize(ContentXElement element = null, bool recursive = true) { - element = element ?? Element; + element ??= Element; SerializableProperty.SerializeProperties(this, element, true); if (recursive) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs index 579ef14a8..a28080a0f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs @@ -100,6 +100,13 @@ namespace Barotrauma set; } + [Serialize(1.5f, IsPropertySaveable.Yes)] + public float SkillIncreaseExponent + { + get; + set; + } + public SkillSettings(XElement element, SkillSettingsFile file) : base(file, "SkillSettings".ToIdentifier()) { SerializableProperties = SerializableProperty.DeserializeProperties(this, element); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityCondition.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityCondition.cs index ad4b4df25..5b74ede1e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityCondition.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityCondition.cs @@ -13,7 +13,7 @@ namespace Barotrauma.Abilities public AbilityCondition(CharacterTalent characterTalent, ContentXElement conditionElement) { - this.characterTalent = characterTalent; + this.characterTalent = characterTalent ?? throw new ArgumentNullException(nameof(characterTalent)); character = characterTalent.Character; invert = conditionElement.GetAttributeBool("invert", false); } @@ -40,7 +40,8 @@ namespace Barotrauma.Abilities { if (!Enum.TryParse(targetTypeString, true, out TargetType targetType)) { - DebugConsole.ThrowError("Invalid target type type \"" + targetTypeString + "\" in CharacterTalent (" + characterTalent.DebugIdentifier + ")"); + DebugConsole.ThrowError("Invalid target type type \"" + targetTypeString + "\" in CharacterTalent (" + characterTalent.DebugIdentifier + ")", + contentPackage: characterTalent.Prefab.ContentPackage); } targetTypes.Add(targetType); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAffliction.cs index ebd077561..961cfcf71 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAffliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAffliction.cs @@ -1,7 +1,4 @@ -using Barotrauma.Items.Components; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; +using System.Linq; namespace Barotrauma.Abilities { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs index 77cc53bad..905adea68 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs @@ -34,7 +34,8 @@ namespace Barotrauma.Abilities string weaponTypeStr = conditionElement.GetAttributeString("weapontype", "Any"); if (!Enum.TryParse(weaponTypeStr, ignoreCase: true, out weapontype)) { - DebugConsole.ThrowError($"Error in talent \"{characterTalent.DebugIdentifier}\": \"{weaponTypeStr}\" is not a valid weapon type."); + DebugConsole.ThrowError($"Error in talent \"{characterTalent.DebugIdentifier}\": \"{weaponTypeStr}\" is not a valid weapon type.", + contentPackage: conditionElement.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs index 26af153b2..e887d7b99 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Abilities { @@ -11,13 +10,19 @@ namespace Barotrauma.Abilities private readonly List conditionals = new List(); + /// + /// If enabled, the conditional is checked on the target of the ability (e.g. the character that was killed if the effect type is OnKillCharacter). + /// Defaults to true, except in the case of , which by default targets the character who has the talent. + /// + private readonly bool targetAbilityTarget = false; + public AbilityConditionCharacter(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { targetTypes = ParseTargetTypes( conditionElement.GetAttributeStringArray("targettypes", conditionElement.GetAttributeStringArray("targettype", Array.Empty()))); - foreach (XElement subElement in conditionElement.Elements()) + foreach (ContentXElement subElement in conditionElement.Elements()) { if (subElement.NameAsIdentifier() == "conditional") { @@ -25,29 +30,47 @@ namespace Barotrauma.Abilities } } - if (!targetTypes.Any() && !conditionals.Any()) + //don't log this error if this is a subclass of AbilityConditionCharacter + //(in that case not having any conditionals here is ok) + if (!targetTypes.Any() && !conditionals.Any() && GetType() == typeof(AbilityConditionCharacter)) { - DebugConsole.ThrowError($"Error in talent \"{characterTalent}\". No target types or conditionals defined - the condition will match any character."); + DebugConsole.ThrowError($"Error in talent \"{characterTalent}\". No target types or conditionals defined - the condition will match any character.", + contentPackage: conditionElement.ContentPackage); } + + targetAbilityTarget = conditionElement.GetAttributeBool(nameof(targetAbilityTarget), this is not AbilityConditionHasPermanentStat); } - protected override bool MatchesConditionSpecific(AbilityObject abilityObject) + public sealed override bool MatchesCondition() { - if (abilityObject is IAbilityCharacter abilityCharacter) + //by default data-reliant conditions don't accept null, but in this case it's ok, + //because we can assume it's the character who has the talent + return MatchesCondition(abilityObject: null); + } + + public sealed override bool MatchesCondition(AbilityObject abilityObject) + { + return invert ? !MatchesConditionSpecific(abilityObject) : MatchesConditionSpecific(abilityObject); + } + + protected sealed override bool MatchesConditionSpecific(AbilityObject abilityObject) + { + Character targetCharacter = + targetAbilityTarget ? + (abilityObject as IAbilityCharacter)?.Character ?? character : + character; + if (targetCharacter is null) { return false; } + if (!IsViableTarget(targetTypes, targetCharacter)) { return false; } + foreach (var conditional in conditionals) { - if (abilityCharacter.Character is not Character character) { return false; } - if (!IsViableTarget(targetTypes, character)) { return false; } - foreach (var conditional in conditionals) - { - if (!conditional.Matches(character)) { return false; } - } - return true; - } - else - { - LogAbilityConditionError(abilityObject, typeof(IAbilityCharacter)); - return false; + if (!conditional.Matches(targetCharacter)) { return false; } } + return MatchesCharacter(targetCharacter); + } + + protected virtual bool MatchesCharacter(Character character) + { + return true; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacterNotLooted.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacterNotLooted.cs index a79830e5b..3dfbe7808 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacterNotLooted.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacterNotLooted.cs @@ -1,6 +1,6 @@ namespace Barotrauma.Abilities { - internal sealed class AbilityConditionCharacterNotLooted : AbilityConditionData + internal sealed class AbilityConditionCharacterNotLooted : AbilityConditionCharacter { private readonly Identifier identifier; @@ -9,11 +9,9 @@ namespace Barotrauma.Abilities identifier = conditionElement.GetAttributeIdentifier("identifier", Identifier.Empty); } - protected override bool MatchesConditionSpecific(AbilityObject abilityObject) + protected override bool MatchesCharacter(Character character) { - if (abilityObject is not IAbilityCharacter ability) { return false; } - - return !ability.Character.MarkedAsLooted.Contains(identifier); + return character != null &&!character.MarkedAsLooted.Contains(identifier); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacterUnconcious.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacterUnconcious.cs index 2809f3546..5646497c2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacterUnconcious.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacterUnconcious.cs @@ -2,15 +2,13 @@ namespace Barotrauma.Abilities { - internal sealed class AbilityConditionCharacterUnconcious : AbilityConditionData + internal sealed class AbilityConditionCharacterUnconcious : AbilityConditionCharacter { public AbilityConditionCharacterUnconcious(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { } - protected override bool MatchesConditionSpecific(AbilityObject abilityObject) + protected override bool MatchesCharacter(Character character) { - if (abilityObject is not IAbilityCharacter targetCharacter) { return false; } - - return targetCharacter.Character.IsUnconscious; + return character is { IsUnconscious: true }; } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs index 426156bec..c8d3495de 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs @@ -17,13 +17,15 @@ namespace Barotrauma.Abilities protected void LogAbilityConditionError(AbilityObject abilityObject, Type expectedData) { - DebugConsole.ThrowError($"Used data-reliant ability condition when data is incompatible! Expected {expectedData}, but received {abilityObject} in talent {characterTalent.DebugIdentifier}"); + DebugConsole.ThrowError($"Used data-reliant ability condition when data is incompatible! Expected {expectedData}, but received {abilityObject} in talent {characterTalent.DebugIdentifier}", + contentPackage: characterTalent.Prefab.ContentPackage); } protected abstract bool MatchesConditionSpecific(AbilityObject abilityObject); public override bool MatchesCondition() { - DebugConsole.ThrowError($"Used data-reliant ability condition in a state-based ability in talent {characterTalent.DebugIdentifier}! This is not allowed."); + DebugConsole.ThrowError($"Used data-reliant ability condition in a state-based ability in talent {characterTalent.DebugIdentifier}! This is not allowed.", + contentPackage: characterTalent.Prefab.ContentPackage); return false; } public override bool MatchesCondition(AbilityObject abilityObject) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs index e4580fadd..309c60d80 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs @@ -19,7 +19,8 @@ namespace Barotrauma.Abilities if (identifiers.None() && tags.None() && category == MapEntityCategory.None) { - DebugConsole.ThrowError($"Error in talent \"{characterTalent}\". No identifiers, tags or category defined."); + DebugConsole.ThrowError($"Error in talent \"{characterTalent}\". No identifiers, tags or category defined.", + contentPackage: conditionElement.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemIsStatic.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemIsStatic.cs new file mode 100644 index 000000000..f14f65fbb --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemIsStatic.cs @@ -0,0 +1,19 @@ +using Barotrauma.Items.Components; + +namespace Barotrauma.Abilities +{ + class AbilityConditionItemIsStatic : AbilityConditionData + { + public AbilityConditionItemIsStatic(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { } + + protected override bool MatchesConditionSpecific(AbilityObject abilityObject) + { + if (abilityObject is IAbilityItem { Item: var item }) + { + return item.GetComponent() is null && item.GetComponent() is null; + } + + return false; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs index f92523e10..71ae4f7dd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs @@ -22,7 +22,8 @@ namespace Barotrauma.Abilities { if (!isAffiliated) { - DebugConsole.ThrowError($"Error in AbilityConditionMission \"{characterTalent.DebugIdentifier}\" - \"{missionTypeString}\" is not a valid mission type."); + DebugConsole.ThrowError($"Error in AbilityConditionMission \"{characterTalent.DebugIdentifier}\" - \"{missionTypeString}\" is not a valid mission type.", + contentPackage: conditionElement.ContentPackage); } continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs index 237e15b5f..e9e68472f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs @@ -1,6 +1,8 @@ -namespace Barotrauma.Abilities +using System; + +namespace Barotrauma.Abilities { - class AbilityConditionHasPermanentStat : AbilityConditionDataless + class AbilityConditionHasPermanentStat : AbilityConditionCharacter { private readonly Identifier statIdentifier; private readonly StatTypes statType; @@ -12,7 +14,8 @@ statIdentifier = conditionElement.GetAttributeIdentifier("statidentifier", Identifier.Empty); if (statIdentifier.IsEmpty) { - DebugConsole.ThrowError($"No stat identifier defined for {this} in talent {characterTalent.DebugIdentifier}!"); + DebugConsole.ThrowError($"No stat identifier defined for {this} in talent {characterTalent.DebugIdentifier}!", + contentPackage: conditionElement.ContentPackage); } string statTypeName = conditionElement.GetAttributeString("stattype", string.Empty); statType = string.IsNullOrEmpty(statTypeName) ? StatTypes.None : CharacterAbilityGroup.ParseStatType(statTypeName, characterTalent.DebugIdentifier); @@ -20,8 +23,13 @@ placeholder = conditionElement.GetAttributeEnum("placeholder", PermanentStatPlaceholder.None); } - protected override bool MatchesConditionSpecific() + protected override bool MatchesCharacter(Character character) { + if (character?.Info == null) + { + DebugConsole.AddWarning($"Error in {nameof(AbilityConditionHasPermanentStat.MatchesCharacter)}: character {character} has no CharacterInfo. Are you trying to use the condition on a non-player character?\n{Environment.StackTrace.CleanupStackTrace()}"); + return false; + } Identifier identifier = CharacterAbilityGivePermanentStat.HandlePlaceholders(placeholder, statIdentifier); return character.Info.GetSavedStatValue(statType, identifier) >= min; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasStatusTag.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasStatusTag.cs index 2ee0a66ee..c9a48c4eb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasStatusTag.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasStatusTag.cs @@ -13,7 +13,8 @@ namespace Barotrauma.Abilities tag = conditionElement.GetAttributeIdentifier("tag", Identifier.Empty); if (tag.IsEmpty) { - DebugConsole.AddWarning($"Error in talent \"{characterTalent.Prefab.OriginalName}\" - tag not defined in AbilityConditionHasStatusTag."); + DebugConsole.AddWarning($"Error in talent \"{characterTalent.Prefab.OriginalName}\" - tag not defined in AbilityConditionHasStatusTag.", + characterTalent.Prefab.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionLowestLevel.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionLowestLevel.cs index 3cf37c2b9..fb6749419 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionLowestLevel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionLowestLevel.cs @@ -2,21 +2,18 @@ namespace Barotrauma.Abilities { - internal sealed class AbilityConditionLowestLevel : AbilityConditionDataless + internal sealed class AbilityConditionLowestLevel : AbilityConditionCharacter { public AbilityConditionLowestLevel(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { } - protected override bool MatchesConditionSpecific() + protected override bool MatchesCharacter(Character character) { int ownLevel = character.Info.GetCurrentLevel(); - - foreach (Character crew in GameSession.GetSessionCrewCharacters(CharacterType.Both)) + foreach (Character otherCharacter in GameSession.GetSessionCrewCharacters(CharacterType.Both)) { - if (crew == character) { continue; } - - if (crew.Info.GetCurrentLevel() < ownLevel) { return false; } + if (otherCharacter == character) { continue; } + if (otherCharacter.Info.GetCurrentLevel() < ownLevel) { return false; } } - return true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs index 5a7d22598..7ccc4e036 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs @@ -26,7 +26,7 @@ namespace Barotrauma.Abilities public CharacterAbility(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) { - CharacterAbilityGroup = characterAbilityGroup; + CharacterAbilityGroup = characterAbilityGroup ?? throw new ArgumentNullException(nameof(characterAbilityGroup)); CharacterTalent = characterAbilityGroup.CharacterTalent; Character = CharacterTalent.Character; RequiresAlive = abilityElement.GetAttributeBool("requiresalive", true); @@ -59,7 +59,8 @@ namespace Barotrauma.Abilities protected virtual void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) { - DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}: Ability {this} does not have an implementation for VerifyState! This ability does not work in interval ability groups."); + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}: Ability {this} does not have an implementation for VerifyState! This ability does not work in interval ability groups.", + contentPackage: CharacterTalent.Prefab.ContentPackage); } public void ApplyAbilityEffect(AbilityObject abilityObject) @@ -76,17 +77,20 @@ namespace Barotrauma.Abilities protected virtual void ApplyEffect() { - DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not have a definition for ApplyEffect in talent {CharacterTalent.DebugIdentifier}"); + DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not have a definition for ApplyEffect in talent {CharacterTalent.DebugIdentifier}", + CharacterTalent.Prefab.ContentPackage); } protected virtual void ApplyEffect(AbilityObject abilityObject) { - DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not take a parameter for ApplyEffect in talent {CharacterTalent.DebugIdentifier}"); + DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not take a parameter for ApplyEffect in talent {CharacterTalent.DebugIdentifier}", + CharacterTalent.Prefab.ContentPackage); } protected void LogAbilityObjectMismatch() { - DebugConsole.ThrowError($"Incompatible ability! Ability {this} is incompatitible with this type of ability effect type in talent {CharacterTalent.DebugIdentifier}"); + DebugConsole.ThrowError($"Incompatible ability! Ability {this} is incompatitible with this type of ability effect type in talent {CharacterTalent.DebugIdentifier}", + contentPackage: CharacterTalent.Prefab.ContentPackage); } // XML @@ -99,13 +103,18 @@ namespace Barotrauma.Abilities abilityType = Type.GetType("Barotrauma.Abilities." + type + "", false, true); if (abilityType == null) { - if (errorMessages) DebugConsole.ThrowError("Could not find the CharacterAbility \"" + type + "\" (" + characterAbilityGroup.CharacterTalent.DebugIdentifier + ")"); + if (errorMessages) DebugConsole.ThrowError("Could not find the CharacterAbility \"" + type + "\" (" + characterAbilityGroup.CharacterTalent.DebugIdentifier + ")", + contentPackage: abilityElement.ContentPackage); return null; } } catch (Exception e) { - if (errorMessages) DebugConsole.ThrowError("Could not find the CharacterAbility \"" + type + "\" (" + characterAbilityGroup.CharacterTalent.DebugIdentifier + ")", e); + if (errorMessages) + { + DebugConsole.ThrowError("Could not find the CharacterAbility \"" + type + "\" (" + characterAbilityGroup.CharacterTalent.DebugIdentifier + ")", e, + contentPackage: abilityElement.ContentPackage); + } return null; } @@ -118,7 +127,8 @@ namespace Barotrauma.Abilities } catch (TargetInvocationException e) { - DebugConsole.ThrowError("Error while creating an instance of a CharacterAbility of the type " + abilityType + ".", e.InnerException); + DebugConsole.ThrowError("Error while creating an instance of a CharacterAbility of the type " + abilityType + ".", e.InnerException, + contentPackage: abilityElement.ContentPackage); return null; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyForce.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyForce.cs index 0b350c514..0ca9c74d0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyForce.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyForce.cs @@ -30,7 +30,8 @@ namespace Barotrauma.Abilities } else { - DebugConsole.ThrowError($"Error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - \"{limbTypeStr}\" is not a valid limb type."); + DebugConsole.ThrowError($"Error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - \"{limbTypeStr}\" is not a valid limb type.", + contentPackage: abilityElement.ContentPackage); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToApprenticeship.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToApprenticeship.cs index 27d4afe94..453ad0d09 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToApprenticeship.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToApprenticeship.cs @@ -21,7 +21,8 @@ namespace Barotrauma.Abilities JobPrefab? apprenticeJob = GetApprenticeJob(Character, jobPrefabList); if (apprenticeJob is null) { - DebugConsole.ThrowError($"{nameof(CharacterAbilityUnlockApprenticeshipTalentTree)}: Could not find apprentice job for character {Character.Name}"); + DebugConsole.ThrowError($"{nameof(CharacterAbilityUnlockApprenticeshipTalentTree)}: Could not find apprentice job for character {Character.Name}", + contentPackage: CharacterTalent.Prefab.ContentPackage); return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs index 0cd2b4857..2b98992fa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs @@ -14,7 +14,8 @@ targetAllies = abilityElement.GetAttributeBool("targetallies", false); if (skillIdentifier.IsEmpty) { - DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}: skill identifier not defined."); + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}: skill identifier not defined.", + contentPackage: abilityElement.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs index 13bae5e96..3b9653393 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs @@ -16,7 +16,8 @@ if (afflictionId.IsEmpty) { - DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, CharacterAbilityGiveAffliction - affliction identifier not set."); + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, CharacterAbilityGiveAffliction - affliction identifier not set.", + contentPackage: abilityElement.ContentPackage); } } @@ -27,7 +28,8 @@ var afflictionPrefab = AfflictionPrefab.Prefabs.Find(a => a.Identifier == afflictionId); if (afflictionPrefab == null) { - DebugConsole.ThrowError($"Error in CharacterAbilityGiveAffliction - could not find an affliction with the identifier \"{afflictionId}\"."); + DebugConsole.ThrowError($"Error in CharacterAbilityGiveAffliction - could not find an affliction with the identifier \"{afflictionId}\".", + contentPackage: CharacterTalent.Prefab.ContentPackage); return; } float strength = this.strength; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveExperience.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveExperience.cs index 5686f777a..3aa2719ea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveExperience.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveExperience.cs @@ -14,29 +14,35 @@ internal sealed class CharacterAbilityGiveExperience : CharacterAbility if (amount == 0 && level == 0) { - DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - no exp amount or level defined in {nameof(CharacterAbilityGiveExperience)}."); + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - no exp amount or level defined in {nameof(CharacterAbilityGiveExperience)}.", + contentPackage: abilityElement.ContentPackage); } if (amount > 0 && level > 0) { - DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - {nameof(CharacterAbilityGiveExperience)} defines both an exp amount and a level."); + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - {nameof(CharacterAbilityGiveExperience)} defines both an exp amount and a level.", + contentPackage: abilityElement.ContentPackage); } } private void ApplyEffectSpecific(Character targetCharacter) { - if (amount != 0) - { - targetCharacter.Info?.GiveExperience(amount); - } if (level > 0) { - targetCharacter.Info?.GiveExperience(targetCharacter.Info.GetExperienceRequiredForLevel(level)); + targetCharacter.Info?.GiveExperience(targetCharacter.Info.GetExperienceRequiredForLevel(level) + amount); + } + else if (amount != 0) + { + targetCharacter.Info?.GiveExperience(amount); } } protected override void ApplyEffect(AbilityObject abilityObject) { - if ((abilityObject as IAbilityCharacter)?.Character is { } targetCharacter) + if (abilityObject is AbilityCharacterKill { Killer: { } killer }) + { + ApplyEffectSpecific(killer); + } + else if ((abilityObject as IAbilityCharacter)?.Character is { } targetCharacter) { ApplyEffectSpecific(targetCharacter); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStat.cs index c78b2c66b..7e3d5ef4c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStat.cs @@ -7,12 +7,14 @@ namespace Barotrauma.Abilities private readonly ItemTalentStats stat; private readonly float value; private readonly bool stackable; + private readonly bool save; public CharacterAbilityGiveItemStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { stat = abilityElement.GetAttributeEnum("stattype", ItemTalentStats.None); value = abilityElement.GetAttributeFloat("value", 0f); stackable = abilityElement.GetAttributeBool("stackable", true); + save = abilityElement.GetAttributeBool("save", false); } protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) @@ -27,7 +29,7 @@ namespace Barotrauma.Abilities { if (abilityObject is not IAbilityItem ability) { return; } - ability.Item.StatManager.ApplyStat(stat, stackable, value, CharacterTalent); + ability.Item.StatManager.ApplyStat(stat, stackable, save, value, CharacterTalent); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs index 147725f90..ac3d4f94c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs @@ -10,6 +10,7 @@ namespace Barotrauma.Abilities private readonly float value; private readonly ImmutableHashSet tags; private readonly bool stackable; + private readonly bool save; public CharacterAbilityGiveItemStatToTags(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { @@ -17,6 +18,7 @@ namespace Barotrauma.Abilities value = abilityElement.GetAttributeFloat("value", 0f); tags = abilityElement.GetAttributeIdentifierImmutableHashSet("tags", ImmutableHashSet.Empty); stackable = abilityElement.GetAttributeBool("stackable", true); + save = abilityElement.GetAttributeBool("save", false); } public override void InitializeAbility(bool addingFirstTime) @@ -44,7 +46,7 @@ namespace Barotrauma.Abilities if (item.Submarine?.TeamID != Character.TeamID) { continue; } if (item.HasTag(tags) || tags.Contains(item.Prefab.Identifier)) { - item.StatManager.ApplyStat(stat, stackable, value, CharacterTalent); + item.StatManager.ApplyStat(stat, stackable, save, value, CharacterTalent); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs index a45e92b1d..2b5f00421 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs @@ -14,7 +14,8 @@ if (amount == 0) { - DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, CharacterAbilityGiveMoney - amount of money set to 0."); + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, CharacterAbilityGiveMoney - amount of money set to 0.", + contentPackage: abilityElement.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs index a2a94cf37..31c58117b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs @@ -1,4 +1,6 @@ -namespace Barotrauma.Abilities +using System; + +namespace Barotrauma.Abilities { public enum PermanentStatPlaceholder { @@ -19,7 +21,12 @@ private readonly bool setValue; private readonly PermanentStatPlaceholder placeholder; - //private readonly float maximumValue; + /// + /// If enabled, the effect is applied on the target of the ability (e.g. the character that was killed if the effect type is OnKillCharacter). + /// Defaults to false (= targets the character who has the talent). + /// + private readonly bool targetAbilityTarget = false; + public override bool AllowClientSimulation => true; public override bool AppliesEffectOnIntervalUpdate => true; @@ -28,7 +35,8 @@ statIdentifier = abilityElement.GetAttributeIdentifier("statidentifier", Identifier.Empty); if (statIdentifier.IsEmpty) { - DebugConsole.ThrowError($"Error in talent \"{CharacterTalent.DebugIdentifier}\" - stat identifier not defined."); + DebugConsole.ThrowError($"Error in talent \"{CharacterTalent.DebugIdentifier}\" - stat identifier not defined.", + contentPackage: abilityElement.ContentPackage); } string statTypeName = abilityElement.GetAttributeString("stattype", string.Empty); statType = string.IsNullOrEmpty(statTypeName) ? StatTypes.None : CharacterAbilityGroup.ParseStatType(statTypeName, CharacterTalent.DebugIdentifier); @@ -39,27 +47,28 @@ giveOnAddingFirstTime = abilityElement.GetAttributeBool("giveonaddingfirsttime", characterAbilityGroup.AbilityEffectType == AbilityEffectType.None); setValue = abilityElement.GetAttributeBool("setvalue", false); placeholder = abilityElement.GetAttributeEnum("placeholder", PermanentStatPlaceholder.None); + targetAbilityTarget = abilityElement.GetAttributeBool(nameof(targetAbilityTarget), false); } public override void InitializeAbility(bool addingFirstTime) { if (giveOnAddingFirstTime && addingFirstTime) { - ApplyEffectSpecific(); + ApplyEffectSpecific(abilityObject: null); } } protected override void ApplyEffect(AbilityObject abilityObject) { - ApplyEffectSpecific(); + ApplyEffectSpecific(abilityObject); } protected override void ApplyEffect() { - ApplyEffectSpecific(); + ApplyEffectSpecific(abilityObject: null); } - private void ApplyEffectSpecific() + private void ApplyEffectSpecific(AbilityObject abilityObject) { Identifier identifier = HandlePlaceholders(placeholder, statIdentifier); if (targetAllies) @@ -71,7 +80,21 @@ } else { - Character?.Info?.ChangeSavedStatValue(statType, value, identifier, removeOnDeath, maxValue: maxValue, setValue: setValue); + Character targetCharacter = + targetAbilityTarget ? + (abilityObject as IAbilityCharacter)?.Character ?? Character : + Character; + if (targetCharacter == null) + { + DebugConsole.ThrowError($"Error in {nameof(CharacterAbilityGivePermanentStat.ApplyEffectSpecific)}: character was null.\n{Environment.StackTrace.CleanupStackTrace()}"); + return; + } + if (targetCharacter?.Info == null) + { + DebugConsole.AddWarning($"Error in {nameof(CharacterAbilityGivePermanentStat.ApplyEffectSpecific)}: character {targetCharacter} has no CharacterInfo. Are you trying to use the condition on a non-player character?\n{Environment.StackTrace.CleanupStackTrace()}"); + return; + } + targetCharacter.Info.ChangeSavedStatValue(statType, value, identifier, removeOnDeath, maxValue: maxValue, setValue: setValue); } } @@ -82,7 +105,7 @@ switch (placeholder) { case PermanentStatPlaceholder.LocationName when map.CurrentLocation is { } location: - return original.Replace("[placeholder]", location.Name); + return original.Replace("[placeholder]", location.NameIdentifier.Value); case PermanentStatPlaceholder.LocationIndex: return original.Replace("[placeholder]", map.CurrentLocationIndex.ToString()); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveReputation.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveReputation.cs index 6d3777d53..a0bfb0f34 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveReputation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveReputation.cs @@ -13,11 +13,13 @@ namespace Barotrauma.Abilities amount = abilityElement.GetAttributeFloat("amount", 0f); if (factionIdentifier.IsEmpty) { - DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, faction identifier not defined."); + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, faction identifier not defined.", + contentPackage: abilityElement.ContentPackage); } if (amount == 0) { - DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, amount of reputation to give is 0."); + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, amount of reputation to give is 0.", + contentPackage: abilityElement.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs index 9df7fc87b..062032ca7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs @@ -12,11 +12,13 @@ if (resistanceId.IsEmpty) { - DebugConsole.ThrowError("Error in CharacterAbilityGiveResistance - resistance identifier not set."); + DebugConsole.ThrowError("Error in CharacterAbilityGiveResistance - resistance identifier not set.", + contentPackage: abilityElement.ContentPackage); } if (MathUtils.NearlyEqual(multiplier, 1)) { - DebugConsole.AddWarning($"Possible error in talent {CharacterTalent.DebugIdentifier} - multiplier set to 1, which will do nothing."); + DebugConsole.AddWarning($"Possible error in talent {CharacterTalent.DebugIdentifier} - multiplier set to 1, which will do nothing.", + contentPackage: abilityElement.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPoints.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPoints.cs index e61e3981b..ea40eb130 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPoints.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPoints.cs @@ -9,7 +9,8 @@ amount = abilityElement.GetAttributeInt("amount", 0); if (amount == 0) { - DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, amount of talent points to give is 0."); + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, amount of talent points to give is 0.", + contentPackage: abilityElement.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPointsToAllies.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPointsToAllies.cs index 2b4dd4cac..f3f2d91cc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPointsToAllies.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPointsToAllies.cs @@ -11,7 +11,8 @@ namespace Barotrauma.Abilities amount = abilityElement.GetAttributeInt("amount", 0); if (amount == 0) { - DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, amount of talent points to give is 0."); + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, amount of talent points to give is 0.", + contentPackage: abilityElement.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityIncreaseSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityIncreaseSkill.cs index 2e1816bd4..4618bcbeb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityIncreaseSkill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityIncreaseSkill.cs @@ -16,11 +16,13 @@ namespace Barotrauma.Abilities if (skillIdentifier.IsEmpty) { - DebugConsole.ThrowError($"Error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - skill identifier not defined in CharacterAbilityIncreaseSkill."); + DebugConsole.ThrowError($"Error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - skill identifier not defined in CharacterAbilityIncreaseSkill.", + contentPackage: abilityElement.ContentPackage); } if (MathUtils.NearlyEqual(skillIncrease, 0)) { - DebugConsole.AddWarning($"Possible error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - skill increase set to 0."); + DebugConsole.AddWarning($"Possible error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - skill increase set to 0.", + contentPackage: abilityElement.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityMarkAsLooted.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityMarkAsLooted.cs index 45ddb19fb..703b07c48 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityMarkAsLooted.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityMarkAsLooted.cs @@ -8,7 +8,8 @@ namespace Barotrauma.Abilities identifier = abilityElement.GetAttributeIdentifier("identifier", Identifier.Empty); if (identifier.IsEmpty) { - DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, identifier is empty in {nameof(CharacterAbilityMarkAsLooted)}."); + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, identifier is empty in {nameof(CharacterAbilityMarkAsLooted)}.", + contentPackage: abilityElement.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyResistance.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyResistance.cs index 3c1ec2272..4cee5bb66 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyResistance.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyResistance.cs @@ -15,11 +15,13 @@ if (resistanceId.IsEmpty) { - DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - resistance identifier not set in {nameof(CharacterAbilityModifyResistance)}."); + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - resistance identifier not set in {nameof(CharacterAbilityModifyResistance)}.", + contentPackage: abilityElement.ContentPackage); } if (MathUtils.NearlyEqual(multiplier, 1.0f)) { - DebugConsole.AddWarning($"Possible error in talent {CharacterTalent.DebugIdentifier} - resistance set to 1, which will do nothing."); + DebugConsole.AddWarning($"Possible error in talent {CharacterTalent.DebugIdentifier} - resistance set to 1, which will do nothing.", + contentPackage: abilityElement.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyValue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyValue.cs index 57ed31b3b..4168ec6a1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyValue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyValue.cs @@ -11,7 +11,8 @@ multiplyValue = abilityElement.GetAttributeFloat("multiplyvalue", 1f); if (MathUtils.NearlyEqual(addedValue, 0.0f) && MathUtils.NearlyEqual(multiplyValue, 1.0f)) { - DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilityModifyValue)} - added value is 0 and multiplier is 1, the ability will do nothing."); + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilityModifyValue)} - added value is 0 and multiplier is 1, the ability will do nothing.", + contentPackage: abilityElement.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityPutItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityPutItem.cs index ba7ef06eb..346c075b7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityPutItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityPutItem.cs @@ -11,7 +11,8 @@ amount = abilityElement.GetAttributeInt("amount", 1); if (itemIdentifier.IsEmpty) { - DebugConsole.ThrowError($"Error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - itemIdentifier not defined."); + DebugConsole.ThrowError($"Error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - itemIdentifier not defined.", + contentPackage: abilityElement.ContentPackage); } } @@ -19,14 +20,16 @@ { if (itemIdentifier.IsEmpty) { - DebugConsole.ThrowError("Cannot put item in inventory - itemIdentifier not defined."); + DebugConsole.ThrowError("Cannot put item in inventory - itemIdentifier not defined.", + contentPackage: CharacterTalent.Prefab.ContentPackage); return; } ItemPrefab itemPrefab = ItemPrefab.Find(null, itemIdentifier); if (itemPrefab == null) { - DebugConsole.ThrowError("Cannot put item in inventory - item prefab " + itemIdentifier + " not found."); + DebugConsole.ThrowError("Cannot put item in inventory - item prefab " + itemIdentifier + " not found.", + contentPackage: CharacterTalent.Prefab.ContentPackage); return; } for (int i = 0; i < amount; i++) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityReduceAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityReduceAffliction.cs index 14a84d337..4f64d8bfd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityReduceAffliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityReduceAffliction.cs @@ -14,7 +14,8 @@ namespace Barotrauma.Abilities if (afflictionId.IsEmpty) { - DebugConsole.ThrowError($"Error in {nameof(CharacterAbilityReduceAffliction)} - affliction identifier not set."); + DebugConsole.ThrowError($"Error in {nameof(CharacterAbilityReduceAffliction)} - affliction identifier not set.", + contentPackage: abilityElement.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs index 3e85a16dd..e1dac6415 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs @@ -12,7 +12,8 @@ namespace Barotrauma.Abilities statIdentifier = abilityElement.GetAttributeIdentifier("statidentifier", Identifier.Empty); if (statIdentifier.IsEmpty) { - DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilityResetPermanentStat)} - statIdentifier is empty."); + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilityResetPermanentStat)} - statIdentifier is empty.", + contentPackage: abilityElement.ContentPackage); } } protected override void ApplyEffect(AbilityObject abilityObject) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySetMetadataInt.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySetMetadataInt.cs index 3953cce9f..daa34f959 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySetMetadataInt.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySetMetadataInt.cs @@ -13,7 +13,8 @@ namespace Barotrauma.Abilities value = abilityElement.GetAttributeInt("value", 0); if (identifier.IsEmpty) { - DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilitySetMetadataInt)} - identifier is empty."); + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilitySetMetadataInt)} - identifier is empty.", + contentPackage: abilityElement.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs index 7a4ceeb07..7ce696992 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs @@ -24,7 +24,8 @@ namespace Barotrauma.Abilities JobPrefab? apprentice = CharacterAbilityApplyStatusEffectsToApprenticeship.GetApprenticeJob(Character, JobPrefab.Prefabs.ToImmutableHashSet()); if (apprentice is null) { - DebugConsole.ThrowError($"{nameof(CharacterAbilityUnlockApprenticeshipTalentTree)}: Could not find apprentice job for character {Character.Name}"); + DebugConsole.ThrowError($"{nameof(CharacterAbilityUnlockApprenticeshipTalentTree)}: Could not find apprentice job for character {Character.Name}", + contentPackage: CharacterTalent.Prefab.ContentPackage); return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs index 6b59a5825..4d177dd69 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs @@ -31,7 +31,7 @@ namespace Barotrauma.Abilities public CharacterAbilityGroup(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, ContentXElement abilityElementGroup) { AbilityEffectType = abilityEffectType; - CharacterTalent = characterTalent; + CharacterTalent = characterTalent ?? throw new ArgumentNullException(nameof(characterTalent)); Character = CharacterTalent.Character; maxTriggerCount = abilityElementGroup.GetAttributeInt("maxtriggercount", int.MaxValue); foreach (var subElement in abilityElementGroup.Elements()) @@ -44,9 +44,13 @@ namespace Barotrauma.Abilities case "fallbackabilities": LoadFallbackAbilities(subElement); break; + case "condition": case "conditions": LoadConditions(subElement); break; + default: + DebugConsole.ThrowError($"Error in talent {characterTalent.Prefab.Identifier}: unrecognized xml element \"{subElement.Name}\"."); + break; } } @@ -55,7 +59,8 @@ namespace Barotrauma.Abilities case AbilityEffectType.OnDieToCharacter: if (characterAbilities.Any(a => a.RequiresAlive)) { - DebugConsole.AddWarning($"Potential error in talent {characterTalent}: an ability group has the type {AbilityEffectType.OnDieToCharacter}, but includes abilities that require the character to be alive, meaning they will never execute."); + DebugConsole.AddWarning($"Potential error in talent {characterTalent}: an ability group has the type {AbilityEffectType.OnDieToCharacter}, but includes abilities that require the character to be alive, meaning they will never execute.", + characterTalent.Prefab.ContentPackage); } break; } @@ -90,7 +95,8 @@ namespace Barotrauma.Abilities if (newCondition == null) { - DebugConsole.ThrowError($"AbilityCondition was not found in talent {CharacterTalent.DebugIdentifier}!"); + DebugConsole.ThrowError($"AbilityCondition was not found in talent {CharacterTalent.DebugIdentifier}!", + contentPackage: conditionElement.ContentPackage); return; } @@ -107,7 +113,8 @@ namespace Barotrauma.Abilities { if (characterAbility == null) { - DebugConsole.ThrowError($"Trying to add null ability for talent {CharacterTalent.DebugIdentifier}!"); + DebugConsole.ThrowError($"Trying to add null ability for talent {CharacterTalent.DebugIdentifier}!", + contentPackage: CharacterTalent.Prefab.ContentPackage); return; } @@ -118,7 +125,8 @@ namespace Barotrauma.Abilities { if (characterAbility == null) { - DebugConsole.ThrowError($"Trying to add null ability for talent {CharacterTalent.DebugIdentifier}!"); + DebugConsole.ThrowError($"Trying to add null ability for talent {CharacterTalent.DebugIdentifier}!", + contentPackage: CharacterTalent.Prefab.ContentPackage); return; } @@ -135,13 +143,21 @@ namespace Barotrauma.Abilities conditionType = Type.GetType("Barotrauma.Abilities." + type + "", false, true); if (conditionType == null) { - if (errorMessages) DebugConsole.ThrowError("Could not find the component \"" + type + "\" (" + characterTalent.DebugIdentifier + ")"); + if (errorMessages) + { + DebugConsole.ThrowError("Could not find the component \"" + type + "\" (" + characterTalent.DebugIdentifier + ")", + contentPackage: characterTalent.Prefab.ContentPackage); + } return null; } } catch (Exception e) { - if (errorMessages) DebugConsole.ThrowError("Could not find the component \"" + type + "\" (" + characterTalent.DebugIdentifier + ")", e); + if (errorMessages) + { + DebugConsole.ThrowError("Could not find the component \"" + type + "\" (" + characterTalent.DebugIdentifier + ")", e, + contentPackage: characterTalent.Prefab.ContentPackage); + } return null; } @@ -154,13 +170,15 @@ namespace Barotrauma.Abilities } catch (TargetInvocationException e) { - DebugConsole.ThrowError("Error while creating an instance of an ability condition of the type " + conditionType + ".", e.InnerException); + DebugConsole.ThrowError("Error while creating an instance of an ability condition of the type " + conditionType + ".", e.InnerException, + contentPackage: characterTalent.Prefab.ContentPackage); return null; } if (newCondition == null) { - DebugConsole.ThrowError("Error while creating an instance of an ability condition of the type " + conditionType + ", instance was null"); + DebugConsole.ThrowError("Error while creating an instance of an ability condition of the type " + conditionType + ", instance was null", + contentPackage: characterTalent.Prefab.ContentPackage); return null; } @@ -189,7 +207,8 @@ namespace Barotrauma.Abilities if (newAbility == null) { - DebugConsole.ThrowError($"Unable to create an ability for {characterTalent.DebugIdentifier}!"); + DebugConsole.ThrowError($"Unable to create an ability for {characterTalent.DebugIdentifier}!", + contentPackage: characterTalent.Prefab.ContentPackage); return null; } @@ -200,7 +219,8 @@ namespace Barotrauma.Abilities { if (statusEffectElements == null) { - DebugConsole.ThrowError("StatusEffect list was not found in talent " + characterTalent.DebugIdentifier); + DebugConsole.ThrowError("StatusEffect list was not found in talent " + characterTalent.DebugIdentifier, + contentPackage: characterTalent.Prefab.ContentPackage); return null; } @@ -233,7 +253,8 @@ namespace Barotrauma.Abilities { if (afflictionElements == null) { - DebugConsole.ThrowError("Affliction list was not found in talent " + characterTalent.DebugIdentifier); + DebugConsole.ThrowError("Affliction list was not found in talent " + characterTalent.DebugIdentifier, + contentPackage: characterTalent.Prefab.ContentPackage); return null; } @@ -248,7 +269,8 @@ namespace Barotrauma.Abilities AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier == afflictionIdentifier); if (afflictionPrefab == null) { - DebugConsole.ThrowError("Error in CharacterTalent (" + characterTalent.DebugIdentifier + ") - Affliction prefab with the identifier \"" + afflictionIdentifier + "\" not found."); + DebugConsole.ThrowError("Error in CharacterTalent (" + characterTalent.DebugIdentifier + ") - Affliction prefab with the identifier \"" + afflictionIdentifier + "\" not found.", + contentPackage: characterTalent.Prefab.ContentPackage); continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs index 0cb53857e..ae7f18849 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs @@ -23,9 +23,8 @@ namespace Barotrauma public CharacterTalent(TalentPrefab talentPrefab, Character character) { - Character = character; - - Prefab = talentPrefab; + Character = character ?? throw new ArgumentNullException(nameof(character)); + Prefab = talentPrefab ?? throw new ArgumentNullException(nameof(talentPrefab)); var element = talentPrefab.ConfigElement; DebugIdentifier = talentPrefab.OriginalName; @@ -46,7 +45,8 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"No recipe identifier defined for talent {DebugIdentifier}"); + DebugConsole.ThrowError($"No recipe identifier defined for talent {DebugIdentifier}", + contentPackage: element.ContentPackage); } break; case "addedstoreitem": @@ -56,7 +56,8 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"No store item identifier defined for talent {DebugIdentifier}"); + DebugConsole.ThrowError($"No store item identifier defined for talent {DebugIdentifier}", + contentPackage: element.ContentPackage); } break; } @@ -146,11 +147,13 @@ namespace Barotrauma { if (!Enum.TryParse(abilityEffectTypeString, true, out AbilityEffectType abilityEffectType)) { - DebugConsole.ThrowError("Invalid ability effect type \"" + abilityEffectTypeString + "\" in CharacterTalent (" + characterTalent.DebugIdentifier + ")"); + DebugConsole.ThrowError("Invalid ability effect type \"" + abilityEffectTypeString + "\" in CharacterTalent (" + characterTalent.DebugIdentifier + ")", + contentPackage: characterTalent?.Prefab?.ContentPackage); } if (abilityEffectType == AbilityEffectType.Undefined) { - DebugConsole.ThrowError("Ability effect type not defined in CharacterTalent (" + characterTalent.DebugIdentifier + ")"); + DebugConsole.ThrowError("Ability effect type not defined in CharacterTalent (" + characterTalent.DebugIdentifier + ")", + contentPackage: characterTalent?.Prefab?.ContentPackage); } return abilityEffectType; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs index bd105e729..f640dee46 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs @@ -84,7 +84,8 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError($"Error while loading talent migration for talent \"{Identifier}\".", e); + DebugConsole.ThrowError($"Error while loading talent migration for talent \"{Identifier}\".", e, + element?.ContentPackage); } } break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs index 2c1867589..04ac83453 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs @@ -37,7 +37,8 @@ namespace Barotrauma if (Identifier.IsEmpty) { - DebugConsole.ThrowError($"No job defined for talent tree in \"{file.Path}\"!"); + DebugConsole.ThrowError($"No job defined for talent tree in \"{file.Path}\"!", + contentPackage: element.ContentPackage); return; } @@ -304,7 +305,8 @@ namespace Barotrauma if (RequiredTalents > MaxChosenTalents) { - DebugConsole.ThrowError($"Error in talent tree {debugIdentifier} - MaxChosenTalents is larger than RequiredTalents."); + DebugConsole.ThrowError($"Error in talent tree {debugIdentifier} - MaxChosenTalents is larger than RequiredTalents.", + contentPackage: talentOptionsElement.ContentPackage); } HashSet identifiers = new HashSet(); @@ -333,11 +335,13 @@ namespace Barotrauma if (RequiredTalents > talentIdentifiers.Count) { - DebugConsole.ThrowError($"Error in talent tree {debugIdentifier} - completing a stage of the tree requires more talents than there are in the stage."); + DebugConsole.ThrowError($"Error in talent tree {debugIdentifier} - completing a stage of the tree requires more talents than there are in the stage.", + contentPackage: talentOptionsElement.ContentPackage); } if (MaxChosenTalents > talentIdentifiers.Count) { - DebugConsole.ThrowError($"Error in talent tree {debugIdentifier} - maximum number of talents to choose is larger than the number of talents."); + DebugConsole.ThrowError($"Error in talent tree {debugIdentifier} - maximum number of talents to choose is larger than the number of talents.", + contentPackage: talentOptionsElement.ContentPackage); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/ItemSlotIndexPair.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/ItemSlotIndexPair.cs index c2ccdb8bb..e9137e756 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/ItemSlotIndexPair.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/ItemSlotIndexPair.cs @@ -39,6 +39,23 @@ namespace Barotrauma } public Item? FindItemInContainer(ItemContainer? container) - => container?.Inventory.GetItemsAt(Slot).ElementAt(StackIndex); + { + var items = container?.Inventory.GetItemsAt(Slot); + if (items != null && StackIndex >= 0 && StackIndex < items.Count()) + { + return items.ElementAt(StackIndex); + } + else + { + string errorMsg = + $"Circuit box error: failed to find an item in the container {container?.Item.Name ?? "null"}."; + DebugConsole.ThrowError( + errorMsg + + $" Items: {items?.Count().ToString() ?? "null"}, " + + $" Slot: {Slot}, StackIndex: {StackIndex}"); + GameAnalyticsManager.AddErrorEventOnce("ItemSlotIndexPair.FindItemInContainer", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + return null; + } + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/AfflictionsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/AfflictionsFile.cs index e3cfc518c..2069fe68f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/AfflictionsFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/AfflictionsFile.cs @@ -50,7 +50,8 @@ namespace Barotrauma if (identifier.IsEmpty) { DebugConsole.ThrowError( - $"No identifier defined for the affliction '{elementName}' in file '{Path}'"); + $"No identifier defined for the affliction '{elementName}' in file '{Path}'", + contentPackage: element?.ContentPackage); return; } @@ -60,12 +61,13 @@ namespace Barotrauma { DebugConsole.NewMessage( $"Overriding an affliction or a buff with the identifier '{identifier}' using the file '{Path}'", - Color.Yellow); + Color.MediumPurple); } else { DebugConsole.ThrowError( - $"Duplicate affliction: '{identifier}' defined in {elementName} of '{Path}'"); + $"Duplicate affliction: '{identifier}' defined in {elementName} of '{Path}'", + contentPackage: element?.ContentPackage); return; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs index e3412c1be..d8269dbd0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs @@ -17,12 +17,12 @@ namespace Barotrauma XDocument doc = XMLExtensions.TryLoadXml(Path); if (doc == null) { - DebugConsole.ThrowError($"Loading character file failed: {Path}"); + DebugConsole.ThrowError($"Loading character file failed: {Path}", contentPackage: ContentPackage); return; } if (CharacterPrefab.Prefabs.AllPrefabs.Any(kvp => kvp.Value.Any(cf => cf?.ContentFile == this))) { - DebugConsole.ThrowError($"Duplicate path: {Path}"); + DebugConsole.ThrowError($"Duplicate path: {Path}", contentPackage: ContentPackage); return; } var mainElement = doc.Root.FromPackage(ContentPackage); @@ -69,7 +69,8 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError($"Failed to preload a ragdoll file for the character \"{characterPrefab.Name}\"", e); + DebugConsole.ThrowError($"Failed to preload a ragdoll file for the character \"{characterPrefab.Name}\"", e, + contentPackage: characterPrefab.ContentPackage); return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs index f2470ed6a..b7d896fe6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs @@ -76,7 +76,8 @@ namespace Barotrauma { DebugConsole.AddWarning( $"The content type \"TraitorMission\" in content package \"{package.Name}\" is no longer supported." + - $" Traitor missions should be implemented using the scripted event system and the content type TraitorEvents."); + $" Traitor missions should be implemented using the scripted event system and the content type TraitorEvents.", + package); } return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs index b57133514..ef041dbef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs @@ -55,7 +55,7 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"GenericPrefabFile: Invalid {GetType().Name} element: {parentElement.Name} in {Path}"); + DebugConsole.ThrowError($"GenericPrefabFile: Invalid {GetType().Name} element: {parentElement.Name} in {Path}", contentPackage: ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/NPCConversationsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/NPCConversationsFile.cs index f26a17cd7..95ee2d5c5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/NPCConversationsFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/NPCConversationsFile.cs @@ -2,6 +2,7 @@ using System.Xml.Linq; namespace Barotrauma { + [NotSyncedInMultiplayer] sealed class NPCConversationsFile : ContentFile { public NPCConversationsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OrdersFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OrdersFile.cs index 57273ced6..cb7ca8d87 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OrdersFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OrdersFile.cs @@ -42,7 +42,8 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"OrdersFile: Invalid {GetType().Name} element: {parentElement.Name} in {Path}"); + DebugConsole.ThrowError($"OrdersFile: Invalid {GetType().Name} element: {parentElement.Name} in {Path}", + contentPackage: parentElement?.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RandomEventsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RandomEventsFile.cs index 4c25ad989..51518e428 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RandomEventsFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RandomEventsFile.cs @@ -65,7 +65,8 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"RandomEventsFile: Invalid {GetType().Name} element: {parentElement.Name} in {Path}"); + DebugConsole.ThrowError($"RandomEventsFile: Invalid {GetType().Name} element: {parentElement.Name} in {Path}", + contentPackage: parentElement.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TextFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TextFile.cs index 9f21480f9..ff4493c9c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TextFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TextFile.cs @@ -4,6 +4,7 @@ using System.Xml.Linq; namespace Barotrauma { + [NotSyncedInMultiplayer] public sealed class TextFile : ContentFile { public TextFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } @@ -18,13 +19,12 @@ namespace Barotrauma LanguageIdentifier language = languageName.ToLanguageIdentifier(); if (!TextManager.TextPacks.ContainsKey(language)) { - TextManager.TextPacks.TryAdd(language, ImmutableHashSet.Empty); + TextManager.TextPacks.TryAdd(language, ImmutableList.Empty); } - var newPack = new TextPack(this, mainElement, language); - var newHashSet = TextManager.TextPacks[language].Add(newPack); + var newList = TextManager.TextPacks[language].Add(newPack); TextManager.TextPacks.TryRemove(language, out _); - TextManager.TextPacks.TryAdd(language, newHashSet); + TextManager.TextPacks.TryAdd(language, newList); TextManager.IncrementLanguageVersion(); } @@ -32,16 +32,27 @@ namespace Barotrauma { foreach (var kvp in TextManager.TextPacks.ToArray()) { - var newHashSet = kvp.Value.Where(p => p.ContentFile != this).ToImmutableHashSet(); + var newList = kvp.Value.Where(p => p.ContentFile != this).ToImmutableList(); TextManager.TextPacks.TryRemove(kvp.Key, out _); - if (newHashSet.Count != 0) { TextManager.TextPacks.TryAdd(kvp.Key, newHashSet); } + if (newList.Count != 0) { TextManager.TextPacks.TryAdd(kvp.Key, newList); } } TextManager.IncrementLanguageVersion(); + if (!TextManager.TextPacks.ContainsKey(GameSettings.CurrentConfig.Language)) + { + DebugConsole.AddWarning($"The language {GameSettings.CurrentConfig.Language} is no longer available. Switching to {TextManager.DefaultLanguage}..."); + var config = GameSettings.CurrentConfig; + config.Language = TextManager.DefaultLanguage; + GameSettings.SetCurrentConfig(config); + } } public override void Sort() { - //Overrides for text packs don't exist! Should we change this? + foreach (var language in TextManager.TextPacks.Keys.ToList()) + { + TextManager.TextPacks[language] = + TextManager.TextPacks[language].Sort((t1, t2) => (t1.ContentFile.ContentPackage?.Index ?? int.MaxValue) - (t2.ContentFile.ContentPackage?.Index ?? int.MaxValue)); + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs index 3d78d1da8..c932b4470 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs @@ -283,7 +283,7 @@ namespace Barotrauma catch (Exception e) { var innermost = e.GetInnermost(); - DebugConsole.LogError($"Failed to load \"{filesToLoad[i].Path}\": {innermost.Message}\n{innermost.StackTrace}"); + DebugConsole.LogError($"Failed to load \"{filesToLoad[i].Path}\": {innermost.Message}\n{innermost.StackTrace}", contentPackage: this); exception = e; } if (exception != null) @@ -391,7 +391,8 @@ namespace Barotrauma DebugConsole.AddWarning( $"The following errors occurred while loading the content package \"{Name}\". The package might not work correctly.\n" + - string.Join('\n', FatalLoadErrors.Select(errorToStr))); + string.Join('\n', FatalLoadErrors.Select(errorToStr)), + this); static string errorToStr(LoadError error) => error.ToString(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs index f113e04f1..dc6a465be 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs @@ -155,6 +155,7 @@ namespace Barotrauma .Distinct(new TypeComparer()) .ForEach(f => f.Sort()); MergedHash = Md5Hash.MergeHashes(All.Select(cp => cp.Hash)); + TextManager.IncrementLanguageVersion(); } public static int IndexOf(ContentPackage contentPackage) diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs index c97b534ce..26afb242f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs @@ -70,7 +70,7 @@ namespace Barotrauma public Identifier[] GetAttributeIdentifierArray(string key, Identifier[] def, bool trim = true) => Element.GetAttributeIdentifierArray(key, def, trim); [return: NotNullIfNotNull("def")] public ImmutableHashSet GetAttributeIdentifierImmutableHashSet(string key, ImmutableHashSet? def, bool trim = true) => Element.GetAttributeIdentifierImmutableHashSet(key, def, trim); - + [return: NotNullIfNotNull(parameterName: "def")] public string? GetAttributeString(string key, string? def) => Element.GetAttributeString(key, def); public string GetAttributeStringUnrestricted(string key, string def) => Element.GetAttributeStringUnrestricted(key, def); public string[]? GetAttributeStringArray(string key, string[]? def, bool convertToLowerInvariant = false) => Element.GetAttributeStringArray(key, def, convertToLowerInvariant); @@ -90,6 +90,7 @@ namespace Barotrauma public Color? GetAttributeColor(string key) => Element.GetAttributeColor(key); public Color[]? GetAttributeColorArray(string key, Color[]? def) => Element.GetAttributeColorArray(key, def); public Rectangle GetAttributeRect(string key, in Rectangle def) => Element.GetAttributeRect(key, def); + public Version GetAttributeVersion(string key, Version def) => Element.GetAttributeVersion(key, def); public T GetAttributeEnum(string key, in T def) where T : struct, Enum => Element.GetAttributeEnum(key, def); public (T1, T2) GetAttributeTuple(string key, in (T1, T2) def) => Element.GetAttributeTuple(key, def); public (T1, T2)[] GetAttributeTupleArray(string key, in (T1, T2)[] def) => Element.GetAttributeTupleArray(key, def); diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 552fe6a3f..ae30a5412 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -7,6 +7,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.Immutable; using System.ComponentModel; using System.Globalization; using Barotrauma.IO; @@ -40,8 +41,8 @@ namespace Barotrauma { public partial class Command { - public readonly string[] names; - public readonly string help; + public readonly ImmutableArray Names; + public readonly string Help; public Action OnExecute; @@ -57,8 +58,8 @@ namespace Barotrauma /// public Command(string name, string help, Action onExecute, Func getValidArgs = null, bool isCheat = false) { - names = name.Split('|'); - this.help = help; + Names = name.Split('|').ToIdentifiers().ToImmutableArray(); + this.Help = help; this.OnExecute = onExecute; @@ -76,7 +77,8 @@ namespace Barotrauma #endif if (!allowCheats && !CheatsEnabled && IsCheat) { - NewMessage("You need to enable cheats using the command \"enablecheats\" before you can use the command \"" + names[0] + "\".", Color.Red); + NewMessage( + $"You need to enable cheats using the command \"enablecheats\" before you can use the command \"{Names.First()}\".", Color.Red); #if USE_STEAM NewMessage("Enabling cheats will disable Steam achievements during this play session.", Color.Red); #endif @@ -88,7 +90,7 @@ namespace Barotrauma public override int GetHashCode() { - return names[0].GetHashCode(); + return Names.First().GetHashCode(); } } @@ -164,7 +166,7 @@ namespace Barotrauma private static void AssignOnExecute(string names, Action onExecute) { - var matchingCommand = commands.Find(c => c.names.Intersect(names.Split('|')).Count() > 0); + var matchingCommand = commands.Find(c => c.Names.Intersect(names.Split('|').ToIdentifiers()).Any()); if (matchingCommand == null) { throw new Exception("AssignOnExecute failed. Command matching the name(s) \"" + names + "\" not found."); @@ -187,13 +189,13 @@ namespace Barotrauma { foreach (Command c in commands) { - if (string.IsNullOrEmpty(c.help)) continue; + if (string.IsNullOrEmpty(c.Help)) continue; ShowHelpMessage(c); } } else { - var matchingCommand = commands.Find(c => c.names.Any(name => name == args[0])); + var matchingCommand = commands.Find(c => c.Names.Any(name => name == args[0])); if (matchingCommand == null) { NewMessage("Command " + args[0] + " not found.", Color.Red); @@ -208,7 +210,7 @@ namespace Barotrauma { return new string[][] { - commands.SelectMany(c => c.names).ToArray(), + commands.SelectMany(c => c.Names).Select(n => n.Value).ToArray(), Array.Empty() }; })); @@ -403,7 +405,7 @@ namespace Barotrauma return new string[][] { GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(), - commands.Select(c => c.names[0]).Union(new string[]{ "All" }).ToArray() + commands.Select(c => c.Names.First().Value).Union(new []{ "All" }).ToArray() }; })); @@ -415,7 +417,7 @@ namespace Barotrauma return new string[][] { GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(), - commands.Select(c => c.names[0]).Union(new string[]{ "All" }).ToArray() + commands.Select(c => c.Names.First().Value).Union(new []{ "All" }).ToArray() }; })); @@ -856,7 +858,13 @@ namespace Barotrauma commands.Add(new Command("debugevent", "debugevent [identifier]: outputs debug info about a specific event that's currently active. Mainly intended for debugging events in multiplayer: in single player, the same information is available by enabling debugdraw.", (string[] args) => { - if (GameMain.GameSession?.EventManager is EventManager eventManager && args.Length > 0) + if (args.Length == 0) + { + ThrowError($"Please specify the identifier of the event you want to debug."); + return; + } + + if (GameMain.GameSession?.EventManager is EventManager eventManager) { var ev = eventManager.ActiveEvents.FirstOrDefault(ev => ev.Prefab?.Identifier == args[0]); if (ev == null) @@ -875,9 +883,18 @@ namespace Barotrauma } }, isCheat: true, getValidArgs: () => { + IEnumerable eventPrefabs; + if (GameMain.GameSession?.EventManager == null || GameMain.GameSession.EventManager.ActiveEvents.None()) + { + eventPrefabs = EventSet.GetAllEventPrefabs().Where(prefab => prefab.Identifier != Identifier.Empty); + } + else + { + eventPrefabs = GameMain.GameSession.EventManager.ActiveEvents.Select(e => e.Prefab); + } return new[] { - GameMain.GameSession?.EventManager?.ActiveEvents.Select(ev => ev.Prefab.Identifier.ToString()).ToArray() ?? Array.Empty() + eventPrefabs.Select(ev => ev.Identifier.ToString()).ToArray() ?? Array.Empty() }; })); @@ -1133,7 +1150,7 @@ namespace Barotrauma } },null)); - commands.Add(new Command("teleportsub", "teleportsub [start/end/cursor]: Teleport the submarine to the position of the cursor, or the start or end of the level. WARNING: does not take outposts into account, so often leads to physics glitches. Only use for debugging.", (string[] args) => + 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) => { if (Submarine.MainSub == null) { return; } @@ -1159,7 +1176,7 @@ namespace Barotrauma } Submarine.MainSub.SetPosition(pos); } - else + else if (args[0].Equals("end", StringComparison.OrdinalIgnoreCase)) { if (Level.Loaded == null) { @@ -1172,13 +1189,29 @@ namespace Barotrauma pos -= Vector2.UnitY * (Submarine.MainSub.Borders.Height + Level.Loaded.EndOutpost.Borders.Height) / 2; } Submarine.MainSub.SetPosition(pos); + } + else if (args[0].Equals("endoutpost", StringComparison.OrdinalIgnoreCase)) + { + Submarine.MainSub.SetPosition(Level.Loaded.EndExitPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height); + + var submarineDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == Submarine.MainSub); + if (Level.Loaded?.EndOutpost == null) + { + NewMessage("Can't teleport the sub to the end outpost (no outpost at the end of the level).", Color.Red); + return; + } + var outpostDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == Level.Loaded.EndOutpost); + if (submarineDockingPort != null && outpostDockingPort != null) + { + submarineDockingPort.Dock(outpostDockingPort); + } } }, () => { return new string[][] { - new string[] { "start", "end", "cursor" } + new string[] { "start", "end", "endoutpost", "cursor" } }; }, isCheat: true)); @@ -1595,7 +1628,7 @@ namespace Barotrauma int i = 0; foreach (LocationConnection connection in campaign.Map.CurrentLocation.Connections) { - NewMessage(" " + i + ". " + connection.OtherLocation(campaign.Map.CurrentLocation).Name, Color.White); + NewMessage(" " + i + ". " + connection.OtherLocation(campaign.Map.CurrentLocation).DisplayName, Color.White); i++; } ShowQuestionPrompt("Select a destination (0 - " + (campaign.Map.CurrentLocation.Connections.Count - 1) + "):", (string selectedDestination) => @@ -1609,7 +1642,7 @@ namespace Barotrauma } Location location = campaign.Map.CurrentLocation.Connections[destinationIndex].OtherLocation(campaign.Map.CurrentLocation); campaign.Map.SelectLocation(location); - NewMessage(location.Name + " selected.", Color.White); + NewMessage(location.DisplayName + " selected.", Color.White); }); } else @@ -1623,7 +1656,7 @@ namespace Barotrauma } Location location = campaign.Map.CurrentLocation.Connections[destinationIndex].OtherLocation(campaign.Map.CurrentLocation); campaign.Map.SelectLocation(location); - NewMessage(location.Name + " selected.", Color.White); + NewMessage(location.DisplayName + " selected.", Color.White); } })); @@ -1895,7 +1928,7 @@ namespace Barotrauma { if (location.Stores != null) { - var msg = "--- Location: " + location.Name + " ---"; + var msg = "--- Location: " + location.DisplayName + " ---"; foreach (var store in location.Stores) { msg += $"\nStore identifier: {store.Value.Identifier}"; @@ -1959,7 +1992,7 @@ namespace Barotrauma InitProjectSpecific(); - commands.Sort((c1, c2) => c1.names[0].CompareTo(c2.names[0])); + commands.Sort((c1, c2) => c1.Names.First().CompareTo(c2.Names.First())); } public static string AutoComplete(string command, int increment = 1) @@ -1970,14 +2003,14 @@ namespace Barotrauma //if an argument is given or the last character is a space, attempt to autocomplete the argument if (args.Length > 0 || (splitCommand.Length > 0 && command.Last() == ' ')) { - Command matchingCommand = commands.Find(c => c.names.Contains(splitCommand[0])); - if (matchingCommand == null || matchingCommand.GetValidArgs == null) return command; + Command matchingCommand = commands.Find(c => c.Names.Contains(splitCommand[0].ToIdentifier())); + if (matchingCommand?.GetValidArgs == null) { return command; } int autoCompletedArgIndex = args.Length > 0 && command.Last() != ' ' ? args.Length - 1 : args.Length; //get all valid arguments for the given command string[][] allArgs = matchingCommand.GetValidArgs(); - if (allArgs == null || allArgs.GetLength(0) < autoCompletedArgIndex + 1) return command; + if (allArgs == null || allArgs.GetLength(0) < autoCompletedArgIndex + 1) { return command; } if (string.IsNullOrEmpty(currentAutoCompletedCommand)) { @@ -1989,7 +2022,7 @@ namespace Barotrauma currentAutoCompletedCommand.Trim().Length <= arg.Length && arg.Substring(0, currentAutoCompletedCommand.Trim().Length).ToLower() == currentAutoCompletedCommand.Trim().ToLower()).ToArray(); - if (validArgs.Length == 0) return command; + if (validArgs.Length == 0) { return command; } currentAutoCompletedIndex = MathUtils.PositiveModulo(currentAutoCompletedIndex + increment, validArgs.Length); string autoCompletedArg = validArgs[currentAutoCompletedIndex]; @@ -2010,13 +2043,13 @@ namespace Barotrauma currentAutoCompletedCommand = command; } - List matchingCommands = new List(); + List matchingCommands = new List(); foreach (Command c in commands) { - foreach (string name in c.names) + foreach (var name in c.Names) { - if (currentAutoCompletedCommand.Length > name.Length) continue; - if (currentAutoCompletedCommand == name.Substring(0, currentAutoCompletedCommand.Length)) + if (currentAutoCompletedCommand.Length > name.Value.Length) { continue; } + if (name.StartsWith(currentAutoCompletedCommand)) { matchingCommands.Add(name); } @@ -2026,7 +2059,7 @@ namespace Barotrauma if (matchingCommands.Count == 0) return command; currentAutoCompletedIndex = MathUtils.PositiveModulo(currentAutoCompletedIndex + increment, matchingCommands.Count); - return matchingCommands[currentAutoCompletedIndex]; + return matchingCommands[currentAutoCompletedIndex].Value; } } @@ -2064,9 +2097,9 @@ namespace Barotrauma return; } - string firstCommand = splitCommand[0].ToLowerInvariant(); + Identifier firstCommand = splitCommand[0].ToIdentifier(); - if (!firstCommand.Equals("admin", StringComparison.OrdinalIgnoreCase)) + if (firstCommand != "admin") { NewCommand(command); } @@ -2074,7 +2107,7 @@ namespace Barotrauma #if CLIENT if (GameMain.Client != null) { - Command matchingCommand = commands.Find(c => c.names.Contains(firstCommand)); + 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 @@ -2095,12 +2128,12 @@ namespace Barotrauma } return; } - if (!IsCommandPermitted(splitCommand[0].ToLowerInvariant(), GameMain.Client)) + if (!IsCommandPermitted(firstCommand, GameMain.Client)) { #if DEBUG - AddWarning($"You're not permitted to use the command \"{splitCommand[0].ToLowerInvariant()}\". 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 \"{splitCommand[0].ToLowerInvariant()}\"!"); + ThrowError($"You're not permitted to use the command \"{firstCommand}\"!"); return; #endif } @@ -2110,7 +2143,7 @@ namespace Barotrauma bool commandFound = false; foreach (Command c in commands) { - if (!c.names.Contains(firstCommand)) { continue; } + if (!c.Names.Contains(firstCommand)) { continue; } c.Execute(splitCommand.Skip(1).ToArray()); commandFound = true; break; @@ -2397,8 +2430,9 @@ namespace Barotrauma #endif } - public static void LogError(string msg, Color? color = null) + public static void LogError(string msg, Color? color = null, ContentPackage contentPackage = null) { + msg = AddContentPackageInfoToMessage(msg, contentPackage); color ??= Color.Red; NewMessage(msg, color.Value, isCommand: false, isError: true); } @@ -2515,7 +2549,7 @@ namespace Barotrauma return true; } - public static Command FindCommand(string commandName) => commands.Find(c => c.names.Any(n => n.Equals(commandName, StringComparison.OrdinalIgnoreCase))); + public static Command FindCommand(string commandName) => commands.Find(c => c.Names.Contains(commandName.ToIdentifier())); public static void Log(LocalizedString message) => Log(message?.Value); @@ -2532,8 +2566,9 @@ namespace Barotrauma ThrowError(error.Value, e, createMessageBox, appendStackTrace); } - public static void ThrowError(string error, Exception e = null, bool createMessageBox = false, bool appendStackTrace = false) + public static void ThrowError(string error, Exception e = null, ContentPackage contentPackage = null, bool createMessageBox = false, bool appendStackTrace = false) { + error = AddContentPackageInfoToMessage(error, contentPackage); if (e != null) { error += " {" + e.Message + "}\n"; @@ -2547,7 +2582,7 @@ namespace Barotrauma error += "\n\nInner exception: " + innermost.Message + "\n"; if (innermost.StackTrace != null) { - error += innermost.StackTrace.CleanupStackTrace(); ; + error += innermost.StackTrace.CleanupStackTrace(); } } } @@ -2580,10 +2615,22 @@ namespace Barotrauma errorMsg); } - public static void AddWarning(string warning) + public static void AddWarning(string warning, ContentPackage contentPackage = null) { + warning = AddContentPackageInfoToMessage($"WARNING: {warning}", contentPackage); System.Diagnostics.Debug.WriteLine(warning); - NewMessage($"WARNING: {warning}", Color.Yellow); + NewMessage(warning, Color.Yellow); + } + + private static string AddContentPackageInfoToMessage(string message, ContentPackage contentPackage) + { + if (contentPackage == null) { return message; } +#if CLIENT + string color = XMLExtensions.ToStringHex(Color.MediumPurple); + return $"‖color:{color}‖[{contentPackage.Name}]‖color:end‖ {message}"; +#else + return $"[{contentPackage.Name}] {message}"; +#endif } #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index 813524345..9fcaed105 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -159,11 +159,13 @@ namespace Barotrauma OnItemDeconstructedInventory, OnStopTinkering, OnItemPicked, + OnItemSelected, OnGeneticMaterialCombinedOrRefined, OnCrewGeneticMaterialCombinedOrRefined, AfterSubmarineAttacked, OnApplyTreatment, OnStatusEffectIdentifier, + OnRepairedOutsideLeak } /// @@ -550,7 +552,27 @@ namespace Barotrauma /// /// Can be used to prevent certain talents from being unlocked by specifying the talent's identifier via CharacterAbilityGivePermanentStat. /// - LockedTalents + LockedTalents, + + /// + /// Used to reduce or increase the cost of hiring certain jobs by a percentage. + /// + HireCostMultiplier, + + /// + /// Used to increase how much items can stack in the characters inventory. + /// + InventoryExtraStackSize, + + /// + /// Modifies the range of the sounds emitted by the character (can be used to make the character easier or more difficult for monsters to hear) + /// + SoundRangeMultiplier, + + /// + /// Modifies how far the character can be seen from (can be used to make the character easier or more difficult for monsters to see) + /// + SightRangeMultiplier } internal enum ItemTalentStats @@ -565,7 +587,8 @@ namespace Barotrauma ReactorMaxOutput, ReactorFuelConsumption, DeconstructorSpeed, - FabricationSpeed + FabricationSpeed, + ExtraStackSize } /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs index 16622dee8..da3967687 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs @@ -34,7 +34,8 @@ namespace Barotrauma { if (prefab.ConfigElement.GetAttribute("itemname") != null) { - DebugConsole.ThrowError("Error in ArtifactEvent - use item identifier instead of the name of the item."); + DebugConsole.ThrowError("Error in ArtifactEvent - use item identifier instead of the name of the item.", + contentPackage: prefab?.ContentPackage); string itemName = prefab.ConfigElement.GetAttributeString("itemname", ""); itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab; if (itemPrefab == null) @@ -44,11 +45,12 @@ namespace Barotrauma } else { - string itemIdentifier = prefab.ConfigElement.GetAttributeString("itemidentifier", ""); - itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab; + Identifier itemIdentifier = prefab.ConfigElement.GetAttributeIdentifier("itemidentifier", Identifier.Empty); + itemPrefab = MapEntityPrefab.FindByIdentifier(itemIdentifier) as ItemPrefab; if (itemPrefab == null) { - DebugConsole.ThrowError("Error in ArtifactEvent - couldn't find an item prefab with the identifier " + itemIdentifier); + DebugConsole.ThrowError("Error in ArtifactEvent - couldn't find an item prefab with the identifier " + itemIdentifier, + contentPackage: prefab?.ContentPackage); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs index 1df74bcb8..0e4358efb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs @@ -39,7 +39,7 @@ namespace Barotrauma public Event(EventPrefab prefab) { - this.prefab = prefab; + this.prefab = prefab ?? throw new ArgumentNullException(nameof(prefab)); } public virtual IEnumerable GetFilesToPreload() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs index 5fed3e0ce..8c11d1829 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs @@ -1,3 +1,6 @@ +using Barotrauma.Extensions; +using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; @@ -8,21 +11,56 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes)] public Identifier TargetTag { get; set; } - private PropertyConditional Conditional { get; } + [Serialize(PropertyConditional.LogicalOperatorType.Or, IsPropertySaveable.Yes)] + public PropertyConditional.LogicalOperatorType LogicalOperator { get; set; } + + private ImmutableArray Conditionals { get; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier ApplyTagToLinkedHulls { get; set; } + + [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the hull the target item is inside when the item is used.")] + public Identifier ApplyTagToHull { get; set; } public CheckConditionalAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { 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: {GetEventName()} uses a CheckConditionalAction with no target tag! This will cause the check to automatically succeed.", + contentPackage: parentEvent.Prefab.ContentPackage); } - Conditional = PropertyConditional.FromXElement(element, IsNotTargetTagAttribute).FirstOrDefault(); - if (Conditional == null) + var conditionalElements = element.GetChildElements("Conditional"); + if (conditionalElements.None()) { - DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction with no valid PropertyConditional! This will cause the check to automatically succeed."); + //backwards compatibility + Conditionals = PropertyConditional.FromXElement(element, IsConditionalAttribute).ToImmutableArray(); + } + else + { + var conditionalList = new List(); + foreach (ContentXElement subElement in conditionalElements) + { + conditionalList.AddRange(PropertyConditional.FromXElement(subElement)); + break; + } + Conditionals = conditionalList.ToImmutableArray(); } - static bool IsNotTargetTagAttribute(XAttribute attribute) => attribute.NameAsIdentifier() != "targettag"; + if (Conditionals.None()) + { + DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction with no valid PropertyConditional! This will cause the check to automatically succeed.", + contentPackage: parentEvent.Prefab.ContentPackage); + } + + static bool IsConditionalAttribute(XAttribute attribute) + { + var nameAsIdentifier = attribute.NameAsIdentifier(); + return + nameAsIdentifier != nameof(TargetTag) && + nameAsIdentifier != nameof(LogicalOperator) && + nameAsIdentifier != nameof(ApplyTagToLinkedHulls) && + nameAsIdentifier != nameof(ApplyTagToHull); + } } private string GetEventName() @@ -32,31 +70,65 @@ namespace Barotrauma protected override bool? DetermineSuccess() { - ISerializableEntity target = null; + IEnumerable targets = null; if (!TargetTag.IsEmpty) { - foreach (var t in ParentEvent.GetTargets(TargetTag)) + targets = ParentEvent.GetTargets(TargetTag).OfType(); + } + + 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.", + contentPackage: ParentEvent.Prefab.ContentPackage); + } + + if (targets.None() || Conditionals.None()) + { + foreach (var target in targets) { - if (t is ISerializableEntity e) - { - target = e; - break; - } + ApplyTagsToHulls(target as Entity, ApplyTagToHull, ApplyTagToLinkedHulls); } - } - if (target == null) - { - 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."); - } - if (target == null || Conditional == null) - { return true; } + else + { + bool success = false; + foreach (var target in targets) + { + if (ConditionalsMatch(target)) + { + success = true; + ApplyTagsToHulls(target as Entity, ApplyTagToHull, ApplyTagToLinkedHulls); + } + } + return success; + } + } + + private bool ConditionalsMatch(ISerializableEntity target) + { + if (LogicalOperator == PropertyConditional.LogicalOperatorType.And) + { + return Conditionals.All(c => ConditionalMatches(target, c)); + } + else + { + return Conditionals.Any(c => ConditionalMatches(target, c)); + } + } + + private static bool ConditionalMatches(ISerializableEntity target, PropertyConditional conditional) + { if (target is Item item) { - return item.ConditionalMatches(Conditional); + if (!conditional.TargetItemComponent.IsNullOrEmpty() && + item.Components.None(ic => ic.Name == conditional.TargetItemComponent)) + { + return false; + } + return item.ConditionalMatches(conditional); } - return Conditional.Matches(target); + return conditional.Matches(target); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs index df038e70c..f56d50e29 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs @@ -30,7 +30,8 @@ namespace Barotrauma Condition = element.GetAttributeString("value", string.Empty)!; if (string.IsNullOrEmpty(Condition)) { - DebugConsole.ThrowError($"Error in scripted event \"{parentEvent.Prefab.Identifier}\". CheckDataAction with no condition set ({element})."); + DebugConsole.ThrowError($"Error in scripted event \"{parentEvent.Prefab.Identifier}\". CheckDataAction with no condition set ({element}).", + contentPackage: element?.ContentPackage); } } } @@ -42,7 +43,8 @@ namespace Barotrauma Condition = element.GetAttributeString("value", string.Empty)!; if (string.IsNullOrEmpty(Condition)) { - DebugConsole.ThrowError($"Error in scripted event \"{parentDebugString}\". CheckDataAction with no condition set ({element})."); + DebugConsole.ThrowError($"Error in scripted event \"{parentDebugString}\". CheckDataAction with no condition set ({element}).", + contentPackage: element?.ContentPackage); } } } @@ -59,7 +61,8 @@ namespace Barotrauma (Operator, string value) = PropertyConditional.ExtractComparisonOperatorFromConditionString(Condition); if (Operator == PropertyConditional.ComparisonOperatorType.None) { - DebugConsole.ThrowError($"{Condition} is invalid, it should start with an operator followed by a boolean or a floating point value."); + DebugConsole.ThrowError($"{Condition} is invalid, it should start with an operator followed by a boolean or a floating point value.", + contentPackage: ParentEvent?.Prefab?.ContentPackage); return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs index 275203b94..727d627dd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs @@ -77,7 +77,8 @@ namespace Barotrauma ItemIdentifiers.None() && TargetTag.IsEmpty) { - DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". {nameof(CheckItemAction)} does't define either tags or identifiers of the item to check."); + DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". {nameof(CheckItemAction)} does't define either tags or identifiers of the item to check.", + contentPackage: element.ContentPackage); } checkPercentage = element.GetAttribute(nameof(RequiredConditionalMatchPercentage)) is not null; if (checkPercentage && conditionals.None()) @@ -86,7 +87,8 @@ namespace Barotrauma } if (Amount != 1 && checkPercentage) { - DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". Cannot define both '{Amount}' and '{RequiredConditionalMatchPercentage}' in {nameof(CheckItemAction)}."); + DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". Cannot define both '{Amount}' and '{RequiredConditionalMatchPercentage}' in {nameof(CheckItemAction)}.", + contentPackage: element.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckOrderAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckOrderAction.cs index 870a7ee9c..39b427ba4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckOrderAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckOrderAction.cs @@ -32,7 +32,8 @@ namespace Barotrauma var targetCharacters = ParentEvent.GetTargets(TargetTag); if (targetCharacters.None()) { - DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckOrderAction but no valid target characters were found for tag \"{TargetTag}\"! This will cause the check to automatically fail."); + DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckOrderAction but no valid target characters were found for tag \"{TargetTag}\"! This will cause the check to automatically fail.", + contentPackage: ParentEvent.Prefab.ContentPackage); return false; } foreach (var t in targetCharacters) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckReputationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckReputationAction.cs index 5f6f19e47..478fb872d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckReputationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckReputationAction.cs @@ -1,7 +1,5 @@ #nullable enable -using System; using System.Diagnostics; -using System.Xml.Linq; namespace Barotrauma { @@ -31,7 +29,8 @@ namespace Barotrauma } default: { - DebugConsole.ThrowError("CheckReputationAction requires a \"TargetType\" but none were specified."); + DebugConsole.ThrowError("CheckReputationAction requires a \"TargetType\" but none were specified.", + contentPackage: ParentEvent.Prefab.ContentPackage); break; } } @@ -41,7 +40,8 @@ namespace Barotrauma protected override bool GetBool(CampaignMode campaignMode) { - DebugConsole.ThrowError("Boolean comparison cannot be applied to reputations."); + DebugConsole.ThrowError("Boolean comparison cannot be applied to reputations.", + contentPackage: ParentEvent.Prefab.ContentPackage); return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckSelectedAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckSelectedAction.cs index c03e7991e..0e673e047 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckSelectedAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckSelectedAction.cs @@ -87,13 +87,13 @@ namespace Barotrauma #if DEBUG void Error(string errorMsg) { - DebugConsole.ThrowError(errorMsg); + DebugConsole.ThrowError(errorMsg, contentPackage: ParentEvent.Prefab.ContentPackage); } #else void Error(string errorMsg) { - DebugConsole.LogError(errorMsg); + DebugConsole.LogError(errorMsg, contentPackage: ParentEvent.Prefab.ContentPackage); } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorEventStateAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorEventStateAction.cs index f3833fa6d..ab2b9fde6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorEventStateAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorEventStateAction.cs @@ -17,7 +17,8 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"Cannot use the action {nameof(CheckTraitorEventStateAction)} in the event \"{parentEvent.Prefab.Identifier}\" because it's not a traitor event."); + DebugConsole.ThrowError($"Cannot use the action {nameof(CheckTraitorEventStateAction)} in the event \"{parentEvent.Prefab.Identifier}\" because it's not a traitor event.", + contentPackage: element.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorVoteAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorVoteAction.cs index 7f8e26526..9ab84cd3e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorVoteAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorVoteAction.cs @@ -16,7 +16,8 @@ namespace Barotrauma { if (parentEvent is not TraitorEvent) { - DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\" - {nameof(CheckTraitorVoteAction)} can only be used in traitor events."); + DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\" - {nameof(CheckTraitorVoteAction)} can only be used in traitor events.", + contentPackage: element.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckVisibilityAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckVisibilityAction.cs index 35e4eeb9c..119fa0649 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckVisibilityAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckVisibilityAction.cs @@ -1,5 +1,6 @@ #nullable enable - +using Microsoft.Xna.Framework; +using System.Linq; namespace Barotrauma { @@ -8,12 +9,18 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes, description: "Tag of the entity to do the visibility check from.")] public Identifier EntityTag { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "Entities that also have this tag are excluded.")] + public Identifier ExcludedEntityTag { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the entity to do the visibility check to.")] public Identifier TargetTag { get; set; } [Serialize(false, IsPropertySaveable.Yes, description: "Does the entity need to be facing the target? Only valid if the entity is a character.")] public bool CheckFacing { get; set; } + [Serialize(1000.0f, IsPropertySaveable.Yes, description: "Maximum distance between the targets.")] + public float MaxDistance { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the entity who saw the target when the check succeeds.")] public Identifier ApplyTagToEntity { get; set; } @@ -31,11 +38,17 @@ namespace Barotrauma { foreach (var entity in ParentEvent.GetTargets(EntityTag)) { + if (!ExcludedEntityTag.IsEmpty) + { + if (ParentEvent.GetTargets(ExcludedEntityTag).Contains(entity)) { continue; } + } + foreach (var target in ParentEvent.GetTargets(TargetTag)) { if (!AllowSameEntity && entity == target) { continue; } - if (Character.IsTargetVisible(target, entity, CheckFacing)) - { + if (Vector2.DistanceSquared(target.WorldPosition, entity.WorldPosition) > MaxDistance * MaxDistance) { continue; } + if (Character.IsTargetVisible(target, entity, seeThroughWindows: true, CheckFacing)) + { if (!ApplyTagToEntity.IsEmpty) { ParentEvent.AddTarget(ApplyTagToEntity, entity); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs index 8092bdb91..7817c012c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs @@ -116,7 +116,8 @@ namespace Barotrauma { DebugConsole.ThrowError( $"Error in {nameof(EventObjectiveAction)} in the event \"{parentEvent.Prefab.Identifier}\"" + - $" - unrecognized child element \"Replace\"."); + $" - unrecognized child element \"Replace\".", + contentPackage: element.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CountTargetsAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CountTargetsAction.cs index c2287af04..70b6f74b9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CountTargetsAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CountTargetsAction.cs @@ -61,14 +61,16 @@ namespace Barotrauma } if (MinAmount > MaxAmount && MaxAmount > -1) { - DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". {MinAmount} is larger than {MaxAmount} in {nameof(CountTargetsAction)}."); + DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". {MinAmount} is larger than {MaxAmount} in {nameof(CountTargetsAction)}.", + contentPackage: element.ContentPackage); } } else { if (MinPercentageRelativeToTarget < 0.0f && MaxPercentageRelativeToTarget < 0.0f) { - DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". Comparing to another target, but neither {nameof(MinPercentageRelativeToTarget)} or {nameof(MaxPercentageRelativeToTarget)} is set."); + DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". Comparing to another target, but neither {nameof(MinPercentageRelativeToTarget)} or {nameof(MaxPercentageRelativeToTarget)} is set.", + contentPackage: element.ContentPackage); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs index dd086ec74..f9be2652e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs @@ -36,7 +36,8 @@ namespace Barotrauma { if (e.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase)) { - DebugConsole.ThrowError($"Error in event prefab \"{scriptedEvent.Prefab.Identifier}\". Status effect configured as a sub action (text: \"{Text}\"). Please configure status effects as child elements of a StatusEffectAction."); + DebugConsole.ThrowError($"Error in event prefab \"{scriptedEvent.Prefab.Identifier}\". Status effect configured as a sub action (text: \"{Text}\"). Please configure status effects as child elements of a StatusEffectAction.", + contentPackage: elem.ContentPackage); continue; } var action = Instantiate(scriptedEvent, e); @@ -140,14 +141,19 @@ namespace Barotrauma Identifier typeName = element.Name.ToString().ToIdentifier(); if (typeName == "TutorialSegmentAction") { - typeName = "EventObjectiveAction".ToIdentifier(); + typeName = nameof(EventObjectiveAction).ToIdentifier(); + } + else if (typeName == "TutorialHighlightAction") + { + typeName = nameof(HighlightAction).ToIdentifier(); } actionType = Type.GetType("Barotrauma." + typeName, throwOnError: true, ignoreCase: true); if (actionType == null) { throw new NullReferenceException(); } } catch { - DebugConsole.ThrowError($"Could not find an {nameof(EventAction)} class of the type \"{element.Name}\"."); + DebugConsole.ThrowError($"Could not find an {nameof(EventAction)} class of the type \"{element.Name}\".", + contentPackage: element.ContentPackage); return null; } @@ -162,11 +168,36 @@ namespace Barotrauma } catch (Exception ex) { - DebugConsole.ThrowError(ex.InnerException != null ? ex.InnerException.ToString() : ex.ToString()); + DebugConsole.ThrowError(ex.InnerException != null ? ex.InnerException.ToString() : ex.ToString(), + contentPackage: element.ContentPackage); return null; } } + protected void ApplyTagsToHulls(Entity entity, Identifier hullTag, Identifier linkedHullTag) + { + var currentHull = entity switch + { + Item item => item.CurrentHull, + Character character => character.CurrentHull, + _ => null, + }; + if (currentHull == null) { return; } + + if (!hullTag.IsEmpty) + { + ParentEvent.AddTarget(hullTag, currentHull); + } + if (!linkedHullTag.IsEmpty) + { + ParentEvent.AddTarget(linkedHullTag, currentHull); + foreach (var linkedHull in currentHull.GetLinkedEntities()) + { + ParentEvent.AddTarget(linkedHullTag, linkedHull); + } + } + } + /// /// Rich test to display in debugdraw /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventLogAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventLogAction.cs index 7fc3af8b5..c6f24003d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventLogAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventLogAction.cs @@ -24,7 +24,8 @@ namespace Barotrauma { if (Id == Identifier.Empty) { - DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\". {nameof(EventLogAction)} with no id."); + DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\". {nameof(EventLogAction)} with no id.", + contentPackage: element.ContentPackage); } //append the target tag so logs targeted to different players don't interfere with each other even if they use the same Id Id = (Id.ToString() + TargetTag).ToIdentifier(); @@ -42,7 +43,8 @@ namespace Barotrauma { if (Text.IsNullOrEmpty()) { - DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\". {nameof(EventLogAction)} with no text set ({element})."); + DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\". {nameof(EventLogAction)} with no text set ({element}).", + contentPackage: element.ContentPackage); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventObjectiveAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventObjectiveAction.cs index 17316466f..2faa398bf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventObjectiveAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventObjectiveAction.cs @@ -2,7 +2,7 @@ namespace Barotrauma { partial class EventObjectiveAction : EventAction { - public enum SegmentActionType { Trigger, Add, Complete, CompleteAndRemove, Remove, Fail, FailAndRemove }; + public enum SegmentActionType { Trigger, Add, AddIfNotFound, Complete, CompleteAndRemove, Remove, Fail, FailAndRemove }; [Serialize(SegmentActionType.Trigger, IsPropertySaveable.Yes)] public SegmentActionType Type { get; set; } @@ -49,13 +49,15 @@ namespace Barotrauma { DebugConsole.ThrowError( $"Error in {nameof(EventObjectiveAction)} in the event \"{parentEvent.Prefab.Identifier}\""+ - $" - {nameof(TextTag)} will do nothing unless the action triggers a message box or a video."); + $" - {nameof(TextTag)} will do nothing unless the action triggers a message box or a video.", + contentPackage: element.ContentPackage); } if (element.GetChildElement("Replace") != null) { DebugConsole.ThrowError( $"Error in {nameof(EventObjectiveAction)} in the event \"{parentEvent.Prefab.Identifier}\"" + - $" - unrecognized child element \"Replace\"."); + $" - unrecognized child element \"Replace\".", + contentPackage: element.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveExpAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveExpAction.cs index 7031d0daf..f2899ff8a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveExpAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveExpAction.cs @@ -14,7 +14,8 @@ namespace Barotrauma { if (TargetTag.IsEmpty) { - DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": {nameof(GiveExpAction)} without a target tag (the action needs to know whose skill to check)."); + DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": {nameof(GiveExpAction)} without a target tag (the action needs to know whose skill to check).", + contentPackage: element.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs index b051966cd..8f8cc7d0b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs @@ -17,7 +17,8 @@ namespace Barotrauma { if (TargetTag.IsEmpty) { - DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": {nameof(GiveSkillExpAction)} without a target tag (the action needs to know whose skill to check)."); + DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": {nameof(GiveSkillExpAction)} without a target tag (the action needs to know whose skill to check).", + contentPackage: element.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GoTo.cs index 95c317203..d2cb4011f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GoTo.cs @@ -5,17 +5,31 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes)] public string Name { get; set; } + [Serialize(-1, IsPropertySaveable.Yes)] + public int MaxTimes { get; set; } + + private int counter; + public GoTo(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } public override bool IsFinished(ref string goTo) { - goTo = Name; + if (counter < MaxTimes || MaxTimes <= 0) + { + goTo = Name; + counter++; + } return true; } public override string ToDebugString() { - return $"[-] Go to label \"{Name}\""; + string msg = $"[-] Go to label \"{Name}\""; + if (MaxTimes > 0) + { + msg += $" ({counter}/{MaxTimes})"; + } + return msg; } public override void Reset() { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/HighlightAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/HighlightAction.cs new file mode 100644 index 000000000..8900299ea --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/HighlightAction.cs @@ -0,0 +1,43 @@ +#nullable enable +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma; + +partial class HighlightAction : EventAction +{ + private static readonly Color highlightColor = Color.Orange; + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } + + [Serialize("", IsPropertySaveable.Yes, description: "Only the player controlling this character will see the highlight. If empty, all players will see it.")] + public Identifier TargetCharacter { get; set; } + + [Serialize(true, IsPropertySaveable.Yes)] + public bool State { get; set; } + + private bool isFinished; + + public HighlightAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + } + + public override void Update(float deltaTime) + { + if (isFinished) { return; } + var targetCharacters = TargetCharacter.IsEmpty ? null : ParentEvent.GetTargets(TargetCharacter).OfType(); + foreach (var target in ParentEvent.GetTargets(TargetTag)) + { + SetHighlightProjSpecific(target, targetCharacters); + } + isFinished = true; + } + + partial void SetHighlightProjSpecific(Entity entity, IEnumerable? targetCharacters); + + public override bool IsFinished(ref string goToLabel) => isFinished; + + public override void Reset() => isFinished = false; +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs index 3973643a5..4592e6751 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs @@ -37,11 +37,13 @@ namespace Barotrauma { if (MissionIdentifier.IsEmpty && MissionTag.IsEmpty) { - DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": neither MissionIdentifier or MissionTag has been configured."); + DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": neither MissionIdentifier or MissionTag has been configured.", + contentPackage: element.ContentPackage); } if (!MissionIdentifier.IsEmpty && !MissionTag.IsEmpty) { - DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": both MissionIdentifier or MissionTag have been configured. The tag will be ignored."); + DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": both MissionIdentifier or MissionTag have been configured. The tag will be ignored.", + contentPackage: element.ContentPackage); } LocationTypes = element.GetAttributeIdentifierArray("locationtype", Array.Empty()).ToImmutableArray(); random = new MTRandom(parentEvent.RandomSeed); @@ -103,11 +105,13 @@ namespace Barotrauma { if (!MissionIdentifier.IsEmpty) { - unlockedMission = unlockLocation.UnlockMissionByIdentifier(MissionIdentifier); + unlockedMission = unlockLocation.UnlockMissionByIdentifier(MissionIdentifier, + invokingContentPackage: ParentEvent.Prefab.ContentPackage); } else if (!MissionTag.IsEmpty) { - unlockedMission = unlockLocation.UnlockMissionByTag(MissionTag, random); + unlockedMission = unlockLocation.UnlockMissionByTag(MissionTag, random, + invokingContentPackage: ParentEvent.Prefab.ContentPackage); } if (campaign is MultiPlayerCampaign mpCampaign) { @@ -119,11 +123,11 @@ namespace Barotrauma campaign.Map.Discover(unlockLocation, checkTalents: false); if (unlockedMission.Locations[0] == unlockedMission.Locations[1] || unlockedMission.Locations[1] == null) { - DebugConsole.NewMessage($"Unlocked mission \"{unlockedMission.Name}\" in the location \"{unlockLocation.Name}\"."); + DebugConsole.NewMessage($"Unlocked mission \"{unlockedMission.Name}\" in the location \"{unlockLocation.DisplayName}\"."); } else { - DebugConsole.NewMessage($"Unlocked mission \"{unlockedMission.Name}\" in the connection from \"{unlockedMission.Locations[0].Name}\" to \"{unlockedMission.Locations[1].Name}\"."); + DebugConsole.NewMessage($"Unlocked mission \"{unlockedMission.Name}\" in the connection from \"{unlockedMission.Locations[0].DisplayName}\" to \"{unlockedMission.Locations[1].DisplayName}\"."); } #if CLIENT new GUIMessageBox(string.Empty, TextManager.GetWithVariable("missionunlocked", "[missionname]", unlockedMission.Name), @@ -139,7 +143,8 @@ namespace Barotrauma } else { - DebugConsole.AddWarning($"Failed to find a suitable location to unlock the mission \"{missionDebugId}\" (LocationType: {string.Join(", ", LocationTypes)}, MinLocationDistance: {MinLocationDistance}, UnlockFurtherOnMap: {UnlockFurtherOnMap})"); + DebugConsole.AddWarning($"Failed to find a suitable location to unlock the mission \"{missionDebugId}\" (LocationType: {string.Join(", ", LocationTypes)}, MinLocationDistance: {MinLocationDistance}, UnlockFurtherOnMap: {UnlockFurtherOnMap})", + ParentEvent.Prefab.ContentPackage); } } isFinished = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs index d6deb9b1c..02b726aa8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs @@ -24,7 +24,8 @@ namespace Barotrauma State = element.GetAttributeInt("value", State); if (MissionIdentifier.IsEmpty) { - DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": MissionIdentifier has not been configured."); + DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": MissionIdentifier has not been configured.", + contentPackage: element.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ModifyLocationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ModifyLocationAction.cs index 013b48771..a21925c94 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ModifyLocationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ModifyLocationAction.cs @@ -12,7 +12,7 @@ namespace Barotrauma public Identifier Type { get; set; } [Serialize("", IsPropertySaveable.Yes)] - public string Name { get; set; } + public Identifier Name { get; set; } private bool isFinished; @@ -43,7 +43,8 @@ namespace Barotrauma var faction = campaign.Factions.Find(f => f.Prefab.Identifier == Faction); if (faction == null) { - DebugConsole.ThrowError($"Error in ModifyLocationAction ({ParentEvent.Prefab.Identifier}): could not find a faction with the identifier \"{Faction}\"."); + DebugConsole.ThrowError($"Error in ModifyLocationAction ({ParentEvent.Prefab.Identifier}): could not find a faction with the identifier \"{Faction}\".", + contentPackage: ParentEvent?.Prefab?.ContentPackage); } else { @@ -55,7 +56,8 @@ namespace Barotrauma var secondaryFaction = campaign.Factions.Find(f => f.Prefab.Identifier == SecondaryFaction); if (secondaryFaction == null) { - DebugConsole.ThrowError($"Error in ModifyLocationAction ({ParentEvent.Prefab.Identifier}): could not find a faction with the identifier \"{SecondaryFaction}\"."); + DebugConsole.ThrowError($"Error in ModifyLocationAction ({ParentEvent.Prefab.Identifier}): could not find a faction with the identifier \"{SecondaryFaction}\".", + contentPackage: ParentEvent.Prefab.ContentPackage); } else { @@ -67,16 +69,17 @@ namespace Barotrauma var locationType = LocationType.Prefabs.Find(lt => lt.Identifier == Type); if (locationType == null) { - DebugConsole.ThrowError($"Error in ModifyLocationAction ({ParentEvent.Prefab.Identifier}): could not find a location type with the identifier \"{Type}\"."); + DebugConsole.ThrowError($"Error in ModifyLocationAction ({ParentEvent.Prefab.Identifier}): could not find a location type with the identifier \"{Type}\".", + contentPackage: ParentEvent.Prefab.ContentPackage); } else if (!location.LocationTypeChangesBlocked) { location.ChangeType(campaign, locationType); } } - if (!string.IsNullOrEmpty(Name)) + if (!Name.IsEmpty) { - location.ForceName(TextManager.Get(Name).Fallback(Name).Value); + location.ForceName(Name); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs index 64861f6e4..239b95f60 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs @@ -30,7 +30,8 @@ namespace Barotrauma var enums = Enum.GetValues(typeof(CharacterTeamType)).Cast(); if (!enums.Contains(TeamID)) { - DebugConsole.ThrowError($"Error in {nameof(NPCChangeTeamAction)} in the event {ParentEvent.Prefab.Identifier}. \"{TeamID}\" is not a valid Team ID. Valid values are {string.Join(',', Enum.GetNames(typeof(CharacterTeamType)))}."); + DebugConsole.ThrowError($"Error in {nameof(NPCChangeTeamAction)} in the event {ParentEvent.Prefab.Identifier}. \"{TeamID}\" is not a valid Team ID. Valid values are {string.Join(',', Enum.GetNames(typeof(CharacterTeamType)))}.", + contentPackage: element.ContentPackage); } } @@ -100,7 +101,7 @@ namespace Barotrauma WayPoint.WayPointList.Find(wp => wp.Submarine == sub && wp.SpawnType == SpawnType.Human); if (subWaypoint != null) { - npc.GiveIdCardTags(subWaypoint, requireSpawnPointTagsNotGiven: false, createNetworkEvent: true); + npc.GiveIdCardTags(subWaypoint, createNetworkEvent: true); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RNGAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RNGAction.cs index 3ab365409..a9e4ffea3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RNGAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RNGAction.cs @@ -13,11 +13,13 @@ namespace Barotrauma { if (Chance >= 1.0f) { - DebugConsole.ThrowError($"Incorrectly configured RNG Action in event \"{parentEvent.Prefab.Identifier}\". Probability is 1.0 (100%) or more, the action will always succeed."); + DebugConsole.ThrowError($"Incorrectly configured RNG Action in event \"{parentEvent.Prefab.Identifier}\". Probability is 1.0 (100%) or more, the action will always succeed.", + contentPackage: element.ContentPackage); } else if (Chance <= 0.0f) { - DebugConsole.ThrowError($"Incorrectly configured RNG Action in event \"{parentEvent.Prefab.Identifier}\". Probability is 0 or less, the action will never succeed."); + DebugConsole.ThrowError($"Incorrectly configured RNG Action in event \"{parentEvent.Prefab.Identifier}\". Probability is 0 or less, the action will never succeed.", + contentPackage: element.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs index 41c9391f2..f161a63fa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs @@ -54,7 +54,8 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"Faction with the identifier \"{Identifier}\" was not found."); + DebugConsole.ThrowError($"Faction with the identifier \"{Identifier}\" was not found.", + contentPackage: ParentEvent.Prefab.ContentPackage); } break; @@ -66,7 +67,8 @@ namespace Barotrauma } default: { - DebugConsole.ThrowError("ReputationAction requires a \"TargetType\" but none were specified."); + DebugConsole.ThrowError("ReputationAction requires a \"TargetType\" but none were specified.", + contentPackage: ParentEvent.Prefab.ContentPackage); break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetTraitorEventStateAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetTraitorEventStateAction.cs index 5361d5696..af798fefc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetTraitorEventStateAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetTraitorEventStateAction.cs @@ -14,7 +14,8 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"Cannot use the action {nameof(SetTraitorEventStateAction)} in the event \"{parentEvent.Prefab.Identifier}\" because it's not a traitor event."); + DebugConsole.ThrowError($"Cannot use the action {nameof(SetTraitorEventStateAction)} in the event \"{parentEvent.Prefab.Identifier}\" because it's not a traitor event.", + contentPackage: element.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SkillCheckAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SkillCheckAction.cs index c74ddea95..b3376e4a3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SkillCheckAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SkillCheckAction.cs @@ -23,7 +23,8 @@ namespace Barotrauma { if (TargetTag.IsEmpty) { - DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": SkillCheckAction without a target tag (the action needs to know whose skill to check)."); + DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": SkillCheckAction without a target tag (the action needs to know whose skill to check).", + contentPackage: element.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs index e9c3a624e..15a58b8c0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -105,7 +105,8 @@ namespace Barotrauma { DebugConsole.ThrowError( $"Error in even \"{(parentEvent.Prefab?.Identifier.ToString() ?? "unknown")}\". " + - $"The attribute \"submarinetype\" is not valid in {nameof(SpawnAction)}. Did you mean {nameof(SpawnLocation)}?"); + $"The attribute \"submarinetype\" is not valid in {nameof(SpawnAction)}. Did you mean {nameof(SpawnLocation)}?", + contentPackage: ParentEvent.Prefab.ContentPackage); } } @@ -233,7 +234,8 @@ namespace Barotrauma { if (MapEntityPrefab.FindByIdentifier(ItemIdentifier) is not ItemPrefab itemPrefab) { - DebugConsole.ThrowError("Error in SpawnAction (item prefab \"" + ItemIdentifier + "\" not found)"); + DebugConsole.ThrowError("Error in SpawnAction (item prefab \"" + ItemIdentifier + "\" not found)", + contentPackage: ParentEvent.Prefab.ContentPackage); } else { @@ -256,7 +258,8 @@ namespace Barotrauma if (spawnInventory == null) { - DebugConsole.ThrowError($"Could not spawn \"{ItemIdentifier}\" in target inventory \"{TargetInventory}\" - matching target not found."); + DebugConsole.ThrowError($"Could not spawn \"{ItemIdentifier}\" in target inventory \"{TargetInventory}\" - matching target not found.", + contentPackage: ParentEvent.Prefab.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs index 508ff6a43..8a1e389fe 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs @@ -90,31 +90,46 @@ namespace Barotrauma private void TagPlayers() { - AddTargetPredicate(Tag, e => e is Character c && c.IsPlayer && (!c.IsIncapacitated || !IgnoreIncapacitatedCharacters)); + AddTargetPredicate( + Tag, + ScriptedEvent.TargetPredicate.EntityType.Character, + e => e is Character c && c.IsPlayer && (!c.IsIncapacitated || !IgnoreIncapacitatedCharacters)); } private void TagTraitors() { - AddTargetPredicate(Tags.Traitor, e => e is Character c && (c.IsPlayer || c.IsBot) && c.IsTraitor && !c.IsIncapacitated); + AddTargetPredicate( + Tags.Traitor, + ScriptedEvent.TargetPredicate.EntityType.Character, + e => e is Character c && (c.IsPlayer || c.IsBot) && c.IsTraitor && !c.IsIncapacitated); } private void TagNonTraitors() { - AddTargetPredicate(Tags.NonTraitor, e => e is Character c && (c.IsPlayer || c.IsBot) && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated); + AddTargetPredicate( + Tags.NonTraitor, + ScriptedEvent.TargetPredicate.EntityType.Character, + e => e is Character c && (c.IsPlayer || c.IsBot) && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated); } private void TagNonTraitorPlayers() { - AddTargetPredicate(Tags.NonTraitorPlayer, e => e is Character c && c.IsPlayer && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated); + AddTargetPredicate( + Tags.NonTraitorPlayer, + ScriptedEvent.TargetPredicate.EntityType.Character, + e => e is Character c && c.IsPlayer && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated); } private void TagBots(bool playerCrewOnly) { - AddTargetPredicate(Tag, e => - e is Character c && - c.IsBot && - (!c.IsIncapacitated || !IgnoreIncapacitatedCharacters) && - (!playerCrewOnly || c.TeamID == CharacterTeamType.Team1)); + AddTargetPredicate( + Tag, + ScriptedEvent.TargetPredicate.EntityType.Character, + e => + e is Character c && + c.IsBot && + (!c.IsIncapacitated || !IgnoreIncapacitatedCharacters) && + (!playerCrewOnly || c.TeamID == CharacterTeamType.Team1)); } private void TagCrew() @@ -139,42 +154,67 @@ namespace Barotrauma private void TagStructuresByIdentifier(Identifier identifier) { - AddTargetPredicate(Tag, e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.Prefab.Identifier == identifier); + AddTargetPredicate( + Tag, + ScriptedEvent.TargetPredicate.EntityType.Structure, + e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.Prefab.Identifier == identifier); } private void TagStructuresBySpecialTag(Identifier tag) { - AddTargetPredicate(Tag, e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.SpecialTag.ToIdentifier() == tag); + AddTargetPredicate( + Tag, + ScriptedEvent.TargetPredicate.EntityType.Structure, + e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.SpecialTag.ToIdentifier() == tag); } private void TagItemsByIdentifier(Identifier identifier) { - AddTargetPredicate(Tag, e => e is Item it && IsValidItem(it) && it.Prefab.Identifier == identifier); + AddTargetPredicate( + Tag, + ScriptedEvent.TargetPredicate.EntityType.Item, + e => e is Item it && IsValidItem(it) && it.Prefab.Identifier == identifier); } private void TagItemsByTag(Identifier tag) { - AddTargetPredicate(Tag, e => e is Item it && IsValidItem(it) && it.HasTag(tag)); + AddTargetPredicate( + Tag, + ScriptedEvent.TargetPredicate.EntityType.Item, + e => e is Item it && IsValidItem(it) && it.HasTag(tag)); } private void TagHulls() { - AddTargetPredicate(Tag, e => e is Hull h && SubmarineTypeMatches(h.Submarine)); + AddTargetPredicate( + Tag, + ScriptedEvent.TargetPredicate.EntityType.Hull, + e => e is Hull h && SubmarineTypeMatches(h.Submarine)); } private void TagHullsByName(Identifier name) { - AddTargetPredicate(Tag, e => e is Hull h && SubmarineTypeMatches(h.Submarine) && h.RoomName.Contains(name.Value, StringComparison.OrdinalIgnoreCase)); + AddTargetPredicate( + Tag, + ScriptedEvent.TargetPredicate.EntityType.Hull, + e => e is Hull h && SubmarineTypeMatches(h.Submarine) && h.RoomName.Contains(name.Value, StringComparison.OrdinalIgnoreCase)); } private void TagSubmarinesByType(Identifier type) { - AddTargetPredicate(Tag, e => e is Submarine s && SubmarineTypeMatches(s) && (type.IsEmpty || type == s.Info?.Type.ToIdentifier())); + AddTargetPredicate( + Tag, + ScriptedEvent.TargetPredicate.EntityType.Submarine, + e => e is Submarine s && SubmarineTypeMatches(s) && (type.IsEmpty || type == s.Info?.Type.ToIdentifier())); } private bool IsValidItem(Item it) { - return (!it.HiddenInGame || AllowHiddenItems) && SubmarineTypeMatches(it.Submarine); + return + (!it.HiddenInGame || AllowHiddenItems) && + //if the item has just spawned, it may be in a hull but not moved into the coordinate space of the hull yet + //= it.Submarine still null + SubmarineTypeMatches(it.Submarine ?? it.CurrentHull?.Submarine ?? it.ParentInventory?.Owner?.Submarine); } private bool SubmarineTypeMatches(Submarine sub) @@ -197,7 +237,7 @@ namespace Barotrauma } } - private void AddTargetPredicate(Identifier tag, Predicate predicate) + private void AddTargetPredicate(Identifier tag, ScriptedEvent.TargetPredicate.EntityType entityType, Predicate predicate) { if (ChoosePercentage > 0.0f) { @@ -209,7 +249,7 @@ namespace Barotrauma } else { - ParentEvent.AddTargetPredicate(tag, predicate); + ParentEvent.AddTargetPredicate(tag, entityType, predicate); mustRecheckTargets = true; } } @@ -291,7 +331,8 @@ namespace Barotrauma else { string errorMessage = $"Error in TagAction (event \"{ParentEvent.Prefab.Identifier}\") - unrecognized target criteria \"{key}\"."; - DebugConsole.ThrowError(errorMessage); + DebugConsole.ThrowError(errorMessage, + contentPackage: ParentEvent.Prefab?.ContentPackage); GameAnalyticsManager.AddErrorEventOnce($"TagAction.Update:InvalidCriteria_{ParentEvent.Prefab.Identifier}_{key}", GameAnalyticsManager.ErrorSeverity.Error, errorMessage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs index 51e265f83..ae6a8c75c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs @@ -36,7 +36,8 @@ var eventPrefab = EventSet.GetEventPrefab(Identifier); if (eventPrefab == null) { - DebugConsole.ThrowError($"Error in TriggerEventAction - could not find an event with the identifier {Identifier}."); + DebugConsole.ThrowError($"Error in TriggerEventAction - could not find an event with the identifier {Identifier}.", + contentPackage: ParentEvent.Prefab.ContentPackage); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialHighlightAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialHighlightAction.cs deleted file mode 100644 index 190f7fdc0..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialHighlightAction.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace Barotrauma; - -partial class TutorialHighlightAction : EventAction -{ - [Serialize("", IsPropertySaveable.Yes)] - public Identifier TargetTag { get; set; } - - [Serialize(true, IsPropertySaveable.Yes)] - public bool State { get; set; } - - private bool isFinished; - - public TutorialHighlightAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) - { - if (GameMain.NetworkMember != null) - { - DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": {nameof(TutorialHighlightAction)} is not supported in multiplayer."); - } - } - - public override void Update(float deltaTime) - { - if (isFinished) { return; } - UpdateProjSpecific(); - isFinished = true; - } - - partial void UpdateProjSpecific(); - - public override bool IsFinished(ref string goToLabel) => isFinished; - - public override void Reset() => isFinished = false; -} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemFabricatedAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemFabricatedAction.cs index 601981cb6..89106db36 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemFabricatedAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemFabricatedAction.cs @@ -28,7 +28,8 @@ namespace Barotrauma { if (ItemTag.IsEmpty && ItemIdentifier.IsEmpty) { - DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". {nameof(WaitForItemFabricatedAction)} does't define either a tag or an identifier of the item to check."); + DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". {nameof(WaitForItemFabricatedAction)} does't define either a tag or an identifier of the item to check.", + contentPackage: element.ContentPackage); } foreach (var item in Item.ItemList) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemUsedAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemUsedAction.cs index d7e48a299..dce9b2f62 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemUsedAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemUsedAction.cs @@ -30,11 +30,16 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the hull the target item is inside, and all the hulls it's linked to, when the item is used.")] public Identifier ApplyTagToLinkedHulls { get; set; } + [Serialize(1, IsPropertySaveable.Yes)] + public int RequiredUseCount { get; set; } + private bool isFinished; private readonly HashSet targets = new HashSet(); private readonly HashSet targetComponents = new HashSet(); + private int useCount = 0; + private Identifier onUseEventIdentifier; private Identifier OnUseEventIdentifier { @@ -58,6 +63,14 @@ namespace Barotrauma private void OnItemUsed(Item item, Character user) { + if (!UserTag.IsEmpty) + { + if (!ParentEvent.GetTargets(UserTag).Contains(user)) { return; } + } + + useCount++; + if (useCount < RequiredUseCount) { return; } + if (!ApplyTagToItem.IsEmpty) { ParentEvent.AddTarget(ApplyTagToItem, item); @@ -66,22 +79,7 @@ namespace Barotrauma { ParentEvent.AddTarget(ApplyTagToUser, user); } - if (item.CurrentHull != null) - { - if (!ApplyTagToHull.IsEmpty) - { - ParentEvent.AddTarget(ApplyTagToHull, item.CurrentHull); - } - if (!ApplyTagToLinkedHulls.IsEmpty) - { - ParentEvent.AddTarget(ApplyTagToLinkedHulls, item.CurrentHull); - foreach (var linkedHull in item.CurrentHull.GetLinkedEntities()) - { - ParentEvent.AddTarget(ApplyTagToLinkedHulls, linkedHull); - } - } - } - + ApplyTagsToHulls(item, ApplyTagToHull, ApplyTagToLinkedHulls); DeregisterTargets(); isFinished = true; } @@ -142,6 +140,7 @@ namespace Barotrauma public override void Reset() { isFinished = false; + useCount = 0; DeregisterTargets(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index c7c89bd26..72f17db3c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -412,6 +412,7 @@ namespace Barotrauma preloadedSprites.ForEach(s => s.Remove()); preloadedSprites.Clear(); + timeStamps.Clear(); pathFinder = null; } @@ -518,7 +519,8 @@ namespace Barotrauma { foreach (Identifier missingId in subEventPrefab.GetMissingIdentifiers()) { - DebugConsole.ThrowError($"Error in event set \"{eventSet.Identifier}\" ({eventSet.ContentFile?.ContentPackage?.Name ?? "null"}) - could not find an event prefab with the identifier \"{missingId}\"."); + DebugConsole.ThrowError($"Error in event set \"{eventSet.Identifier}\" ({eventSet.ContentFile?.ContentPackage?.Name ?? "null"}) - could not find an event prefab with the identifier \"{missingId}\".", + contentPackage: eventSet.ContentPackage); } } @@ -904,7 +906,18 @@ namespace Barotrauma activeEvents.Add(QueuedEvents.Dequeue()); } } - + + public void EntitySpawned(Entity entity) + { + foreach (var ev in activeEvents) + { + if (ev is ScriptedEvent scriptedEvent) + { + scriptedEvent.EntitySpawned(entity); + } + } + } + private void CalculateCurrentIntensity(float deltaTime) { intensityUpdateTimer -= deltaTime; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs index e1c076d3c..721f1bcb0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs @@ -10,16 +10,48 @@ namespace Barotrauma public readonly ContentXElement ConfigElement; public readonly Type EventType; + + /// + /// The probability for the event to do something if it gets selected. For example, the probability for a MonsterEvent to spawn the monster(s). + /// public readonly float Probability; + + /// + /// When this event occurs, should it trigger the event cooldown during which no new events are triggered? + /// public readonly bool TriggerEventCooldown; + + /// + /// The commonness of the event (i.e. how likely it is for this specific event to be chosen from the event set it's configured in). + /// Only valid if the event set is configured to choose a random event (as opposed to just executing all the events in the set). + /// public readonly float Commonness; + + /// + /// If set, the event set can only be chosen in this biome. + /// public readonly Identifier BiomeIdentifier; + + /// + /// If set, the event set can only be chosen in locations that belong to this faction. + /// public readonly Identifier Faction; public readonly LocalizedString Name; + /// + /// If set, this event is used as an event that can unlock a path to the next biome. + /// public readonly bool UnlockPathEvent; + + /// + /// Only valid if UnlockPathEvent is set to true. The tooltip displayed on the pathway this event is blocking. + /// public readonly string UnlockPathTooltip; + + /// + /// Only valid if UnlockPathEvent is set to true. The reputation requirement displayed on the pathway this event is blocking. + /// public readonly int UnlockPathReputation; public static EventPrefab Create(ContentXElement element, RandomEventsFile file, Identifier fallbackIdentifier = default) @@ -44,12 +76,14 @@ namespace Barotrauma EventType = Type.GetType("Barotrauma." + ConfigElement.Name, true, true); if (EventType == null) { - DebugConsole.ThrowError("Could not find an event class of the type \"" + ConfigElement.Name + "\"."); + DebugConsole.ThrowError("Could not find an event class of the type \"" + ConfigElement.Name + "\".", + contentPackage: element.ContentPackage); } } catch { - DebugConsole.ThrowError("Could not find an event class of the type \"" + ConfigElement.Name + "\"."); + DebugConsole.ThrowError("Could not find an event class of the type \"" + ConfigElement.Name + "\".", + contentPackage: element.ContentPackage); } Name = TextManager.Get($"eventname.{Identifier}").Fallback(Identifier.ToString()); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs index e43b63056..b68a159dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs @@ -23,6 +23,10 @@ namespace Barotrauma } #endif + /// + /// Event sets are sets of random events that occur within a level (most commonly, monster spawns and scripted events). + /// Event sets can also be nested: a "parent set" can choose from several "subsets", either randomly or by some kind of criteria. + /// sealed class EventSet : Prefab { internal class EventDebugStats @@ -78,21 +82,48 @@ namespace Barotrauma return GetAllEventPrefabs().Find(prefab => prefab.Identifier == identifier); } + /// + /// If enabled, this set can only be chosen in the campaign mode. + /// public readonly bool IsCampaignSet; - //0-100 - public readonly float MinLevelDifficulty, MaxLevelDifficulty; + /// + /// The difficulty of the current level must be equal to or higher than this for this set to be chosen. + /// + public readonly float MinLevelDifficulty; + /// + /// The difficulty of the current level must be equal to or less than this for this set to be chosen. + /// + public readonly float MaxLevelDifficulty; + /// + /// If set, the event set can only be chosen in this biome. + /// public readonly Identifier BiomeIdentifier; + /// + /// If set, the event set can only be chosen in this type of level (outpost level or a connection between outpost levels). + /// public readonly LevelData.LevelType LevelType; + /// + /// If set, the event set can only be chosen in locations of this type. + /// public readonly ImmutableArray LocationTypeIdentifiers; + /// + /// If set, the event set can only be chosen in locations that belong to this faction. + /// public readonly Identifier Faction; + /// + /// If set, one event, or a sub event set, is chosen randomly from this set. + /// public readonly bool ChooseRandom; + /// + /// Only valid if ChooseRandom is enabled. How many random events to choose from the set? + /// private readonly int eventCount = 1; public readonly int SubSetCount = 1; private readonly Dictionary overrideEventCount = new Dictionary(); @@ -102,47 +133,100 @@ namespace Barotrauma /// public readonly bool Exhaustible; + /// + /// The event set won't become active until the submarine has travelled at least this far. A value between 0-1, where 0 is the beginning of the level and 1 the end of the level (e.g. 0.5 would mean the sub needs to be half-way through the level). + /// public readonly float MinDistanceTraveled; + + /// + /// The event set won't become active until the round has lasted at least this many seconds. + /// public readonly float MinMissionTime; //the events in this set are delayed if the current EventManager intensity is not between these values public readonly float MinIntensity, MaxIntensity; + /// + /// If the event is not allowed at start, it won't become active until the submarine has moved at least 50 meters away from the beginning of the level. Only valid in LocationConnections (levels between locations). + /// public readonly bool AllowAtStart; + /// + /// Normally an event (such as a monster spawn) triggers a cooldown during which no new events are created. This can be used to ignore the cooldown. + /// public readonly bool IgnoreCoolDown; + /// + /// Should this event set trigger the event cooldown (during which no new events are created) when it becomes active? + /// + public readonly bool TriggerEventCooldown; + + /// + /// Normally events can only trigger if the intensity of the situation is low enough (e.g. you won't get new monster spawns if the submarine is already facing a disaster). This can be used to ignore the intensity. + /// public readonly bool IgnoreIntensity; - public readonly bool PerRuin, PerCave, PerWreck; + /// + /// The set is applied once per each ruin in the level. Can be used to ensure there's a consistent amount of monster spawns in the ruins in the level regardless of how many there are (and that no ruin monsters spawn if there are no ruins). + /// + public readonly bool PerRuin; + + /// + /// The set is applied once per each cave in the level. Can be used to ensure there's a consistent amount of monster spawns in the cave in the level regardless of how many there are (and that no cave monsters spawn if there are no caves). + /// + public readonly bool PerCave; + + /// + /// The set is applied once per each wreck in the level. Can be used to ensure there's a consistent amount of monster spawns in the wreck in the level regardless of how many there are (and that no wreck monsters spawn if there are no wreck). + /// + public readonly bool PerWreck; + + /// + /// If enabled, this event will not be applied if the level contains hunting grounds. + /// public readonly bool DisableInHuntingGrounds; /// - /// If true, events from this set can only occur once in the level. + /// If enabled, events from this set can only occur once in the level. /// public readonly bool OncePerLevel; + /// + /// Should the event set be delayed if at least half of the crew is away from the submarine? The maximum amount of time the events can get delayed is defined in event manager settings () + /// public readonly bool DelayWhenCrewAway; - public readonly bool TriggerEventCooldown; - + /// + /// Additive sets are important to be aware of when creating custom event sets! If an additive set gets chosen for a level, the game will also select a non-additive one. + /// This means you can for example configure an additive set that spawns custom monsters (and make it very common if you want the monsters to spawn frequently), which will spawn those custom + /// monsters in addition to the vanilla monsters spawned by vanilla sets, without you having to add your custom monsters to every single vanilla set. + /// public readonly bool Additive; + /// + /// The commonness of the event set (i.e. how likely it is for this specific set to be chosen). + /// public readonly float DefaultCommonness; public readonly ImmutableDictionary OverrideCommonness; + /// + /// If set, the event set can trigger again after this amount of seconds has passed since it last triggered. + /// public readonly float ResetTime; /// - /// Used to force an event set based on how many other locations have been discovered before this. (Used for campaign tutorial event sets.) + /// Used to force an event set based on how many other locations have been discovered before this (used for campaign tutorial event sets). /// public readonly int ForceAtDiscoveredNr; /// - /// Used to force an event set based on how many other outposts have been visited before this. (Used for campaign tutorial event sets.) + /// Used to force an event set based on how many other outposts have been visited before this (used for campaign tutorial event sets). /// public readonly int ForceAtVisitedNr; + /// + /// If enabled, this set can only occur when the campaign tutorial is enabled (generally used for the tutorial events). + /// public readonly bool CampaignTutorialOnly; public readonly struct SubEventPrefab @@ -224,7 +308,8 @@ namespace Barotrauma } else { - DebugConsole.AddWarning($"{file.Path}: All root EventSets should have an identifier"); + DebugConsole.AddWarning($"{file.Path}: All root EventSets should have an identifier", + file.ContentPackage); } } @@ -264,7 +349,8 @@ namespace Barotrauma string levelTypeStr = element.GetAttributeString("leveltype", parentSet?.LevelType.ToString() ?? "LocationConnection"); if (!Enum.TryParse(levelTypeStr, true, out LevelType)) { - DebugConsole.ThrowError($"Error in event set \"{Identifier}\". \"{levelTypeStr}\" is not a valid level type."); + DebugConsole.ThrowError($"Error in event set \"{Identifier}\". \"{levelTypeStr}\" is not a valid level type.", + contentPackage: element.ContentPackage); } Faction = element.GetAttributeIdentifier(nameof(Faction), Identifier.Empty); @@ -304,7 +390,8 @@ namespace Barotrauma ForceAtVisitedNr = element.GetAttributeInt(nameof(ForceAtVisitedNr), -1); if (ForceAtDiscoveredNr >= 0 && ForceAtVisitedNr >= 0) { - DebugConsole.ThrowError($"Error with event set \"{Identifier}\" - both ForceAtDiscoveredNr and ForceAtVisitedNr are defined, this could lead to unexpected behavior"); + DebugConsole.ThrowError($"Error with event set \"{Identifier}\" - both ForceAtDiscoveredNr and ForceAtVisitedNr are defined, this could lead to unexpected behavior", + contentPackage: element.ContentPackage); } DefaultCommonness = element.GetAttributeFloat("commonness", 1.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs index a208a7e50..53ead40a1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs @@ -123,7 +123,8 @@ namespace Barotrauma var itemsToDestroy = Item.ItemList.FindAll(it => it.Submarine?.Info.Type != SubmarineType.Player && it.HasTag(itemTag)); if (!itemsToDestroy.Any()) { - DebugConsole.ThrowError($"Error in mission \"{Prefab.Identifier}\". Could not find an item with the tag \"{itemTag}\"."); + DebugConsole.ThrowError($"Error in mission \"{Prefab.Identifier}\". Could not find an item with the tag \"{itemTag}\".", + contentPackage: Prefab.ContentPackage); } else { @@ -135,10 +136,11 @@ namespace Barotrauma { foreach (XElement element in itemConfig.Elements()) { - string itemIdentifier = element.GetAttributeString("identifier", ""); - if (!(MapEntityPrefab.Find(null, itemIdentifier) is ItemPrefab itemPrefab)) + Identifier itemIdentifier = element.GetAttributeIdentifier("identifier", Identifier.Empty); + if (MapEntityPrefab.FindByIdentifier(itemIdentifier) is not ItemPrefab itemPrefab) { - DebugConsole.ThrowError("Couldn't spawn item for outpost destroy mission: item prefab \"" + itemIdentifier + "\" not found"); + DebugConsole.ThrowError("Couldn't spawn item for outpost destroy mission: item prefab \"" + itemIdentifier + "\" not found", + contentPackage: Prefab.ContentPackage); continue; } @@ -189,7 +191,8 @@ namespace Barotrauma HumanPrefab humanPrefab = GetHumanPrefabFromElement(element); if (humanPrefab == null) { - DebugConsole.ThrowError($"Couldn't spawn a human character for abandoned outpost mission: human prefab \"{element.GetAttributeString("identifier", string.Empty)}\" not found"); + DebugConsole.ThrowError($"Couldn't spawn a human character for abandoned outpost mission: human prefab \"{element.GetAttributeString("identifier", string.Empty)}\" not found", + contentPackage: Prefab.ContentPackage); continue; } for (int i = 0; i < count; i++) @@ -203,7 +206,8 @@ namespace Barotrauma var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName); if (characterPrefab == null) { - DebugConsole.ThrowError($"Couldn't spawn a character for abandoned outpost mission: character prefab \"{speciesName}\" not found"); + DebugConsole.ThrowError($"Couldn't spawn a character for abandoned outpost mission: character prefab \"{speciesName}\" not found", + contentPackage: Prefab.ContentPackage); continue; } for (int i = 0; i < count; i++) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs index 1a5a8cb3a..d0c52b245 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs @@ -51,12 +51,14 @@ namespace Barotrauma TargetRuin = Level.Loaded?.Ruins?.GetRandom(randSync: Rand.RandSync.ServerAndClient); if (TargetRuin == null) { - DebugConsole.ThrowError($"Failed to initialize an Alien Ruin mission (\"{Prefab.Identifier}\"): level contains no alien ruins"); + DebugConsole.ThrowError($"Failed to initialize an Alien Ruin mission (\"{Prefab.Identifier}\"): level contains no alien ruins", + 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 Alien Ruin mission (\"{Prefab.Identifier}\"): no target identifiers set in the mission definition", + contentPackage: Prefab.ContentPackage); return; } foreach (var item in Item.ItemList) @@ -88,12 +90,14 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"Error in an Alien Ruin mission (\"{Prefab.Identifier}\"): could not find a character prefab with the species \"{identifier}\""); + DebugConsole.ThrowError($"Error in an Alien Ruin mission (\"{Prefab.Identifier}\"): could not find a character prefab with the species \"{identifier}\"", + contentPackage: Prefab.ContentPackage); } } if (enemyPrefabs.None()) { - DebugConsole.ThrowError($"Error in an Alien Ruin mission (\"{Prefab.Identifier}\"): no enemy species defined that could be used to spawn more ({minEnemyCount - existingEnemyCount}) enemies"); + DebugConsole.ThrowError($"Error in an Alien Ruin mission (\"{Prefab.Identifier}\"): no enemy species defined that could be used to spawn more ({minEnemyCount - existingEnemyCount}) enemies", + contentPackage: Prefab.ContentPackage); return; } for (int i = 0; i < (minEnemyCount - existingEnemyCount); i++) @@ -102,7 +106,8 @@ namespace Barotrauma var spawnPos = TargetRuin.Submarine.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"); + 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", + contentPackage: Prefab.ContentPackage); return; } var newEnemy = Character.Create(prefab.Identifier, spawnPos.Value, ToolBox.RandomSeed(8), createNetworkEvent: false); @@ -151,7 +156,8 @@ namespace Barotrauma #if DEBUG else { - DebugConsole.ThrowError($"Error in Alien Ruin mission (\"{Prefab.Identifier}\"): unexpected target of type {target?.GetType()?.ToString()}"); + DebugConsole.ThrowError($"Error in Alien Ruin mission (\"{Prefab.Identifier}\"): unexpected target of type {target?.GetType()?.ToString()}", + contentPackage: Prefab.ContentPackage); } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs index 224dc40f1..3f22e082b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs @@ -46,6 +46,7 @@ namespace Barotrauma } sonarLabel = TextManager.Get("beaconstationsonarlabel"); + DebugConsole.NewMessage("Initialized beacon mission: " + prefab.Identifier, Color.LightSkyBlue, debugOnly: true); } private void LoadMonsters(XElement monsterElement, MonsterSet set) @@ -65,7 +66,8 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"Error in beacon mission \"{Prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\"."); + DebugConsole.ThrowError($"Error in beacon mission \"{Prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\".", + contentPackage: Prefab.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs index 71dcf88a7..2d40a2fc5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs @@ -238,7 +238,8 @@ namespace Barotrauma if (itemConfig == null) { - DebugConsole.ThrowError("Failed to initialize items for cargo mission (itemConfig == null)"); + DebugConsole.ThrowError("Failed to initialize items for cargo mission (itemConfig == null)", + contentPackage: Prefab.ContentPackage); return; } @@ -262,7 +263,7 @@ namespace Barotrauma SpawnedInCurrentOutpost = true, AllowStealing = false }; - item.AddTag("cargomission"); + item.AddTag(Tags.CargoMissionItem); item.AddTag(Prefab.Identifier); foreach (var tag in Prefab.Tags) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs index 296db0f2a..f4c2a781c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs @@ -57,7 +57,7 @@ namespace Barotrauma { for (int n = 0; n < 2; n++) { - descriptions[i] = descriptions[i].Replace("[location" + (n + 1) + "]", locations[n].Name); + descriptions[i] = descriptions[i].Replace("[location" + (n + 1) + "]", locations[n].DisplayName); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs index 8eaba7820..1ffc09b93 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs @@ -110,12 +110,14 @@ namespace Barotrauma bossPrefab = CharacterPrefab.FindBySpeciesName(speciesName); if (bossPrefab == null) { - DebugConsole.ThrowError($"Error in end mission \"{prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\"."); + DebugConsole.ThrowError($"Error in end mission \"{prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\".", + contentPackage: Prefab.ContentPackage); } } else { - DebugConsole.ThrowError($"Error in end mission \"{prefab.Identifier}\". Monster file not set."); + DebugConsole.ThrowError($"Error in end mission \"{prefab.Identifier}\". Monster file not set.", + contentPackage: Prefab.ContentPackage); } Identifier minionName = prefab.ConfigElement.GetAttributeIdentifier("minionfile", Identifier.Empty); @@ -124,7 +126,8 @@ namespace Barotrauma minionPrefab = CharacterPrefab.FindBySpeciesName(minionName); if (minionPrefab == null) { - DebugConsole.ThrowError($"Error in end mission \"{prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\"."); + DebugConsole.ThrowError($"Error in end mission \"{prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\".", + contentPackage: Prefab.ContentPackage); } } @@ -137,7 +140,8 @@ namespace Barotrauma projectilePrefab = MapEntityPrefab.FindByIdentifier(projectileId) as ItemPrefab; if (projectilePrefab == null) { - DebugConsole.ThrowError($"Error in end mission \"{prefab.Identifier}\". Could not find an item prefab with the name \"{projectileId}\"."); + DebugConsole.ThrowError($"Error in end mission \"{prefab.Identifier}\". Could not find an item prefab with the name \"{projectileId}\".", + contentPackage: Prefab.ContentPackage); } } @@ -152,7 +156,8 @@ namespace Barotrauma bossSpawnPoint = WayPoint.WayPointList.FirstOrDefault(wp => wp.Tags.Contains(spawnPointTag)); if (bossSpawnPoint == null) { - DebugConsole.ThrowError($"Error in end mission \"{Prefab.Identifier}\". Could not find a spawn point \"{spawnPointTag}\"."); + DebugConsole.ThrowError($"Error in end mission \"{Prefab.Identifier}\". Could not find a spawn point \"{spawnPointTag}\".", + contentPackage: Prefab.ContentPackage); return; } if (!IsClient) @@ -171,14 +176,16 @@ namespace Barotrauma } if (destructibleItemTag.IsEmpty) { - DebugConsole.ThrowError($"Error in end mission \"{Prefab.Identifier}\". Destructible item tag not set."); + DebugConsole.ThrowError($"Error in end mission \"{Prefab.Identifier}\". Destructible item tag not set.", + contentPackage: Prefab.ContentPackage); return; } destructibleItems.Clear(); destructibleItems.AddRange(Item.ItemList.FindAll(it => it.HasTag(destructibleItemTag))); if (destructibleItems.None()) { - DebugConsole.ThrowError($"Error in end mission \"{Prefab.Identifier}\". Could not find any destructible items with the tag \"{spawnPointTag}\"."); + DebugConsole.ThrowError($"Error in end mission \"{Prefab.Identifier}\". Could not find any destructible items with the tag \"{spawnPointTag}\".", + contentPackage: Prefab.ContentPackage); return; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs index 10fa64cb8..9c900601f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs @@ -77,7 +77,8 @@ namespace Barotrauma { if (inMission) { - DebugConsole.ThrowError("MainSub was null when trying to retrieve submarine size for determining escorted character count!"); + DebugConsole.ThrowError("MainSub was null when trying to retrieve submarine size for determining escorted character count!", + contentPackage: Prefab.ContentPackage); } return 1; } @@ -117,7 +118,10 @@ namespace Barotrauma { characterStatusEffects[humanPrefab] = new List { newEffect }; } - characterStatusEffects[humanPrefab].Add(newEffect); + else + { + characterStatusEffects[humanPrefab].Add(newEffect); + } } } } @@ -180,7 +184,8 @@ namespace Barotrauma if (scalingCharacterCount * characterConfig.Elements().Count() != characters.Count) { - DebugConsole.AddWarning("Character count did not match expected character count in InitCharacters of EscortMission"); + DebugConsole.AddWarning("Character count did not match expected character count in InitCharacters of EscortMission", + Prefab.ContentPackage); return; } int i = 0; @@ -220,7 +225,8 @@ namespace Barotrauma if (characterConfig == null) { - DebugConsole.ThrowError("Failed to initialize characters for escort mission (characterConfig == null)"); + DebugConsole.ThrowError("Failed to initialize characters for escort mission (characterConfig == null)", + contentPackage: Prefab.ContentPackage); return; } @@ -258,7 +264,7 @@ namespace Barotrauma { character.Speak(TextManager.Get("dialogterroristannounce").Value, null, Rand.Range(0.5f, 3f)); } - XElement randomElement = itemConfig.Elements().GetRandomUnsynced(e => e.GetAttributeFloat(0f, "mindifficulty") <= Level.Loaded.Difficulty); + ContentXElement randomElement = itemConfig.Elements().GetRandomUnsynced(e => e.GetAttributeFloat(0f, "mindifficulty") <= Level.Loaded.Difficulty); if (randomElement != null) { HumanPrefab.InitializeItem(character, randomElement, character.Submarine, humanPrefab: null, createNetworkEvents: true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs index fbbdc98c1..561fed7d5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs @@ -119,14 +119,16 @@ namespace Barotrauma { if (MapEntityPrefab.FindByIdentifier(identifier) is not ItemPrefab prefab) { - DebugConsole.ThrowError($"Error in MineralMission: couldn't find an item prefab (identifier: \"{identifier}\")"); + DebugConsole.ThrowError($"Error in MineralMission: couldn't find an item prefab (identifier: \"{identifier}\")", + contentPackage: Prefab.ContentPackage); continue; } var spawnedResources = level.GenerateMissionResources(prefab, amount, positionType, caves); if (spawnedResources.Count < amount) { - DebugConsole.ThrowError($"Error in MineralMission: spawned only {spawnedResources.Count}/{amount} of {prefab.Name}"); + DebugConsole.ThrowError($"Error in MineralMission: spawned only {spawnedResources.Count}/{amount} of {prefab.Name}", + contentPackage: Prefab.ContentPackage); } if (spawnedResources.None()) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index b2976762c..be2130aac 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -185,7 +185,7 @@ namespace Barotrauma for (int n = 0; n < 2; n++) { - string locationName = $"‖color:gui.orange‖{locations[n].Name}‖end‖"; + string locationName = $"‖color:gui.orange‖{locations[n].DisplayName}‖end‖"; if (description != null) { description = description.Replace("[location" + (n + 1) + "]", locationName); } if (successMessage != null) { successMessage = successMessage.Replace("[location" + (n + 1) + "]", locationName); } if (failureMessage != null) { failureMessage = failureMessage.Replace("[location" + (n + 1) + "]", locationName); } @@ -347,7 +347,8 @@ namespace Barotrauma var eventPrefab = EventSet.GetAllEventPrefabs().Find(p => p.Identifier == trigger.EventIdentifier); if (eventPrefab == null) { - DebugConsole.ThrowError($"Mission \"{Name}\" failed to trigger an event (couldn't find an event with the identifier \"{trigger.EventIdentifier}\")."); + DebugConsole.ThrowError($"Mission \"{Name}\" failed to trigger an event (couldn't find an event with the identifier \"{trigger.EventIdentifier}\").", + contentPackage: Prefab.ContentPackage); return; } if (GameMain.GameSession?.EventManager != null) @@ -378,7 +379,7 @@ namespace Barotrauma catch (Exception e) { string errorMsg = "Unknown error while giving mission rewards."; - DebugConsole.ThrowError(errorMsg, e); + DebugConsole.ThrowError(errorMsg, e, contentPackage: Prefab.ContentPackage); GameAnalyticsManager.AddErrorEventOnce("Mission.End:GiveReward", GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + e.StackTrace); #if SERVER GameMain.Server?.SendChatMessage(errorMsg + "\n" + e.StackTrace, Networking.ChatMessageType.Error); @@ -430,8 +431,7 @@ namespace Barotrauma IEnumerable crewCharacters = GameSession.GetSessionCrewCharacters(CharacterType.Both); // use multipliers here so that we can easily add them together without introducing multiplicative XP stacking - var experienceGainMultiplier = new AbilityMissionExperienceGainMultiplier(this, 1f); - crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnAllyGainMissionExperience, experienceGainMultiplier)); + var experienceGainMultiplier = new AbilityMissionExperienceGainMultiplier(this, 1f, character: null); crewCharacters.ForEach(c => experienceGainMultiplier.Value += c.GetStatValue(StatTypes.MissionExperienceGainMultiplier)); DistributeExperienceToCrew(crewCharacters, (int)(baseExperienceGain * experienceGainMultiplier.Value)); @@ -547,7 +547,8 @@ namespace Barotrauma { if (element.Attribute("name") != null) { - DebugConsole.ThrowError("Error in mission \"" + Name + "\" - use character identifiers instead of names to configure the characters."); + DebugConsole.ThrowError($"Error in mission \"{Name}\" - use character identifiers instead of names to configure the characters.", + contentPackage: Prefab.ContentPackage); return null; } @@ -556,7 +557,8 @@ namespace Barotrauma HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier); if (humanPrefab == null) { - DebugConsole.ThrowError($"Couldn't spawn character for mission: character prefab \"{characterIdentifier}\" not found in the NPC set \"{characterFrom}\"."); + DebugConsole.ThrowError($"Couldn't spawn character for mission: character prefab \"{characterIdentifier}\" not found in the NPC set \"{characterFrom}\".", + contentPackage: Prefab.ContentPackage); return null; } @@ -587,12 +589,14 @@ namespace Barotrauma ItemPrefab itemPrefab; if (element.Attribute("name") != null) { - DebugConsole.ThrowError($"Error in mission \"{Name}\" - use item identifiers instead of names to configure the items"); + DebugConsole.ThrowError($"Error in mission \"{Name}\" - use item identifiers instead of names to configure the items", + contentPackage: Prefab.ContentPackage); string itemName = element.GetAttributeString("name", ""); itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab; if (itemPrefab == null) { - DebugConsole.ThrowError($"Couldn't spawn item for mission \"{Name}\": item prefab \"{itemName}\" not found"); + DebugConsole.ThrowError($"Couldn't spawn item for mission \"{Name}\": item prefab \"{itemName}\" not found", + contentPackage: Prefab.ContentPackage); } } else @@ -601,7 +605,8 @@ namespace Barotrauma itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab; if (itemPrefab == null) { - DebugConsole.ThrowError($"Couldn't spawn item for mission \"{Name}\": item prefab \"{itemIdentifier}\" not found"); + DebugConsole.ThrowError($"Couldn't spawn item for mission \"{Name}\": item prefab \"{itemIdentifier}\" not found", + contentPackage: Prefab.ContentPackage); } } return itemPrefab; @@ -614,14 +619,16 @@ namespace Barotrauma WayPoint cargoSpawnPos = WayPoint.GetRandom(SpawnType.Cargo, null, Submarine.MainSub, useSyncedRand: true); if (cargoSpawnPos == null) { - DebugConsole.ThrowError($"Couldn't spawn items for mission \"{Name}\": no waypoints marked as Cargo were found"); + DebugConsole.ThrowError($"Couldn't spawn items for mission \"{Name}\": no waypoints marked as Cargo were found", + contentPackage: Prefab.ContentPackage); return null; } var cargoRoom = cargoSpawnPos.CurrentHull; if (cargoRoom == null) { - DebugConsole.ThrowError($"Couldn't spawn items for mission \"{Name}\": waypoints marked as Cargo must be placed inside a room"); + DebugConsole.ThrowError($"Couldn't spawn items for mission \"{Name}\": waypoints marked as Cargo must be placed inside a room", + contentPackage: Prefab.ContentPackage); return null; } @@ -644,16 +651,18 @@ namespace Barotrauma public Mission Mission { get; set; } } - class AbilityMissionExperienceGainMultiplier : AbilityObject, IAbilityValue, IAbilityMission + class AbilityMissionExperienceGainMultiplier : AbilityObject, IAbilityValue, IAbilityMission, IAbilityCharacter { - public AbilityMissionExperienceGainMultiplier(Mission mission, float missionExperienceGainMultiplier) + public AbilityMissionExperienceGainMultiplier(Mission mission, float missionExperienceGainMultiplier, Character character) { Value = missionExperienceGainMultiplier; Mission = mission; + Character = character; } public float Value { get; set; } public Mission Mission { get; set; } + public Character Character { get; set; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs index ef4c76774..4df37a741 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -94,8 +94,19 @@ namespace Barotrauma DataRewards = new List<(Identifier Identifier, object Value, SetDataAction.OperationType OperationType)>(); public readonly int Commonness; + /// + /// Displayed difficulty (indicator) + /// public readonly int? Difficulty; public const int MinDifficulty = 1, MaxDifficulty = 4; + /// + /// The actual minimum difficulty of the level allowed for this mission to trigger. + /// + public readonly int MinLevelDifficulty = 0; + /// + /// The actual maximum difficulty of the level allowed for this mission to trigger. + /// + public readonly int MaxLevelDifficulty = 100; public readonly int Reward; @@ -211,6 +222,10 @@ namespace Barotrauma int difficulty = element.GetAttributeInt("difficulty", MinDifficulty); Difficulty = Math.Clamp(difficulty, MinDifficulty, MaxDifficulty); } + MinLevelDifficulty = element.GetAttributeInt(nameof(MinLevelDifficulty), MinLevelDifficulty); + MaxLevelDifficulty = element.GetAttributeInt(nameof(MaxLevelDifficulty), MaxLevelDifficulty); + MinLevelDifficulty = Math.Clamp(MinLevelDifficulty, 0, Math.Min(MaxLevelDifficulty, 100)); + MaxLevelDifficulty = Math.Clamp(MaxLevelDifficulty, Math.Max(MinLevelDifficulty, 0), 100); ShowProgressBar = element.GetAttributeBool(nameof(ShowProgressBar), false); ShowProgressInNumbers = element.GetAttributeBool(nameof(ShowProgressInNumbers), false); @@ -372,7 +387,8 @@ namespace Barotrauma } if (constructor == null) { - DebugConsole.ThrowError($"Failed to find a constructor for the mission type \"{Type}\"!"); + DebugConsole.ThrowError($"Failed to find a constructor for the mission type \"{Type}\"!", + contentPackage: element.ContentPackage); } InitProjSpecific(element); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs index 8b3d03131..4b3e1a2b8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs @@ -48,7 +48,8 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"Error in monster mission \"{prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\"."); + DebugConsole.ThrowError($"Error in monster mission \"{prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\".", + contentPackage: prefab.ContentPackage); } } @@ -78,7 +79,8 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"Error in monster mission \"{prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\"."); + DebugConsole.ThrowError($"Error in monster mission \"{prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\".", + contentPackage: prefab.ContentPackage); } } @@ -118,19 +120,19 @@ namespace Barotrauma float minDistBetweenMonsterMissions = 10000; float mindDistFromSub = Level.Loaded.Size.X * 0.3f; var monsterMissions = GameMain.GameSession.Missions.Select(e => e as MonsterMission).Where(m => m != null && m != this && m.spawnPos.HasValue); - if (!Level.Loaded.TryGetInterestingPosition(useSyncedRand: true, spawnPosType, mindDistFromSub, out Vector2 spawnPos, + if (!Level.Loaded.TryGetInterestingPosition(useSyncedRand: true, spawnPosType, mindDistFromSub, out Level.InterestingPosition spawnPos, filter: p => monsterMissions.None(m => Vector2.DistanceSquared(p.Position.ToVector2(), m.spawnPos.Value) < minDistBetweenMonsterMissions * minDistBetweenMonsterMissions), suppressWarning: true)) { Level.Loaded.TryGetInterestingPosition(useSyncedRand: true, spawnPosType, mindDistFromSub, out spawnPos); } - this.spawnPos = spawnPos; + this.spawnPos = spawnPos.Position.ToVector2(); foreach (var (character, amountRange) in monsterPrefabs) { int amount = Rand.Range(amountRange.X, amountRange.Y + 1); for (int i = 0; i < amount; i++) { - monsters.Add(Character.Create(character.Identifier, spawnPos, ToolBox.RandomSeed(8), createNetworkEvent: false)); + monsters.Add(Character.Create(character.Identifier, this.spawnPos.Value, ToolBox.RandomSeed(8), createNetworkEvent: false)); } } InitializeMonsters(monsters); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs index 78f840cf9..1c766a7fc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs @@ -85,7 +85,8 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"Error in monster mission \"{prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\"."); + DebugConsole.ThrowError($"Error in monster mission \"{prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\".", + contentPackage: Prefab.ContentPackage); } } @@ -174,7 +175,8 @@ namespace Barotrauma var itemIdentifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty); if (MapEntityPrefab.FindByIdentifier(itemIdentifier) is not ItemPrefab itemPrefab) { - DebugConsole.ThrowError("Couldn't spawn item for nest mission: item prefab \"" + itemIdentifier + "\" not found"); + DebugConsole.ThrowError("Couldn't spawn item for nest mission: item prefab \"" + itemIdentifier + "\" not found", + contentPackage: Prefab.ContentPackage); continue; } @@ -285,7 +287,8 @@ namespace Barotrauma } if (Level.Loaded.IsPositionInsideWall(nestPosition)) { - DebugConsole.AddWarning($"Error in nest mission \"{Prefab.Identifier}\": nest position was inside a wall ({nestPosition})."); + DebugConsole.AddWarning($"Error in nest mission \"{Prefab.Identifier}\": nest position was inside a wall ({nestPosition}).", + Prefab.ContentPackage); } monsterPrefabs.Clear(); break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs index 80392f0ef..e87e70696 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs @@ -11,9 +11,9 @@ namespace Barotrauma { partial class PirateMission : Mission { - private readonly XElement submarineTypeConfig; - private readonly XElement characterConfig; - private readonly XElement characterTypeConfig; + private readonly ContentXElement submarineTypeConfig; + private readonly ContentXElement characterConfig; + private readonly ContentXElement characterTypeConfig; private readonly float addedMissionDifficultyPerPlayer; private float missionDifficulty; @@ -103,7 +103,8 @@ namespace Barotrauma var characterTypeElement = characterTypeConfig.Elements().FirstOrDefault(e => e.GetAttributeString("typeidentifier", string.Empty) == characterId); if (characterTypeElement == null) { - DebugConsole.ThrowError($"Error in mission \"{prefab.Identifier}\". Could not find a character type element for the character \"{characterId}\"."); + DebugConsole.ThrowError($"Error in mission \"{prefab.Identifier}\". Could not find a character type element for the character \"{characterId}\".", + contentPackage: Prefab.ContentPackage); } } //make sure all defined character types can be found from human prefabs @@ -116,7 +117,8 @@ namespace Barotrauma HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier); if (humanPrefab == null) { - DebugConsole.ThrowError($"Error in mission \"{prefab.Identifier}\". Character prefab \"{characterIdentifier}\" not found in the NPC set \"{characterFrom}\"."); + DebugConsole.ThrowError($"Error in mission \"{prefab.Identifier}\". Character prefab \"{characterIdentifier}\" not found in the NPC set \"{characterFrom}\".", + contentPackage: Prefab.ContentPackage); } } } @@ -151,7 +153,8 @@ namespace Barotrauma ContentPath submarinePath = submarineConfig.GetAttributeContentPath("path", Prefab.ContentPackage); if (submarinePath.IsNullOrEmpty()) { - DebugConsole.ThrowError($"No path used for submarine for the pirate mission \"{Prefab.Identifier}\"!"); + DebugConsole.ThrowError($"No path used for submarine for the pirate mission \"{Prefab.Identifier}\"!", + contentPackage: Prefab.ContentPackage); return; } @@ -165,7 +168,8 @@ namespace Barotrauma if (contentFile == null) { - DebugConsole.ThrowError($"No submarine file found from the path {submarinePath}!"); + DebugConsole.ThrowError($"No submarine file found from the path {submarinePath}!", + contentPackage: Prefab.ContentPackage); return; } @@ -201,16 +205,28 @@ namespace Barotrauma private void CreateMissionPositions(out Vector2 preferredSpawnPos) { - Vector2 patrolPos = enemySub.WorldPosition; + Vector2 patrolPos = Level.Loaded.EndPosition; Point subSize = enemySub.GetDockedBorders().Size; - if (!Level.Loaded.TryGetInterestingPosition(true, Level.PositionType.MainPath, Level.Loaded.Size.X * 0.3f, out preferredSpawnPos)) + preferredSpawnPos = Level.Loaded.EndPosition; + + if (Level.Loaded.TryGetInterestingPosition(true, Level.PositionType.MainPath, Level.Loaded.Size.X * 0.3f, out var potentialSpawnPos)) { - DebugConsole.ThrowError("Could not spawn pirate submarine in an interesting location! " + this); + preferredSpawnPos = potentialSpawnPos.Position.ToVector2(); } - if (!Level.Loaded.TryGetInterestingPositionAwayFromPoint(true, Level.PositionType.MainPath, Level.Loaded.Size.X * 0.3f, out patrolPos, preferredSpawnPos, minDistFromPoint: 10000f)) + else { - DebugConsole.ThrowError("Could not give pirate submarine an interesting location to patrol to! " + this); + DebugConsole.ThrowError("Could not spawn pirate submarine in an interesting location! " + this, + contentPackage: Prefab.ContentPackage); + } + if (Level.Loaded.TryGetInterestingPositionAwayFromPoint(true, Level.PositionType.MainPath, Level.Loaded.Size.X * 0.3f, out var potentialPatrolPos, preferredSpawnPos, minDistFromPoint: 10000f)) + { + patrolPos = potentialPatrolPos.Position.ToVector2(); + } + else + { + DebugConsole.ThrowError("Could not give pirate submarine an interesting location to patrol to! " + this, + contentPackage: Prefab.ContentPackage); } patrolPos = enemySub.FindSpawnPos(patrolPos, subSize); @@ -266,7 +282,8 @@ namespace Barotrauma if (characterConfig == null) { - DebugConsole.ThrowError("Failed to initialize characters for escort mission (characterConfig == null)"); + DebugConsole.ThrowError("Failed to initialize characters for escort mission (characterConfig == null)", + contentPackage: Prefab.ContentPackage); return; } @@ -281,7 +298,7 @@ namespace Barotrauma Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed)); bool commanderAssigned = false; - foreach (XElement element in characterConfig.Elements()) + foreach (ContentXElement element in characterConfig.Elements()) { // it is possible to get more than the "max" amount of characters if the modified difficulty is high enough; this is intentional // if necessary, another "hard max" value could be used to clamp the value for performance/gameplay concerns @@ -293,7 +310,8 @@ namespace Barotrauma if (characterType == null) { - DebugConsole.ThrowError($"No character types defined in CharacterTypes for a declared type identifier in mission \"{Prefab.Identifier}\"."); + DebugConsole.ThrowError($"No character types defined in CharacterTypes for a declared type identifier in mission \"{Prefab.Identifier}\".", + contentPackage: element.ContentPackage); return; } @@ -356,12 +374,12 @@ namespace Barotrauma { DebugConsole.ThrowError(submarineInfo == null ? $"Error in PirateMission: enemy sub was not created (submarineInfo == null)." : - $"Error in PirateMission: enemy sub was not created."); + $"Error in PirateMission: enemy sub was not created.", + contentPackage: Prefab.ContentPackage); return; } - Vector2 spawnPos = Level.Loaded.EndPosition; // in case TryGetInterestingPosition fails, though this should not happen - CreateMissionPositions(out spawnPos); // patrol positions are not explicitly replicated, instead they are acquired the same way the server acquires them + CreateMissionPositions(out Vector2 spawnPos); // patrol positions are not explicitly replicated, instead they are acquired the same way the server acquires them #if DEBUG if (IsClient) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index 7c54af4ab..c8184730c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -106,12 +106,14 @@ namespace Barotrauma if (element.GetAttribute("itemname") != null) { - DebugConsole.ThrowError("Error in SalvageMission - use item identifier instead of the name of the item."); + DebugConsole.ThrowError("Error in SalvageMission - use item identifier instead of the name of the item.", + contentPackage: element.ContentPackage); string itemName = element.GetAttributeString("itemname", ""); ItemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab; if (ItemPrefab == null && ExistingItemTag.IsEmpty) { - DebugConsole.ThrowError($"Error in SalvageMission: couldn't find an item prefab with the name \"{itemName}\""); + DebugConsole.ThrowError($"Error in SalvageMission: couldn't find an item prefab with the name \"{itemName}\"", + contentPackage: element.ContentPackage); } } else @@ -128,7 +130,8 @@ namespace Barotrauma } if (ItemPrefab == null && ExistingItemTag.IsEmpty) { - DebugConsole.ThrowError($"Error in SalvageMission - couldn't find an item prefab with the identifier \"{itemIdentifier}\""); + DebugConsole.ThrowError($"Error in SalvageMission - couldn't find an item prefab with the identifier \"{itemIdentifier}\"", + contentPackage: element.ContentPackage); } } @@ -286,7 +289,8 @@ namespace Barotrauma { if (target.ItemPrefab == null && target.ContainerTag.IsEmpty) { - DebugConsole.ThrowError($"Failed to find a target item for the mission \"{Prefab.Identifier}\". Item tag: {target.ExistingItemTag}"); + DebugConsole.ThrowError($"Failed to find a target item for the mission \"{Prefab.Identifier}\". Item tag: {target.ExistingItemTag}", + contentPackage: Prefab.ContentPackage); continue; } target.Item = new Item(target.ItemPrefab, position, null); @@ -379,7 +383,8 @@ namespace Barotrauma if (target.Item == null) { #if DEBUG - DebugConsole.ThrowError("Error in salvage mission " + Prefab.Identifier + " (item was null)"); + DebugConsole.ThrowError("Error in salvage mission " + Prefab.Identifier + " (item was null)", + contentPackage: Prefab.ContentPackage); #endif return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs index 364675efb..8ee06a07d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs @@ -64,7 +64,8 @@ namespace Barotrauma if (itemConfig == null) { - DebugConsole.ThrowError("Failed to initialize a Scan mission: item config is not set"); + DebugConsole.ThrowError("Failed to initialize a Scan mission: item config is not set", + contentPackage: Prefab.ContentPackage); return; } @@ -77,7 +78,8 @@ namespace Barotrauma TargetRuin = Level.Loaded?.Ruins?.GetRandom(randSync: Rand.RandSync.ServerAndClient); if (TargetRuin == null) { - DebugConsole.ThrowError("Failed to initialize a Scan mission: level contains no alien ruins"); + DebugConsole.ThrowError("Failed to initialize a Scan mission: level contains no alien ruins", + contentPackage: Prefab.ContentPackage); return; } @@ -85,7 +87,8 @@ namespace Barotrauma ruinWaypoints.RemoveAll(wp => wp.CurrentHull == null); if (ruinWaypoints.Count < targetsToScan) { - DebugConsole.ThrowError($"Failed to initialize a Scan mission: target ruin has less waypoints than required as scan targets ({ruinWaypoints.Count} < {targetsToScan})"); + DebugConsole.ThrowError($"Failed to initialize a Scan mission: target ruin has less waypoints than required as scan targets ({ruinWaypoints.Count} < {targetsToScan})", + contentPackage: Prefab.ContentPackage); return; } var availableWaypoints = new List(); @@ -107,7 +110,8 @@ namespace Barotrauma if (availableWaypoints.None()) { #if DEBUG - DebugConsole.ThrowError($"Error initializing a Scan mission: not enough targets available on try #{tries + 1} to reach the required scan target count (current targets: {scanTargets.Count}, required targets: {targetsToScan})"); + DebugConsole.ThrowError($"Error initializing a Scan mission: not enough targets available on try #{tries + 1} to reach the required scan target count (current targets: {scanTargets.Count}, required targets: {targetsToScan})", + contentPackage: Prefab.ContentPackage); #endif break; } @@ -131,7 +135,8 @@ namespace Barotrauma } if (scanTargets.Count < targetsToScan) { - DebugConsole.ThrowError($"Error initializing a Scan mission: not enough targets (current targets: {scanTargets.Count}, required targets: {targetsToScan})"); + DebugConsole.ThrowError($"Error initializing a Scan mission: not enough targets (current targets: {scanTargets.Count}, required targets: {targetsToScan})", + contentPackage: Prefab.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index 71eebca71..187c8f919 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -10,14 +10,45 @@ namespace Barotrauma { class MonsterEvent : Event { + /// + /// The name of the species to spawn + /// public readonly Identifier SpeciesName; - public readonly int MinAmount, MaxAmount; + + /// + /// Minimum amount of monsters to spawn. You can also use "Amount" if you want to spawn a fixed number of monsters. + /// + public readonly int MinAmount; + /// + /// Maximum amount of monsters to spawn. You can also use "Amount" if you want to spawn a fixed number of monsters. + /// + public readonly int MaxAmount; + private readonly List monsters = new List(); + /// + /// The monsters are spawned at least this distance away from the players and submarines. + /// public readonly float SpawnDistance; + + /// + /// Amount of random variance in the spawn position, in pixels. Can be used to prevent all the monsters from spawning at the exact same position. + /// private readonly float scatter; + + /// + /// Used for offsetting the spawns towards the end position of the level, so that they spawn farther afront the sub than normally. In pixels. + /// private readonly float offset; + + /// + /// Delay between spawning the monsters. Only relevant if the event spawns more than one monster. + /// private readonly float delayBetweenSpawns; + + /// + /// Number seconds before the event resets after all the monsters are dead. Can be used to make the event spawn monsters multiple times. + /// private float resetTime; private float resetTimer; @@ -25,11 +56,22 @@ namespace Barotrauma private bool disallowed; + /// + /// Where should the monster spawn? + /// public readonly Level.PositionType SpawnPosType; + + /// + /// If set, the monsters will spawn at a spawnpoint that has this tag. Only relevant for events that spawn monsters in a submarine, beacon station, wreck, outpost or ruin. + /// private readonly string spawnPointTag; private bool spawnPending, spawnReady; + /// + /// Maximum number of the specific type of monster in the entire level. Can be used to prevent the event from spawning more monsters if there's + /// already enough of that type of monster, e.g. spawned by another event or by a mission. + /// public readonly int MaxAmountPerLevel = int.MaxValue; public IReadOnlyList Monsters => monsters; @@ -82,13 +124,7 @@ namespace Barotrauma MaxAmountPerLevel = prefab.ConfigElement.GetAttributeInt("maxamountperlevel", int.MaxValue); - var spawnPosTypeStr = prefab.ConfigElement.GetAttributeString("spawntype", ""); - if (string.IsNullOrWhiteSpace(spawnPosTypeStr) || - !Enum.TryParse(spawnPosTypeStr, true, out SpawnPosType)) - { - SpawnPosType = Level.PositionType.MainPath; - } - + SpawnPosType = prefab.ConfigElement.GetAttributeEnum("spawntype", Level.PositionType.MainPath); //backwards compatibility if (prefab.ConfigElement.GetAttributeBool("spawndeep", false)) { @@ -127,7 +163,8 @@ namespace Barotrauma var file = CharacterPrefab.FindBySpeciesName(SpeciesName)?.ContentFile; if (file == null) { - DebugConsole.ThrowError($"Failed to find config file for species \"{SpeciesName}\""); + DebugConsole.ThrowError($"Failed to find config file for species \"{SpeciesName}\".", + contentPackage: Prefab.ContentPackage); yield break; } else @@ -159,7 +196,8 @@ namespace Barotrauma Character createdCharacter = Character.Create(SpeciesName, Vector2.Zero, seed, characterInfo: null, isRemotePlayer: false, hasAi: true, createNetworkEvent: true, throwErrorIfNotFound: false); if (createdCharacter == null) { - DebugConsole.AddWarning($"Error in MonsterEvent: failed to spawn the character \"{SpeciesName}\". Content package: \"{prefab.ConfigElement?.ContentPackage?.Name ?? "unknown"}\"."); + DebugConsole.AddWarning($"Error in MonsterEvent: failed to spawn the character \"{SpeciesName}\". Content package: \"{prefab.ConfigElement?.ContentPackage?.Name ?? "unknown"}\".", + Prefab.ContentPackage); disallowed = true; continue; } @@ -355,29 +393,28 @@ namespace Barotrauma { if (offset > 0) { - Vector2 dir; - var waypoints = WayPoint.WayPointList.FindAll(wp => wp.Submarine == null && wp.Ruin == null); - var nearestWaypoint = waypoints.OrderBy(wp => Vector2.DistanceSquared(wp.WorldPosition, spawnPos.Value)).FirstOrDefault(); - if (nearestWaypoint != null) + var tunnelType = chosenPosition.PositionType == Level.PositionType.MainPath ? Level.TunnelType.MainPath : Level.TunnelType.SidePath; + var waypoints = WayPoint.WayPointList.FindAll(wp => + wp.Submarine == null && + wp.Ruin == null && + wp.Tunnel?.Type == tunnelType && + wp.WorldPosition.X > spawnPos.Value.X); + + if (waypoints.None()) { - int currentIndex = waypoints.IndexOf(nearestWaypoint); - var nextWaypoint = waypoints[Math.Min(currentIndex + 20, waypoints.Count - 1)]; - dir = Vector2.Normalize(nextWaypoint.WorldPosition - nearestWaypoint.WorldPosition); - // Ensure that the spawn position is not offset to the left. - if (dir.X < 0) - { - dir.X = 0; - } + DebugConsole.AddWarning($"Failed to find a spawn position offset from {spawnPos.Value}.", + Prefab.ContentPackage); } else { - dir = new Vector2(1, Rand.Range(-1f, 1f)); - } - Vector2 targetPos = spawnPos.Value + dir * offset; - var targetWaypoint = waypoints.OrderBy(wp => Vector2.DistanceSquared(wp.WorldPosition, targetPos)).FirstOrDefault(); - if (targetWaypoint != null) - { - spawnPos = targetWaypoint.WorldPosition; + float offsetSqr = offset * offset; + //find the waypoint whose distance from the spawnPos is closest to the desired offset + var targetWaypoint = waypoints.OrderBy(wp => + Math.Abs(Vector2.DistanceSquared(wp.WorldPosition, spawnPos.Value) - offsetSqr)).FirstOrDefault(); + if (targetWaypoint != null) + { + spawnPos = targetWaypoint.WorldPosition; + } } } // Ensure that the position is not inside a submarine (in practice wrecks). @@ -636,7 +673,7 @@ namespace Barotrauma monster.AnimController.SetPosition(FarseerPhysics.ConvertUnits.ToSimUnits(pos)); var eventManager = GameMain.GameSession.EventManager; - if (eventManager != null) + if (eventManager != null && monster.Params.AI != null) { if (SpawnPosType.HasFlag(Level.PositionType.MainPath) || SpawnPosType.HasFlag(Level.PositionType.SidePath)) { @@ -663,7 +700,7 @@ namespace Barotrauma //this will do nothing if the monsters have no swarm behavior defined, //otherwise it'll make the spawned characters act as a swarm SwarmBehavior.CreateSwarm(monsters.Cast()); - DebugConsole.NewMessage($"Spawned: {ToString()}. Strength: {StringFormatter.FormatZeroDecimal(monsters.Sum(m => m.Params.AI.CombatStrength))}.", Color.LightBlue, debugOnly: true); + DebugConsole.NewMessage($"Spawned: {ToString()}. Strength: {StringFormatter.FormatZeroDecimal(monsters.Sum(m => m.Params.AI?.CombatStrength ?? 0))}.", Color.LightBlue, debugOnly: true); } if (GameMain.GameSession != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs index 85c735b0d..517875dd4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs @@ -7,7 +7,21 @@ namespace Barotrauma { class ScriptedEvent : Event { - private readonly Dictionary>> targetPredicates = new Dictionary>>(); + public sealed record TargetPredicate( + TargetPredicate.EntityType Type, + Predicate Predicate) + { + public enum EntityType + { + Character, + Hull, + Item, + Structure, + Submarine + } + } + + private readonly Dictionary> targetPredicates = new Dictionary>(); private readonly Dictionary> cachedTargets = new Dictionary>(); @@ -17,7 +31,7 @@ namespace Barotrauma /// private readonly Dictionary initialAmounts = new Dictionary(); - private int prevEntityCount; + private bool newEntitySpawned; private int prevPlayerCount, prevBotCount; private Character prevControlled; @@ -50,7 +64,8 @@ namespace Barotrauma } if (elementId == "statuseffect") { - DebugConsole.ThrowError($"Error in event prefab \"{prefab.Identifier}\". Status effect configured as an action. Please configure status effects as child elements of a StatusEffectAction."); + DebugConsole.ThrowError($"Error in event prefab \"{prefab.Identifier}\". Status effect configured as an action. Please configure status effects as child elements of a StatusEffectAction.", + contentPackage: prefab.ContentPackage); continue; } var action = EventAction.Instantiate(this, element); @@ -59,7 +74,8 @@ namespace Barotrauma if (!Actions.Any()) { - DebugConsole.ThrowError($"Scripted event \"{prefab.Identifier}\" has no actions. The event will do nothing."); + DebugConsole.ThrowError($"Scripted event \"{prefab.Identifier}\" has no actions. The event will do nothing.", + contentPackage: prefab.ContentPackage); } requiredDestinationTypes = prefab.ConfigElement.GetAttributeStringArray("requireddestinationtypes", null); @@ -69,8 +85,9 @@ namespace Barotrauma foreach (var gotoAction in allActions.OfType()) { if (allActions.None(a => a is Label label && label.Name == gotoAction.Name)) - { - DebugConsole.ThrowError($"Error in event \"{prefab.Identifier}\". Could not find a label matching the GoTo \"{gotoAction.Name}\"."); + { + DebugConsole.ThrowError($"Error in event \"{prefab.Identifier}\". Could not find a label matching the GoTo \"{gotoAction.Name}\".", + contentPackage: prefab.ContentPackage); } } @@ -108,7 +125,8 @@ namespace Barotrauma if (target is Character character) { return character.Name; } if (target is Hull hull) { return hull.DisplayName.Value; } if (target is Submarine sub) { return sub.Info.DisplayName.Value; } - DebugConsole.AddWarning($"Failed to get the name of the event target {target} as a replacement for the tag {tag} in an event text."); + DebugConsole.AddWarning($"Failed to get the name of the event target {target} as a replacement for the tag {tag} in an event text.", + prefab.ContentPackage); return target.ToString(); } else @@ -187,13 +205,13 @@ namespace Barotrauma } } - public void AddTargetPredicate(Identifier tag, Predicate predicate) + public void AddTargetPredicate(Identifier tag, TargetPredicate.EntityType entityType, Predicate predicate) { if (!targetPredicates.ContainsKey(tag)) { - targetPredicates.Add(tag, new List>()); + targetPredicates.Add(tag, new List()); } - targetPredicates[tag].Add(predicate); + targetPredicates[tag].Add(new TargetPredicate(entityType, predicate)); // force re-search for this tag if (cachedTargets.ContainsKey(tag)) { @@ -225,7 +243,6 @@ namespace Barotrauma } List targetsToReturn = new List(); - if (Targets.ContainsKey(tag)) { foreach (Entity e in Targets[tag]) @@ -236,11 +253,24 @@ namespace Barotrauma } if (targetPredicates.ContainsKey(tag)) { - foreach (Entity entity in Entity.GetEntities()) + foreach (var targetPredicate in targetPredicates[tag]) { - if (targetPredicates[tag].Any(p => p(entity)) && !targetsToReturn.Contains(entity)) + IEnumerable entityList = targetPredicate.Type switch { - targetsToReturn.Add(entity); + TargetPredicate.EntityType.Character => Character.CharacterList, + TargetPredicate.EntityType.Item => Item.ItemList, + TargetPredicate.EntityType.Structure => MapEntity.MapEntityList.Where(m => m is Structure), + TargetPredicate.EntityType.Hull => Hull.HullList, + TargetPredicate.EntityType.Submarine => Submarine.Loaded, + _ => Entity.GetEntities(), + }; + foreach (Entity entity in entityList) + { + if (targetsToReturn.Contains(entity)) { continue; } + if (targetPredicate.Predicate(entity)) + { + targetsToReturn.Add(entity); + } } } } @@ -289,14 +319,8 @@ namespace Barotrauma { int botCount = 0; int playerCount = 0; - bool forceRefreshTargets = false; foreach (Character c in Character.CharacterList) { - if (c.Removed) - { - forceRefreshTargets = true; - continue; - } if (c.IsPlayer) { playerCount++; @@ -306,10 +330,11 @@ namespace Barotrauma botCount++; } } - if (forceRefreshTargets || Entity.EntityCount != prevEntityCount || botCount != prevBotCount || playerCount != prevPlayerCount || prevControlled != Character.Controlled) + + if (botCount != prevBotCount || playerCount != prevPlayerCount || prevControlled != Character.Controlled || NeedsToRefreshCachedTargets()) { cachedTargets.Clear(); - prevEntityCount = Entity.EntityCount; + newEntitySpawned = false; prevBotCount = botCount; prevPlayerCount = playerCount; prevControlled = Character.Controlled; @@ -349,7 +374,8 @@ namespace Barotrauma } if (CurrentActionIndex == -1) { - DebugConsole.AddWarning($"Could not find the GoTo label \"{goTo}\" in the event \"{Prefab.Identifier}\". Ending the event."); + DebugConsole.AddWarning($"Could not find the GoTo label \"{goTo}\" in the event \"{Prefab.Identifier}\". Ending the event.", + prefab.ContentPackage); } } @@ -364,6 +390,47 @@ namespace Barotrauma } } + private bool NeedsToRefreshCachedTargets() + { + if (newEntitySpawned) { return true; } + foreach (var cachedTargetList in cachedTargets.Values) + { + foreach (var target in cachedTargetList) + { + //one of the previously cached entities has been removed -> force refresh + if (target.Removed) + { + return true; + } + } + } + return false; + } + + public void EntitySpawned(Entity entity) + { + if (newEntitySpawned) { return; } + if (entity is Character character && + Level.Loaded?.StartOutpost != null && + Level.Loaded.StartOutpost.Info.OutpostNPCs.Values.Any(npcList => npcList.Contains(character))) + { + newEntitySpawned = true; + return; + } + //new entity matches one of the existing predicates -> force refresh + foreach (var targetPredicateList in targetPredicates.Values) + { + foreach (var targetPredicate in targetPredicateList) + { + if (targetPredicate.Predicate(entity)) + { + newEntitySpawned = true; + return; + } + } + } + } + public override bool LevelMeetsRequirements() { if (requiredDestinationTypes == null) { return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs index abbb65f70..6453c4d52 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs @@ -294,6 +294,11 @@ namespace Barotrauma.Extensions .Where(nullable => nullable.HasValue) .Select(nullable => nullable.Value); + public static IEnumerable NotNull(this IEnumerable source) where T : class + => source + .Where(nullable => nullable != null) + .Select(nullable => nullable!); + public static IEnumerable NotNone(this IEnumerable> source) { foreach (var o in source) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index f7273d22c..9f6cbf6dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -26,6 +26,13 @@ namespace Barotrauma public readonly int BuyerCharacterInfoIdentifier; + /// + /// Should the items be given to the buyer immediately, as opposed to spawning them in the sub the next round? + /// + public bool DeliverImmediately { get; set; } + + public bool Delivered; + public PurchasedItem(ItemPrefab itemPrefab, int quantity, int buyerCharacterInfoId) { ItemPrefabIdentifier = itemPrefab.Identifier; @@ -216,9 +223,11 @@ namespace Barotrauma public List GetPurchasedItems(Location.StoreInfo store, bool create = false) => GetPurchasedItems(store?.Identifier ?? Identifier.Empty, create); - public PurchasedItem GetPurchasedItem(Identifier identifier, ItemPrefab prefab) => GetPurchasedItems(identifier)?.FirstOrDefault(i => i.ItemPrefab == prefab); + public int GetPurchasedItemCount(Location.StoreInfo store, ItemPrefab prefab) => + GetPurchasedItemCount(store?.Identifier ?? Identifier.Empty, prefab); - public PurchasedItem GetPurchasedItem(Location.StoreInfo store, ItemPrefab prefab) => GetPurchasedItem(store?.Identifier ?? Identifier.Empty, prefab); + public int GetPurchasedItemCount(Identifier identifier, ItemPrefab prefab) => + GetPurchasedItems(identifier)?.Where(i => i.ItemPrefab == prefab).Sum(it => it.Quantity) ?? 0; public List GetSoldItems(Identifier identifier, bool create = false) => GetItems(identifier, SoldItems, create); @@ -287,23 +296,6 @@ namespace Barotrauma OnItemsInSellFromSubCrateChanged?.Invoke(this); } -#if SERVER - public void OnNewItemsPurchased(Identifier storeIdentifier, List newItems, Client client) - { - StringBuilder sb = new StringBuilder(); - int price = 0; - Dictionary buyValues = GetBuyValuesAtCurrentLocation(storeIdentifier, newItems.Select(i => i.ItemPrefab)); - foreach (PurchasedItem item in newItems) - { - int itemValue = item.Quantity * buyValues[item.ItemPrefab]; - GameAnalyticsManager.AddMoneySpentEvent(itemValue, GameAnalyticsManager.MoneySink.Store, item.ItemPrefab.Identifier.Value); - sb.Append($"\n - {item.ItemPrefab.Name} x{item.Quantity}"); - price += itemValue; - } - GameServer.Log($"{NetworkMember.ClientLogName(client, client?.Name ?? "Unknown")} purchased {newItems.Count} item(s) for {TextManager.FormatCurrency(price)}{sb.ToString()}", ServerLog.MessageType.Money); - } -#endif - public void PurchaseItems(Identifier storeIdentifier, List itemsToPurchase, bool removeFromCrate, Client client = null) { var store = Location.GetStore(storeIdentifier); @@ -314,27 +306,58 @@ namespace Barotrauma var itemsInStoreCrate = GetBuyCrateItems(storeIdentifier, create: true); foreach (PurchasedItem item in itemsToPurchase) { + if (item.Quantity <= 0) { continue; } // Exchange money int itemValue = item.Quantity * buyValues[item.ItemPrefab]; if (!campaign.TryPurchase(client, itemValue)) { continue; } // Add to the purchased items - var purchasedItem = itemsPurchasedFromStore.Find(pi => pi.ItemPrefab == item.ItemPrefab); + var purchasedItem = itemsPurchasedFromStore.Find(pi => pi.ItemPrefab == item.ItemPrefab && pi.DeliverImmediately == item.DeliverImmediately); if (purchasedItem != null) { purchasedItem.Quantity += item.Quantity; } else { - purchasedItem = new PurchasedItem(item.ItemPrefab, item.Quantity, client); + purchasedItem = new PurchasedItem(item.ItemPrefab, item.Quantity, client) { DeliverImmediately = item.DeliverImmediately }; itemsPurchasedFromStore.Add(purchasedItem); } + purchasedItem.Delivered = item.DeliverImmediately; if (GameMain.IsSingleplayer) { GameAnalyticsManager.AddMoneySpentEvent(itemValue, GameAnalyticsManager.MoneySink.Store, item.ItemPrefab.Identifier.Value); } store.Balance += itemValue; - if (removeFromCrate) + } + if (GameMain.NetworkMember is not { IsClient: true }) + { + Character targetCharacter; +#if CLIENT + targetCharacter = Character.Controlled; + if (targetCharacter == null) + { + DebugConsole.ThrowError("Failed to deliver items directly to a character (not controlling a character)."); + } +#else + targetCharacter = client?.Character; + if (targetCharacter == null) + { + DebugConsole.ThrowError($"Failed to deliver items directly to a character ({(client == null ? "client was null" : $"client {client.Name} is not controlling a character")})."); + } +#endif + if (targetCharacter == null) + { + DeliverItemsToSub(itemsToPurchase.Where(it => it.DeliverImmediately), Submarine.MainSub, this); + } + else + { + DeliverItemsToCharacter(itemsToPurchase.Where(it => it.DeliverImmediately), targetCharacter, this); + } + + } + if (removeFromCrate) + { + foreach (PurchasedItem item in itemsToPurchase) { // Remove from the shopping crate if (itemsInStoreCrate.Find(pi => pi.ItemPrefab == item.ItemPrefab) is { } crateItem) @@ -387,9 +410,9 @@ namespace Barotrauma var items = new List(); foreach (var storeSpecificItems in PurchasedItems) { - items.AddRange(storeSpecificItems.Value); + items.AddRange(storeSpecificItems.Value.Where(it => !it.DeliverImmediately)); } - CreateItems(items, Submarine.MainSub, this); + DeliverItemsToSub(items, Submarine.MainSub, this); PurchasedItems.Clear(); OnPurchasedItemsChanged?.Invoke(this); } @@ -440,10 +463,22 @@ namespace Barotrauma if (!item.Components.All(static c => c is not Holdable { Attachable: true, Attached: true })) { return false; } if (!item.Components.All(static c => c is not Wire w || w.Connections.All(static c => c is null))) { return false; } if (!ItemAndAllContainersInteractable(item)) { return false; } - if (item.RootContainer is Item rootContainer && rootContainer.HasTag(Tags.DontSellItems)) { return false; } + if (!AllContainersAllowSellingItems(item)) { return false; } return true; }).Distinct(); + static bool AllContainersAllowSellingItems(Item item) + { + do + { + item = item.Container; + if (item is null) { return true; } + if (item.HasTag(Tags.DontSellItems)) { return false; } + if (item.Components.Any(static c => c.DisallowSellingItemsFromContainer)) { return false; } + } while (item != null); + return true; + } + static bool ItemAndAllContainersInteractable(Item item) { do @@ -501,7 +536,7 @@ namespace Barotrauma => items.Where(it => it.HasTag(Tags.Crate) && !it.NonInteractable && !it.NonPlayerTeamInteractable && !it.HiddenInGame && !it.Removed && (conditional == null || conditional(it))); public static IEnumerable FindReusableCargoContainers(IEnumerable subs, IEnumerable cargoRooms = null) => - FilterCargoCrates(Item.ItemList, it => subs.Contains(it.Submarine) && (cargoRooms == null || cargoRooms.Contains(it.CurrentHull))) + FilterCargoCrates(Item.ItemList, it => subs.Contains(it.Submarine) && !it.HasTag(Tags.CargoMissionItem) && (cargoRooms == null || cargoRooms.Contains(it.CurrentHull))) .Select(it => it.GetComponent()) .Where(c => c != null); @@ -553,9 +588,9 @@ namespace Barotrauma return itemContainer; } - public static void CreateItems(List itemsToSpawn, Submarine sub, CargoManager cargoManager) + public static void DeliverItemsToSub(IEnumerable itemsToSpawn, Submarine sub, CargoManager cargoManager) { - if (itemsToSpawn.Count == 0) { return; } + if (!itemsToSpawn.Any()) { return; } WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, sub); if (wp == null) @@ -571,7 +606,7 @@ namespace Barotrauma return; } - if (sub == Submarine.MainSub) + if (sub == Submarine.MainSub && itemsToSpawn.Any(it => !it.Delivered && it.Quantity > 0)) { #if CLIENT new GUIMessageBox("", @@ -597,37 +632,66 @@ namespace Barotrauma List availableContainers = FindReusableCargoContainers(connectedSubs, FindCargoRooms(connectedSubs)).ToList(); foreach (PurchasedItem pi in itemsToSpawn) { + pi.Delivered = true; Vector2 position = GetCargoPos(cargoRoom, pi.ItemPrefab); - for (int i = 0; i < pi.Quantity; i++) { var item = new Item(pi.ItemPrefab, position, wp.Submarine); var itemContainer = GetOrCreateCargoContainerFor(pi.ItemPrefab, cargoRoom, ref availableContainers); itemContainer?.Inventory.TryPutItem(item, null); - var idCard = item.GetComponent(); - if (cargoManager != null && idCard != null && pi.BuyerCharacterInfoIdentifier != 0) - { - cargoManager.purchasedIDCards.Add((pi, idCard)); - } - itemSpawned(pi, item); + ItemSpawned(pi, item, cargoManager); #if SERVER Entity.Spawner?.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item)); #endif - (itemContainer?.Item ?? item).AssignCampaignInteractionType(CampaignMode.InteractionType.Cargo); - static void itemSpawned(PurchasedItem purchased, Item item) - { - Submarine sub = item.Submarine ?? item.RootContainer?.Submarine; - if (sub != null) - { - foreach (WifiComponent wifiComponent in item.GetComponents()) - { - wifiComponent.TeamID = sub.TeamID; - } - } - } + (itemContainer?.Item ?? item).AssignCampaignInteractionType(CampaignMode.InteractionType.Cargo); + } + } + } + + public static void DeliverItemsToCharacter(IEnumerable itemsToSpawn, Character character, CargoManager cargoManager) + { + if (!itemsToSpawn.Any()) { return; } + + foreach (PurchasedItem pi in itemsToSpawn) + { + pi.Delivered = true; + for (int i = 0; i < pi.Quantity; i++) + { + var item = new Item(pi.ItemPrefab, character.Position, character.Submarine); +#if SERVER + Entity.Spawner?.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item)); +#endif + if (!character.Inventory.TryPutItem(item, user: null, item.AllowedSlots)) + { + foreach (Item containedItem in character.Inventory.AllItemsMod) + { + if (containedItem.OwnInventory != null && + containedItem.OwnInventory.TryPutItem(item, user: null, item.AllowedSlots)) + { + break; + } + } + } + ItemSpawned(pi, item, cargoManager); + } + } + } + private static void ItemSpawned(PurchasedItem purchased, Item item, CargoManager cargoManager) + { + var idCard = item.GetComponent(); + if (cargoManager != null && idCard != null && purchased.BuyerCharacterInfoIdentifier != 0) + { + cargoManager.purchasedIDCards.Add((purchased, idCard)); + } + + Submarine sub = item.Submarine ?? item.RootContainer?.Submarine; + if (sub != null) + { + foreach (WifiComponent wifiComponent in item.GetComponents()) + { + wifiComponent.TeamID = sub.TeamID; } } - itemsToSpawn.Clear(); } private readonly List<(PurchasedItem purchaseInfo, IdCard idCard)> purchasedIDCards = new List<(PurchasedItem purchaseInfo, IdCard idCard)>(); @@ -685,6 +749,7 @@ namespace Barotrauma new XAttribute("id", item.ItemPrefab.Identifier), new XAttribute("qty", item.Quantity), new XAttribute("storeid", storeSpecificItems.Key), + new XAttribute("deliverimmediately", item.DeliverImmediately), new XAttribute("buyer", item.BuyerCharacterInfoIdentifier))); } } @@ -700,17 +765,22 @@ namespace Barotrauma { string prefabId = itemElement.GetAttributeString("id", null); if (string.IsNullOrWhiteSpace(prefabId)) { continue; } - var prefab = ItemPrefab.Prefabs.Find(p => p.Identifier == prefabId); - if (prefab == null) { continue; } + if (!ItemPrefab.Prefabs.TryGet(prefabId.ToIdentifier(), out var prefab)) { continue; } int qty = itemElement.GetAttributeInt("qty", 0); Identifier storeId = itemElement.GetAttributeIdentifier("storeid", "merchant"); + bool deliverImmediately = itemElement.GetAttributeBool("deliverimmediately", false); int buyerId = itemElement.GetAttributeInt("buyer", 0); if (!purchasedItems.TryGetValue(storeId, out var storeItems)) { storeItems = new List(); purchasedItems.Add(storeId, storeItems); } - storeItems.Add(new PurchasedItem(prefab, qty, buyerId)); + storeItems.Add(new PurchasedItem(prefab, qty, buyerId) + { + DeliverImmediately = deliverImmediately, + //must have already been delivered if we had opted for immediate delivery + Delivered = deliverImmediately + }); } } SetPurchasedItems(purchasedItems); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index c2f3a80d4..762807d0d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -120,7 +120,7 @@ namespace Barotrauma foreach (var characterElement in element.Elements()) { if (!characterElement.Name.ToString().Equals("character", StringComparison.OrdinalIgnoreCase)) { continue; } - CharacterInfo characterInfo = new CharacterInfo(characterElement); + CharacterInfo characterInfo = new CharacterInfo(new ContentXElement(contentPackage: null, characterElement)); #if CLIENT if (characterElement.GetAttributeBool("lastcontrolled", false)) { characterInfo.LastControlled = true; } characterInfo.CrewListIndex = characterElement.GetAttributeInt("crewlistindex", -1); @@ -262,7 +262,7 @@ namespace Barotrauma while (spawnWaypoints.Any() && spawnWaypoints.Count < characterInfos.Count) { spawnWaypoints.Add(spawnWaypoints[Rand.Int(spawnWaypoints.Count)]); - } + } } if (spawnWaypoints == null || !spawnWaypoints.Any()) { @@ -306,15 +306,16 @@ namespace Barotrauma } character.LoadTalents(); - character.GiveIdCardTags(new List() { mainSubWaypoints[i], spawnWaypoints[i] }); - character.Info.StartItemsGiven = true; + 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; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs index 004c007bc..b8c680577 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs @@ -45,7 +45,14 @@ namespace Barotrauma } else { - data.Add(identifier, Convert.ChangeType(value, type, NumberFormatInfo.InvariantInfo)); + try + { + data.Add(identifier, Convert.ChangeType(value, type, NumberFormatInfo.InvariantInfo)); + } + catch (Exception e) + { + DebugConsole.ThrowError($"Failed to change the type of the value \"{value}\" to {type}.", e); + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs index 1ff492a18..a24303b83 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs @@ -57,7 +57,7 @@ namespace Barotrauma if (increase != 0 && Character.Controlled != null) { Character.Controlled.AddMessage( - TextManager.GetWithVariable("reputationgainnotification", "[reputationname]", Location?.Name ?? Faction.Prefab.Name).Value, + TextManager.GetWithVariable("reputationgainnotification", "[reputationname]", Location?.DisplayName ?? Faction.Prefab.Name).Value, increase > 0 ? GUIStyle.Green : GUIStyle.Red, playSound: true, Identifier, increase, lifetime: 5.0f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index 2722ee955..e12456b62 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -382,22 +382,28 @@ namespace Barotrauma currentLocation.DeselectMission(mission); } } - if (levelData.HasBeaconStation && !levelData.IsBeaconActive) + if (levelData.HasBeaconStation && !levelData.IsBeaconActive && Missions.None(m => m.Prefab.Type == MissionType.Beacon)) { - var beaconMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Contains("beaconnoreward")).OrderBy(m => m.UintIdentifier); + var beaconMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.IsSideObjective && m.Type == MissionType.Beacon); if (beaconMissionPrefabs.Any()) { - Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed)); - var beaconMissionPrefab = ToolBox.SelectWeightedRandom(beaconMissionPrefabs, p => (float)p.Commonness, rand); - if (!Missions.Any(m => m.Prefab.Type == beaconMissionPrefab.Type)) + var filteredMissions = beaconMissionPrefabs.Where(m => levelData.Difficulty >= m.MinLevelDifficulty && levelData.Difficulty <= m.MaxLevelDifficulty); + if (filteredMissions.None()) { - extraMissions.Add(beaconMissionPrefab.Instantiate(Map.SelectedConnection.Locations, Submarine.MainSub)); + DebugConsole.AddWarning($"No suitable beacon mission found matching the level difficulty {levelData.Difficulty}. Ignoring the restriction."); } + else + { + beaconMissionPrefabs = filteredMissions; + } + Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed)); + var beaconMissionPrefab = ToolBox.SelectWeightedRandom(beaconMissionPrefabs, p => p.Commonness, rand); + extraMissions.Add(beaconMissionPrefab.Instantiate(Map.SelectedConnection.Locations, Submarine.MainSub)); } } if (levelData.HasHuntingGrounds) { - var huntingGroundsMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Contains("huntinggrounds")).OrderBy(m => m.UintIdentifier); + var huntingGroundsMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.IsSideObjective && m.Tags.Contains("huntinggrounds")).OrderBy(m => m.UintIdentifier); if (!huntingGroundsMissionPrefabs.Any()) { DebugConsole.AddWarning("Could not find a hunting grounds mission for the level. No mission with the tag \"huntinggrounds\" found."); @@ -552,8 +558,8 @@ namespace Barotrauma if (availableTransition == TransitionType.None) { DebugConsole.ThrowError("Failed to load a new campaign level. No available level transitions " + - "(current location: " + (map.CurrentLocation?.Name ?? "null") + ", " + - "selected location: " + (map.SelectedLocation?.Name ?? "null") + ", " + + "(current location: " + (map.CurrentLocation?.DisplayName ?? "null") + ", " + + "selected location: " + (map.SelectedLocation?.DisplayName ?? "null") + ", " + "leaving sub: " + (leavingSub?.Info?.Name ?? "null") + ", " + "at start: " + (leavingSub?.AtStartExit.ToString() ?? "null") + ", " + "at end: " + (leavingSub?.AtEndExit.ToString() ?? "null") + ")\n" + @@ -564,8 +570,8 @@ namespace Barotrauma { DebugConsole.ThrowError("Failed to load a new campaign level. No available level transitions " + "(transition type: " + availableTransition + ", " + - "current location: " + (map.CurrentLocation?.Name ?? "null") + ", " + - "selected location: " + (map.SelectedLocation?.Name ?? "null") + ", " + + "current location: " + (map.CurrentLocation?.DisplayName ?? "null") + ", " + + "selected location: " + (map.SelectedLocation?.DisplayName ?? "null") + ", " + "leaving sub: " + (leavingSub?.Info?.Name ?? "null") + ", " + "at start: " + (leavingSub?.AtStartExit.ToString() ?? "null") + ", " + "at end: " + (leavingSub?.AtEndExit.ToString() ?? "null") + ")\n" + @@ -576,8 +582,8 @@ namespace Barotrauma ShowCampaignUI = ForceMapUI = false; #endif DebugConsole.NewMessage("Transitioning to " + (nextLevel?.Seed ?? "null") + - " (current location: " + (map.CurrentLocation?.Name ?? "null") + ", " + - "selected location: " + (map.SelectedLocation?.Name ?? "null") + ", " + + " (current location: " + (map.CurrentLocation?.DisplayName ?? "null") + ", " + + "selected location: " + (map.SelectedLocation?.DisplayName ?? "null") + ", " + "leaving sub: " + (leavingSub?.Info?.Name ?? "null") + ", " + "at start: " + (leavingSub?.AtStartExit.ToString() ?? "null") + ", " + "at end: " + (leavingSub?.AtEndExit.ToString() ?? "null") + ", " + @@ -862,6 +868,16 @@ namespace Barotrauma } } + //remove ID cards left in duffel bags + foreach (var item in Item.ItemList.ToList()) + { + if (item.HasTag(Tags.IdCardTag) && + (item.Container?.HasTag(Tags.DespawnContainer) ?? false)) + { + item.Remove(); + } + } + foreach (CharacterInfo ci in CrewManager.CharacterInfos.ToList()) { if (ci.CauseOfDeath != null) @@ -1008,7 +1024,7 @@ namespace Barotrauma return ToolBox.SelectWeightedRandom(factionsList, weights, random); } - public bool TryHireCharacter(Location location, CharacterInfo characterInfo, Client client = null) + public bool TryHireCharacter(Location location, CharacterInfo characterInfo, Character hirer, Client client = null) { if (characterInfo == null) { return false; } if (characterInfo.MinReputationToHire.factionId != Identifier.Empty) @@ -1018,7 +1034,8 @@ namespace Barotrauma return false; } } - if (!TryPurchase(client, characterInfo.Salary)) { return false; } + + if (!TryPurchase(client, HireManager.GetSalaryFor(characterInfo))) { return false; } characterInfo.IsNewHire = true; characterInfo.Title = null; location.RemoveHireableCharacter(characterInfo); @@ -1247,7 +1264,7 @@ namespace Barotrauma { DebugConsole.NewMessage("********* CAMPAIGN STATUS *********", Color.White); DebugConsole.NewMessage(" Money: " + Bank.Balance, Color.White); - DebugConsole.NewMessage(" Current location: " + map.CurrentLocation.Name, Color.White); + DebugConsole.NewMessage(" Current location: " + map.CurrentLocation.DisplayName, Color.White); DebugConsole.NewMessage(" Available destinations: ", Color.White); for (int i = 0; i < map.CurrentLocation.Connections.Count; i++) @@ -1255,11 +1272,11 @@ namespace Barotrauma Location destination = map.CurrentLocation.Connections[i].OtherLocation(map.CurrentLocation); if (destination == map.SelectedLocation) { - DebugConsole.NewMessage(" " + i + ". " + destination.Name + " [SELECTED]", Color.White); + DebugConsole.NewMessage(" " + i + ". " + destination.DisplayName + " [SELECTED]", Color.White); } else { - DebugConsole.NewMessage(" " + i + ". " + destination.Name, Color.White); + DebugConsole.NewMessage(" " + i + ". " + destination.DisplayName, Color.White); } } @@ -1291,7 +1308,7 @@ namespace Barotrauma { if (NumberOfMissionsAtLocation(location) > Settings.TotalMaxMissionCount) { - DebugConsole.AddWarning($"Client {sender.Name} had too many missions selected for location {location.Name}! Count was {NumberOfMissionsAtLocation(location)}. Deselecting extra missions."); + DebugConsole.AddWarning($"Client {sender.Name} had too many missions selected for location {location.DisplayName}! Count was {NumberOfMissionsAtLocation(location)}. Deselecting extra missions."); foreach (Mission mission in currentLocation.SelectedMissions.Where(m => m.Locations[1] == location).Skip(Settings.TotalMaxMissionCount).ToList()) { currentLocation.DeselectMission(mission); @@ -1344,7 +1361,7 @@ namespace Barotrauma var itemsToTransfer = new List<(Item item, Item container)>(); if (PendingSubmarineSwitch != null) { - var connectedSubs = currentSub.GetConnectedSubs().Where(s => s.Info.Type == SubmarineType.Player); + var connectedSubs = currentSub.GetConnectedSubs().Where(s => s.Info.Type == SubmarineType.Player).ToHashSet(); // Remove items from the old sub foreach (Item item in Item.ItemList) { @@ -1405,8 +1422,8 @@ namespace Barotrauma return; } // First move the cargo containers, so that we can reuse them - var cargoContainers = itemsToTransfer.Where(it => it.item.HasTag(Tags.Crate)); - foreach (var (item, oldContainer) in cargoContainers) + var cargoContainers = itemsToTransfer.Where(it => it.item.HasTag(Tags.Crate)).ToHashSet(); + foreach (var (item, _) in cargoContainers) { Vector2 simPos = ConvertUnits.ToSimUnits(CargoManager.GetCargoPos(spawnHull, item.Prefab)); item.SetTransform(simPos, 0.0f, findNewHull: false, setPrevTransform: false); @@ -1414,7 +1431,6 @@ namespace Barotrauma item.Submarine = spawnHull.Submarine; } // Then move the other items - var cargoRooms = CargoManager.FindCargoRooms(newSub); List availableContainers = CargoManager.FindReusableCargoContainers(connectedSubs).ToList(); foreach (var (item, oldContainer) in itemsToTransfer) { @@ -1444,7 +1460,7 @@ namespace Barotrauma newContainerName = cargoContainer.Item.Prefab.Identifier.ToString(); } } - string msg = "Item transfer log error."; + string msg; if (oldContainer != null) { if (newContainer == null && oldContainer == item.Container) @@ -1466,6 +1482,27 @@ namespace Barotrauma DebugConsole.Log(msg); #endif } + + foreach (var (item, _) in itemsToTransfer) + { + // This ensures that the new submarine takes ownership of + // the items contained within the items that are being transferred directly, + // i.e. circuit box components and wires + PropagateSubmarineProperty(item); + } + + static void PropagateSubmarineProperty(Item item) + { + foreach (var ownedContainer in item.GetComponents()) + { + foreach (var containedItem in ownedContainer.Inventory.AllItems) + { + containedItem.Submarine = item.Submarine; + PropagateSubmarineProperty(containedItem); + } + } + } + newSub.Info.NoItems = false; // Serialize the new sub PendingSubmarineSwitch = new SubmarineInfo(newSub); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs index 31aa8aa9e..a9f757191 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs @@ -83,6 +83,12 @@ namespace Barotrauma } } + [Serialize(0.2f, IsPropertySaveable.Yes, description: "How likely it is for security to inspect player characters for stolen items when your reputation is high?")] + public float MinStolenItemInspectionProbability { get; set; } + + [Serialize(0.9f, IsPropertySaveable.Yes, description: "How likely it is for security to inspect player characters for stolen items when your reputation is low?")] + public float MaxStolenItemInspectionProbability { get; set; } + public const int DefaultMaxMissionCount = 2; public const int MaxMissionCountLimit = 10; public const int MinMissionCountLimit = 1; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index a6c56b8f6..a3bce5575 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -319,6 +319,7 @@ namespace Barotrauma foreach (var item in storeItems.Value) { msg.WriteIdentifier(item.ItemPrefabIdentifier); + msg.WriteBoolean(item.DeliverImmediately); msg.WriteRangedInteger(item.Quantity, 0, CargoManager.MaxQuantity); } } @@ -336,8 +337,12 @@ namespace Barotrauma for (int j = 0; j < itemCount; j++) { Identifier itemId = msg.ReadIdentifier(); + bool deliverImmediately = msg.ReadBoolean(); +#if SERVER + if (!AllowImmediateItemDelivery(sender)) { deliverImmediately = false; } +#endif int quantity = msg.ReadRangedInteger(0, CargoManager.MaxQuantity); - items[storeId].Add(new PurchasedItem(itemId, quantity, sender)); + items[storeId].Add(new PurchasedItem(itemId, quantity, sender) { DeliverImmediately = deliverImmediately }); } } return items; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 5a52f982e..38a259fd5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -161,16 +161,18 @@ namespace Barotrauma { switch (subElement.Name.ToString().ToLowerInvariant()) { -#if CLIENT case "gamemode": //legacy support case "singleplayercampaign": +#if CLIENT CrewManager = new CrewManager(true); var campaign = SinglePlayerCampaign.Load(subElement); campaign.LoadNewLevel(); GameMode = campaign; InitOwnedSubs(submarineInfo, ownedSubmarines); - break; +#else + throw new Exception("The server cannot load a single player campaign."); #endif + break; case "multiplayercampaign": CrewManager = new CrewManager(false); var mpCampaign = MultiPlayerCampaign.LoadNew(subElement); @@ -567,7 +569,7 @@ namespace Barotrauma if (EndLocation != null && levelData != null) { GUI.AddMessage(levelData.Biome.DisplayName, Color.Lerp(Color.CadetBlue, Color.DarkRed, levelData.Difficulty / 100.0f), 5.0f, playSound: false); - GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Destination"), EndLocation.Name), Color.CadetBlue, playSound: false); + GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Destination"), EndLocation.DisplayName), Color.CadetBlue, playSound: false); var missionsToShow = missions.Where(m => m.Prefab.ShowStartMessage); if (missionsToShow.Count() > 1) { @@ -582,7 +584,7 @@ namespace Barotrauma } else { - GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Location"), StartLocation.Name), Color.CadetBlue, playSound: false); + GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Location"), StartLocation.DisplayName), Color.CadetBlue, playSound: false); } } @@ -871,6 +873,7 @@ namespace Barotrauma //Clear the grids to allow for garbage collection Powered.Grids.Clear(); + Powered.ChangedConnections.Clear(); try { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/HireManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/HireManager.cs index 9e76c344f..5e2ecdd71 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/HireManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/HireManager.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; namespace Barotrauma @@ -20,6 +21,23 @@ namespace Barotrauma AvailableCharacters.Remove(character); } + public static int GetSalaryFor(IReadOnlyCollection hires) + { + return hires.Sum(hire => GetSalaryFor(hire)); + } + + public static int GetSalaryFor(CharacterInfo hire) + { + IEnumerable crew = GameSession.GetSessionCrewCharacters(CharacterType.Both); + float multiplier = 0; + foreach (var character in crew) + { + multiplier += character?.Info?.GetSavedStatValueWithAll(StatTypes.HireCostMultiplier, hire.Job.Prefab.Identifier) ?? 0; + } + float finalMultiplier = 1f + MathF.Max(multiplier, -1f); + return (int)(hire.Salary * finalMultiplier); + } + public void GenerateCharacters(Location location, int amount) { AvailableCharacters.ForEach(c => c.Remove()); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs index 5c416862d..fd7a6fcc9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs @@ -177,7 +177,7 @@ namespace Barotrauma return; } - int price = prefab.Price.GetBuyPrice(GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation); + int price = prefab.Price.GetBuyPrice(prefab, GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation); int currentLevel = GetUpgradeLevel(prefab, category); int newLevel = currentLevel + 1; @@ -198,20 +198,23 @@ namespace Barotrauma return result; } - switch (GameMain.NetworkMember) + if (!force) { - case null when Character.Controlled is { } controlled: // singleplayer - if (!TryTakeResources(controlled)) { return; } - break; - case { IsClient: true }: - if (!prefab.HasResourcesToUpgrade(Character.Controlled, newLevel)) { return; } - break; - case { IsServer: true } when client?.Character is { } character: - if (!TryTakeResources(character)) { return; } - break; - default: - DebugConsole.ThrowError($"Tried to purchase \"{prefab.Name}\" without a player."); - return; + switch (GameMain.NetworkMember) + { + case null when Character.Controlled is { } controlled: // singleplayer + if (!TryTakeResources(controlled)) { return; } + break; + case { IsClient: true }: + if (!prefab.HasResourcesToUpgrade(Character.Controlled, newLevel)) { return; } + break; + case { IsServer: true } when client?.Character is { } character: + if (!TryTakeResources(character)) { return; } + break; + default: + DebugConsole.ThrowError($"Tried to purchase \"{prefab.Name}\" without a player."); + return; + } } if (price < 0) @@ -683,7 +686,8 @@ namespace Barotrauma { // automatically fix this if it ever happens? DebugConsole.AddWarning($"The upgrade {newUpgrade.Prefab.Name} in {target.Name} has a different level compared to other items! \n" + - $"Expected level was ${newLevel} but got {newUpgrade.Level} instead."); + $"Expected level was ${newLevel} but got {newUpgrade.Level} instead.", + newUpgrade.Prefab.ContentPackage); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index c6522899c..a8d47043e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -48,13 +48,13 @@ namespace Barotrauma private set; } - private static string[] ParseSlotTypes(XElement element) + private static string[] ParseSlotTypes(ContentXElement element) { string slotString = element.GetAttributeString("slots", null); return slotString == null ? Array.Empty() : slotString.Split(','); } - public CharacterInventory(XElement element, Character character, bool spawnInitialItems) + public CharacterInventory(ContentXElement element, Character character, bool spawnInitialItems) : base(character, ParseSlotTypes(element).Length) { this.character = character; @@ -73,7 +73,8 @@ namespace Barotrauma slotTypeNames[i] = slotTypeNames[i].Trim(); if (!Enum.TryParse(slotTypeNames[i], out parsedSlotType)) { - DebugConsole.ThrowError("Error in the inventory config of \"" + character.SpeciesName + "\" - " + slotTypeNames[i] + " is not a valid inventory slot type."); + DebugConsole.ThrowError("Error in the inventory config of \"" + character.SpeciesName + "\" - " + slotTypeNames[i] + " is not a valid inventory slot type.", + contentPackage: element.ContentPackage); } SlotTypes[i] = parsedSlotType; switch (SlotTypes[i]) @@ -99,7 +100,8 @@ namespace Barotrauma DebugConsole.ThrowError( $"Character \"{character.SpeciesName}\" is configured to spawn with so many items it will have less than 2 free inventory slots. " + "This can cause issues with talents that spawn extra loot in monsters' inventories." - + " Consider increasing the inventory size."); + + " Consider increasing the inventory size.", + contentPackage: element.ContentPackage); } #endif @@ -115,7 +117,8 @@ namespace Barotrauma string itemIdentifier = subElement.GetAttributeString("identifier", ""); if (!ItemPrefab.Prefabs.TryGet(itemIdentifier, out var itemPrefab)) { - DebugConsole.ThrowError("Error in character inventory \"" + character.SpeciesName + "\" - item \"" + itemIdentifier + "\" not found."); + DebugConsole.ThrowError("Error in character inventory \"" + character.SpeciesName + "\" - item \"" + itemIdentifier + "\" not found.", + contentPackage: element.ContentPackage); continue; } @@ -131,7 +134,7 @@ namespace Barotrauma if (item != null && item.ParentInventory != this) { string errorMsg = $"Failed to spawn the initial item \"{item.Prefab.Identifier}\" in the inventory of \"{character.SpeciesName}\"."; - DebugConsole.ThrowError(errorMsg); + DebugConsole.ThrowError(errorMsg, contentPackage: element.ContentPackage); GameAnalyticsManager.AddErrorEventOnce("CharacterInventory:FailedToSpawnInitialItem", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); } else if (!character.Enabled) @@ -148,6 +151,19 @@ namespace Barotrauma partial void InitProjSpecific(XElement element); + public Item FindEquippedItemByTag(Identifier tag) + { + if (tag.IsEmpty) { return null; } + for (int i = 0; i < slots.Length; i++) + { + if (SlotTypes[i] == InvSlotType.Any) { continue; } + + var item = slots[i].FirstOrDefault(); + if (item != null && item.HasTag(tag)) { return item; } + } + return null; + } + public int FindLimbSlot(InvSlotType limbSlot) { for (int i = 0; i < slots.Length; i++) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index 41ff58290..041465193 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -184,6 +184,8 @@ namespace Barotrauma.Items.Components public bool IsFullyClosed => IsClosed && OpenState <= 0f; + public bool HasWindow => Window != Rectangle.Empty; + [Serialize(false, IsPropertySaveable.No, description: "If the door has integrated buttons, it can be opened by interacting with it directly (instead of using buttons wired to it).")] public bool HasIntegratedButtons { get; private set; } @@ -341,7 +343,12 @@ namespace Barotrauma.Items.Components OnFailedToOpen(); return; } - toggleCooldownTimer = ToggleCoolDown; + if (ToggleWhenClicked) + { + //do not activate cooldown at this point if the door doesn't get toggled when clicked + //(i.e. if it just sends out a signal that might get passed back to the door and try to toggle it) + toggleCooldownTimer = ToggleCoolDown; + } if (IsStuck || IsJammed) { #if CLIENT @@ -381,6 +388,31 @@ namespace Barotrauma.Items.Components return false; } + /// + /// Is the given position inside the vertical bounds of the window, and roughly on the door horizontally? Or the other way around if the door opens horizontally. + /// + /// Position in the same coordinate space as the door. + /// Maximum horizontal distance from the door (or vertical if the door opens horizontally) + public bool IsPositionOnWindow(Vector2 position, float maxPerpendicularDistance = 10.0f) + { + if (IsHorizontal) + { + return + position.X >= item.Rect.X + Window.X && + position.X <= item.Rect.X + Window.X + Window.Width && + position.Y >= item.Rect.Y - maxPerpendicularDistance && + position.Y <= item.Rect.Y - item.Rect.Height - maxPerpendicularDistance; + } + else + { + return + position.Y >= item.Rect.Y + Window.Y && + position.Y <= item.Rect.Y + Window.Y + Window.Height && + position.X >= item.Rect.X - maxPerpendicularDistance && + position.X <= item.Rect.Right + maxPerpendicularDistance; + } + } + public override void Update(float deltaTime, Camera cam) { UpdateProjSpecific(deltaTime); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs index a1b8e5738..df2d173c0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs @@ -226,7 +226,8 @@ namespace Barotrauma.Items.Components foreach ((Character character, Node node) in charactersInRange) { if (character == null || character.Removed) { continue; } - character.ApplyAttack(user, node.WorldPosition, attack, MathHelper.Clamp(Voltage, 1.0f, MaxOverVoltageFactor)); + character.ApplyAttack(user, node.WorldPosition, attack, MathHelper.Clamp(Voltage, 1.0f, MaxOverVoltageFactor), + impulseDirection: character.WorldPosition - node.WorldPosition); } } DischargeProjSpecific(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs index 11b26aee2..2edaabeff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs @@ -168,8 +168,8 @@ namespace Barotrauma.Items.Components conditionIncrease += user?.GetStatValue(StatTypes.GeneticMaterialRefineBonus) ?? 0.0f; if (item.Prefab == otherGeneticMaterial.item.Prefab) { + float taintedProbability = GetTaintedProbabilityOnRefine(otherGeneticMaterial, user); item.Condition = Math.Max(item.Condition, otherGeneticMaterial.item.Condition) + conditionIncrease; - float taintedProbability = GetTaintedProbabilityOnRefine(user); if (taintedProbability >= Rand.Range(0.0f, 1.0f)) { MakeTainted(); @@ -221,10 +221,10 @@ namespace Barotrauma.Items.Components return taintedEffectStrength; } - private float GetTaintedProbabilityOnRefine(Character user) + private float GetTaintedProbabilityOnRefine(GeneticMaterial otherGeneticMaterial, Character user) { if (user == null) { return 1.0f; } - float probability = MathHelper.Lerp(0.0f, 0.99f, item.Condition / 100.0f); + float probability = MathHelper.Lerp(0.0f, 0.99f, Math.Max(item.Condition, otherGeneticMaterial.Item.Condition) / 100.0f); probability *= MathHelper.Lerp(1.0f, 0.25f, DegreeOfSuccess(user)); return MathHelper.Clamp(probability, 0.0f, 1.0f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs index f15afb13c..846e92df5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs @@ -59,7 +59,8 @@ namespace Barotrauma.Items.Components StatusEffect effect = StatusEffect.Load(subElement, Prefab?.Name.Value); if (effect.type != ActionType.OnProduceSpawned) { - DebugConsole.ThrowError("Only OnProduceSpawned type can be used in ."); + DebugConsole.ThrowError("Only OnProduceSpawned type can be used in .", + contentPackage: element.ContentPackage); continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs index c482ec981..7d789b672 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs @@ -80,9 +80,6 @@ namespace Barotrauma.Items.Components [Serialize("0,0", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public Vector2 OwnerSheetIndex { get; set; } - [Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] - public bool SpawnPointTagsGiven { get; set; } - public IdCard(Item item, ContentXElement element) : base(item, element) { } public void Initialize(WayPoint spawnPoint, Character character) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index 05fac61cd..737ae2232 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -439,9 +439,10 @@ namespace Barotrauma.Items.Components if (targetItem.Removed) { return; } var attackResult = Attack.DoDamage(user, targetItem, item.WorldPosition, 1.0f); #if CLIENT - if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar) + if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar && Character.Controlled != null && + (user == Character.Controlled || Character.Controlled.CanSeeTarget(item))) { - Character.Controlled?.UpdateHUDProgressBar(targetItem, + Character.Controlled.UpdateHUDProgressBar(targetItem, targetItem.WorldPosition, targetItem.Condition / targetItem.MaxCondition, emptyColor: GUIStyle.HealthBarColorLow, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs index 0e3083684..4935c05f5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs @@ -200,7 +200,7 @@ namespace Barotrauma.Items.Components } #if CLIENT - if (requiredTime < float.MaxValue) + if (requiredTime < float.MaxValue && picker == Character.Controlled) { Character.Controlled?.UpdateHUDProgressBar( this, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index 950e8cfae..315f8cc71 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -134,7 +134,8 @@ namespace Barotrauma.Items.Components suitableProjectiles = element.GetAttributeIdentifierArray(nameof(suitableProjectiles), Array.Empty()).ToHashSet(); if (ReloadSkillRequirement > 0 && ReloadNoSkill <= reload) { - DebugConsole.AddWarning($"Invalid XML at {item.Name}: ReloadNoSkill is lower or equal than it's reload skill, despite having ReloadSkillRequirement."); + DebugConsole.AddWarning($"Invalid XML at {item.Name}: ReloadNoSkill is lower or equal than it's reload skill, despite having ReloadSkillRequirement.", + item.Prefab.ContentPackage); } InitProjSpecific(element); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index e5dfda8a4..4f0686cef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -137,7 +137,8 @@ namespace Barotrauma.Items.Components if (element.GetAttribute("limbfixamount") != null) { - DebugConsole.ThrowError("Error in item \"" + item.Name + "\" - RepairTool damage should be configured using a StatusEffect with Afflictions, not the limbfixamount attribute."); + DebugConsole.ThrowError("Error in item \"" + item.Name + "\" - RepairTool damage should be configured using a StatusEffect with Afflictions, not the limbfixamount attribute.", + contentPackage: element.ContentPackage); } fixableEntities = new HashSet(); @@ -149,7 +150,8 @@ namespace Barotrauma.Items.Components case "fixable": if (subElement.GetAttribute("name") != null) { - DebugConsole.ThrowError("Error in RepairTool " + item.Name + " - use identifiers instead of names to configure fixable entities."); + DebugConsole.ThrowError("Error in RepairTool " + item.Name + " - use identifiers instead of names to configure fixable entities.", + contentPackage: element.ContentPackage); fixableEntities.Add(subElement.GetAttribute("name").Value.ToIdentifier()); } else @@ -536,7 +538,7 @@ namespace Barotrauma.Items.Components { Vector2 displayPos = ConvertUnits.ToDisplayUnits(rayStart + (rayEnd - rayStart) * lastPickedFraction * 0.9f); if (item.CurrentHull.Submarine != null) { displayPos += item.CurrentHull.Submarine.Position; } - new FireSource(displayPos); + new FireSource(displayPos, sourceCharacter: user); } } } @@ -570,8 +572,15 @@ namespace Barotrauma.Items.Components structureFixAmount *= 1 + item.GetQualityModifier(Quality.StatType.RepairToolStructureDamageMultiplier); } + var didLeak = targetStructure.SectionIsLeakingFromOutside(sectionIndex); + targetStructure.AddDamage(sectionIndex, -structureFixAmount * degreeOfSuccess, user); + if (didLeak && !targetStructure.SectionIsLeakingFromOutside(sectionIndex)) + { + user.CheckTalents(AbilityEffectType.OnRepairedOutsideLeak); + } + //if the next section is small enough, apply the effect to it as well //(to make it easier to fix a small "left-over" section) for (int i = -1; i < 2; i += 2) @@ -658,9 +667,10 @@ namespace Barotrauma.Items.Components float addedDetachTime = deltaTime * (1f + user.GetStatValue(StatTypes.RepairToolDeattachTimeMultiplier)) * (1f + item.GetQualityModifier(Quality.StatType.RepairToolDeattachTimeMultiplier)); levelResource.DeattachTimer += addedDetachTime; #if CLIENT - if (targetItem.Prefab.ShowHealthBar) + if (targetItem.Prefab.ShowHealthBar && Character.Controlled != null && + (user == Character.Controlled || Character.Controlled.CanSeeTarget(item))) { - Character.Controlled?.UpdateHUDProgressBar( + Character.Controlled.UpdateHUDProgressBar( this, targetItem.WorldPosition, levelResource.DeattachTimer / levelResource.DeattachDuration, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs index 41d9c5df0..e972c855e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs @@ -38,7 +38,7 @@ namespace Barotrauma.Items.Components { if (aimPos == Vector2.Zero) { - aimPos = new Vector2(0.6f, 0.1f); + aimPos = new Vector2(0.45f, 0.1f); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index 8930f1603..16d6b788e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -60,12 +60,19 @@ namespace Barotrauma.Items.Components set { if (parent == value) { return; } - if (parent != null) { parent.OnActiveStateChanged -= SetActiveState; } - if (value != null) { value.OnActiveStateChanged += SetActiveState; } + if (InheritParentIsActive) + { + if (parent != null) { parent.OnActiveStateChanged -= SetActiveState; } + if (value != null) { value.OnActiveStateChanged += SetActiveState; } + } parent = value; } } + + [Serialize(true, IsPropertySaveable.No, description: "If this is a child component of another component, should this component inherit the IsActive state of the parent?")] + public bool InheritParentIsActive { get; set; } + public readonly ContentXElement originalElement; protected const float CorrectionDelay = 1.0f; @@ -77,6 +84,12 @@ namespace Barotrauma.Items.Components /// public virtual bool DontTransferInventoryBetweenSubs => false; + /// + /// If enabled, the items inside any of the item containers on this item cannot be sold at an outpost. + /// Use in similar cases as . + /// + public virtual bool DisallowSellingItemsFromContainer => false; + [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "How long it takes to pick up the item (in seconds).")] public float PickingTime { @@ -291,7 +304,8 @@ namespace Barotrauma.Items.Components } catch (Exception e) { - DebugConsole.ThrowError("Invalid select key in " + element + "!", e); + DebugConsole.ThrowError("Invalid select key in " + element + "!", e, + contentPackage: element.ContentPackage); } PickKey = InputType.Select; @@ -304,7 +318,8 @@ namespace Barotrauma.Items.Components } catch (Exception e) { - DebugConsole.ThrowError("Invalid pick key in " + element + "!", e); + DebugConsole.ThrowError("Invalid pick key in " + element + "!", e, + contentPackage: element.ContentPackage); } SerializableProperties = SerializableProperty.DeserializeProperties(this, element); @@ -316,7 +331,8 @@ namespace Barotrauma.Items.Components var component = item.Components.Find(ic => ic.Name.Equals(inheritRequiredSkillsFrom, StringComparison.OrdinalIgnoreCase)); if (component == null) { - DebugConsole.ThrowError($"Error in item \"{item.Name}\" - component \"{name}\" is set to inherit its required skills from \"{inheritRequiredSkillsFrom}\", but a component of that type couldn't be found."); + DebugConsole.ThrowError($"Error in item \"{item.Name}\" - component \"{name}\" is set to inherit its required skills from \"{inheritRequiredSkillsFrom}\", but a component of that type couldn't be found.", + contentPackage: element.ContentPackage); } else { @@ -331,7 +347,8 @@ namespace Barotrauma.Items.Components var component = item.Components.Find(ic => ic.Name.Equals(inheritStatusEffectsFrom, StringComparison.OrdinalIgnoreCase)); if (component == null) { - DebugConsole.ThrowError($"Error in item \"{item.Name}\" - component \"{name}\" is set to inherit its StatusEffects from \"{inheritStatusEffectsFrom}\", but a component of that type couldn't be found."); + DebugConsole.ThrowError($"Error in item \"{item.Name}\" - component \"{name}\" is set to inherit its StatusEffects from \"{inheritStatusEffectsFrom}\", but a component of that type couldn't be found.", + contentPackage: element.ContentPackage); } else if (component.statusEffectLists != null) { @@ -366,7 +383,8 @@ namespace Barotrauma.Items.Components case "requiredskills": if (subElement.GetAttribute("name") != null) { - DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - skill requirement in component " + GetType().ToString() + " should use a skill identifier instead of the name of the skill."); + DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - skill requirement in component " + GetType().ToString() + " should use a skill identifier instead of the name of the skill.", + contentPackage: element.ContentPackage); continue; } @@ -383,8 +401,11 @@ namespace Barotrauma.Items.Components if (ic == null) { break; } ic.Parent = this; - ic.IsActive = isActive; - OnActiveStateChanged += ic.SetActiveState; + if (ic.InheritParentIsActive) + { + ic.IsActive = isActive; + OnActiveStateChanged += ic.SetActiveState; + } item.AddComponent(ic); break; @@ -432,7 +453,8 @@ namespace Barotrauma.Items.Components } else if (!allowEmpty) { - DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - component " + GetType().ToString() + " requires an item with no identifiers."); + DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - component " + GetType().ToString() + " requires an item with no identifiers.", + contentPackage: element.ContentPackage); } } @@ -974,7 +996,8 @@ namespace Barotrauma.Items.Components { if (errorMessages) { - DebugConsole.ThrowError($"Could not find the component \"{typeName}\" ({item.Prefab.ContentFile.Path})"); + DebugConsole.ThrowError($"Could not find the component \"{typeName}\" ({item.Prefab.ContentFile.Path})", + contentPackage: element.ContentPackage); } return null; } @@ -983,7 +1006,8 @@ namespace Barotrauma.Items.Components { if (errorMessages) { - DebugConsole.ThrowError($"Could not find the component \"{typeName}\" ({item.Prefab.ContentFile.Path})", e); + DebugConsole.ThrowError($"Could not find the component \"{typeName}\" ({item.Prefab.ContentFile.Path})", e, + contentPackage: element.ContentPackage); } return null; } @@ -996,14 +1020,16 @@ namespace Barotrauma.Items.Components if (constructor == null) { DebugConsole.ThrowError( - $"Could not find the constructor of the component \"{typeName}\" ({item.Prefab.ContentFile.Path})"); + $"Could not find the constructor of the component \"{typeName}\" ({item.Prefab.ContentFile.Path})", + contentPackage: element.ContentPackage); return null; } } catch (Exception e) { DebugConsole.ThrowError( - $"Could not find the constructor of the component \"{typeName}\" ({item.Prefab.ContentFile.Path})", e); + $"Could not find the constructor of the component \"{typeName}\" ({item.Prefab.ContentFile.Path})", e, + contentPackage: element.ContentPackage); return null; } ItemComponent ic = null; @@ -1016,7 +1042,7 @@ namespace Barotrauma.Items.Components } catch (TargetInvocationException e) { - DebugConsole.ThrowError($"Error while loading component of the type {type}.", e.InnerException); + DebugConsole.ThrowError($"Error while loading component of the type {type}.", e.InnerException, contentPackage: element.ContentPackage); GameAnalyticsManager.AddErrorEventOnce( $"ItemComponent.Load:TargetInvocationException{item.Name}{element.Name}", GameAnalyticsManager.ErrorSeverity.Error, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index db2d49659..8a164684c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -236,6 +236,12 @@ namespace Barotrauma.Items.Components get => Inventory.AllItems.Count(it => it.Condition > 0.0f); } + public int ExtraStackSize + { + get => Inventory.ExtraStackSize; + set => Inventory.ExtraStackSize = value; + } + private readonly ImmutableArray slotRestrictions; readonly List targets = new List(); @@ -284,7 +290,8 @@ namespace Barotrauma.Items.Components RelatedItem containable = RelatedItem.Load(subElement, returnEmpty: false, parentDebugName: item.Name); if (containable == null) { - DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - containable with no identifiers."); + DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - containable with no identifiers.", + contentPackage: element.ContentPackage); continue; } ContainableItems ??= new List(); @@ -297,7 +304,10 @@ namespace Barotrauma.Items.Components } } Inventory = new ItemInventory(item, this, totalCapacity, SlotsPerRow); - + + // we have to assign this here because the fields are serialized before the inventory is created otherwise + ExtraStackSize = element.GetAttributeInt(nameof(ExtraStackSize), 0); + List newSlotRestrictions = new List(totalCapacity); for (int i = 0; i < capacity; i++) { @@ -321,7 +331,8 @@ namespace Barotrauma.Items.Components RelatedItem containable = RelatedItem.Load(subSubElement, returnEmpty: false, parentDebugName: item.Name); if (containable == null) { - DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - containable with no identifiers."); + DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - containable with no identifiers.", + contentPackage: element.ContentPackage); continue; } subContainableItems.Add(containable); @@ -349,7 +360,8 @@ namespace Barotrauma.Items.Components RelatedItem containable = RelatedItem.Load(subElement, returnEmpty: false, parentDebugName: item.Name); if (containable == null) { - DebugConsole.ThrowError("Error when loading containable restrictions for \"" + item.Name + "\" - containable with no identifiers."); + DebugConsole.ThrowError("Error when loading containable restrictions for \"" + item.Name + "\" - containable with no identifiers.", + contentPackage: element.ContentPackage); continue; } ContainableItems[containableIndex] = containable; @@ -386,6 +398,7 @@ namespace Barotrauma.Items.Components public void OnItemContained(Item containedItem) { int index = Inventory.FindIndex(containedItem); + RelatedItem relatedItem = null; if (index >= 0 && index < slotRestrictions.Length) { if (slotRestrictions[index].ContainableItems != null) @@ -394,6 +407,8 @@ namespace Barotrauma.Items.Components foreach (var containableItem in slotRestrictions[index].ContainableItems) { if (!containableItem.MatchesItem(containedItem)) { continue; } + //the 1st matching ContainableItem of the slot determines the hiding, position and rotation of the item + relatedItem ??= containableItem; foreach (StatusEffect effect in containableItem.StatusEffects) { activeContainedItems.Add(new ActiveContainedItem(containedItem, effect, containableItem.ExcludeBroken, containableItem.ExcludeFullCondition)); @@ -402,7 +417,6 @@ namespace Barotrauma.Items.Components } } - var relatedItem = FindContainableItem(containedItem); var containedItemInfo = new ContainedItem(containedItem, Hide: relatedItem?.Hide ?? false, ItemPos: relatedItem?.ItemPos, @@ -783,12 +797,9 @@ namespace Barotrauma.Items.Components private RelatedItem FindContainableItem(Item item) { - var relatedItem = ContainableItems?.FirstOrDefault(ci => ci.MatchesItem(item)); - if (relatedItem == null && AllSubContainableItems != null) - { - relatedItem = AllSubContainableItems.FirstOrDefault(ci => ci.MatchesItem(item)); - } - return relatedItem; + int index = Inventory.FindIndex(item); + if (index == -1 ) { return null; } + return slotRestrictions[index]?.ContainableItems?.FirstOrDefault(ci => ci.MatchesItem(item)); } /// @@ -1092,6 +1103,7 @@ namespace Barotrauma.Items.Components itemIds[i].Add(idRemap.GetOffsetId(id)); } } + ExtraStackSize = componentElement.GetAttributeInt(nameof(ExtraStackSize), 0); } public override XElement Save(XElement parentElement) @@ -1104,6 +1116,7 @@ namespace Barotrauma.Items.Components itemIdStrings[i] = string.Join(';', items.Select(it => it.ID.ToString())); } componentElement.Add(new XAttribute("contained", string.Join(',', itemIdStrings))); + componentElement.Add(new XAttribute(nameof(ExtraStackSize), ExtraStackSize)); return componentElement; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index caa19f47c..7b04931f6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -622,7 +622,7 @@ namespace Barotrauma.Items.Components return element; } - private void LoadLimbPositions(XElement element) + private void LoadLimbPositions(ContentXElement element) { limbPositions.Clear(); foreach (var subElement in element.Elements()) @@ -631,7 +631,8 @@ namespace Barotrauma.Items.Components string limbStr = subElement.GetAttributeString("limb", ""); if (!Enum.TryParse(subElement.GetAttribute("limb").Value, out LimbType limbType)) { - DebugConsole.ThrowError($"Error in item \"{item.Name}\" - {limbStr} is not a valid limb type."); + DebugConsole.ThrowError($"Error in item \"{item.Name}\" - {limbStr} is not a valid limb type.", + contentPackage: element.ContentPackage); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index d5a5368ee..e48ec1934 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -104,7 +104,7 @@ namespace Barotrauma.Items.Components // doesn't quite work properly, remaining time changes if tinkering stops float deconstructionSpeedModifier = userDeconstructorSpeedMultiplier * (1f + tinkeringStrength * TinkeringSpeedIncrease); - float deconstructionSpeed = item.StatManager.GetAdjustedValue(ItemTalentStats.DeconstructorSpeed, DeconstructionSpeed); + float deconstructionSpeed = item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.DeconstructorSpeed, DeconstructionSpeed); if (DeconstructItemsSimultaneously) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs index 0796cd686..311da9b7b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs @@ -149,13 +149,13 @@ namespace Barotrauma.Items.Components { forceMultiplier *= MathHelper.Lerp(0.5f, 2.0f, (float)Math.Sqrt(User.GetSkillLevel("helm") / 100)); } - currForce *= item.StatManager.GetAdjustedValue(ItemTalentStats.EngineMaxSpeed, MaxForce) * forceMultiplier; + currForce *= item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.EngineMaxSpeed, MaxForce) * forceMultiplier; if (item.GetComponent() is { IsTinkering: true } repairable) { currForce *= 1f + repairable.TinkeringStrength * TinkeringForceIncrease; } - currForce = item.StatManager.GetAdjustedValue(ItemTalentStats.EngineSpeed, currForce); + currForce = item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.EngineSpeed, currForce); //less effective when in a bad condition currForce *= MathHelper.Lerp(0.5f, 2.0f, condition); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index 0ef1b1dc2..63de9cce6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -101,7 +101,8 @@ namespace Barotrauma.Items.Components { if (subElement.Name.ToString().Equals("fabricableitem", StringComparison.OrdinalIgnoreCase)) { - DebugConsole.ThrowError("Error in item " + item.Name + "! Fabrication recipes should be defined in the craftable item's xml, not in the fabricator."); + DebugConsole.ThrowError("Error in item " + item.Name + "! Fabrication recipes should be defined in the craftable item's xml, not in the fabricator.", + contentPackage: element.ContentPackage); break; } } @@ -119,12 +120,16 @@ namespace Barotrauma.Items.Components } } + //the errors below may be caused by a mod overriding a base item instead of this one, log the package of the base item in that case + var packageToLog = itemPrefab.GetParentModPackageOrThisPackage(); + bool recipeInvalid = false; foreach (var requiredItem in recipe.RequiredItems) { if (requiredItem.ItemPrefabs.None()) { - DebugConsole.ThrowError($"Error in the fabrication recipe for \"{itemPrefab.Name}\". Could not find the ingredient \"{requiredItem}\"."); + DebugConsole.ThrowError($"Error in the fabrication recipe for \"{itemPrefab.Name}\". Could not find the ingredient \"{requiredItem}\".", + contentPackage: packageToLog); recipeInvalid = true; } } @@ -132,7 +137,8 @@ namespace Barotrauma.Items.Components if (fabricationRecipes.TryGetValue(recipe.RecipeHash, out var duplicateRecipe)) { - DebugConsole.ThrowError($"Error in the fabrication recipe for \"{itemPrefab.Name}\". Duplicate recipe in \"{duplicateRecipe.TargetItem.Identifier}\"."); + DebugConsole.ThrowError($"Error in the fabrication recipe for \"{itemPrefab.Name}\". Duplicate recipe in \"{duplicateRecipe.TargetItem.Identifier}\".", + contentPackage: packageToLog); continue; } fabricationRecipes.Add(recipe.RecipeHash, recipe); @@ -463,7 +469,7 @@ namespace Barotrauma.Items.Components character.CheckTalents(AbilityEffectType.OnAllyItemFabricatedAmount, fabricationitemAmount); } user.CheckTalents(AbilityEffectType.OnItemFabricatedAmount, fabricationitemAmount); - quality = GetFabricatedItemQuality(fabricatedItem, user); + quality = GetFabricatedItemQuality(fabricatedItem, user).RollQuality(); } int amount = (int)fabricationitemAmount.Value; @@ -528,12 +534,10 @@ namespace Barotrauma.Items.Components { foreach (Skill skill in fabricatedItem.RequiredSkills) { - float userSkill = user.GetSkillLevel(skill.Identifier); - float addedSkill = skill.Level * SkillSettings.Current.SkillIncreasePerFabricatorRequiredSkill / Math.Max(userSkill, 1.0f); + float addedSkill = skill.Level * SkillSettings.Current.SkillIncreasePerFabricatorRequiredSkill; var addedSkillValue = new AbilityFabricatorSkillGain(skill.Identifier, addedSkill); user.CheckTalents(AbilityEffectType.OnItemFabricationSkillGain, addedSkillValue); - - user.Info.IncreaseSkillLevel( + user.Info.ApplySkillGain( skill.Identifier, addedSkillValue.Value); } @@ -570,10 +574,52 @@ namespace Barotrauma.Items.Components return currPowerConsumption; } - private static int GetFabricatedItemQuality(FabricationRecipe fabricatedItem, Character user) + public static float CalculateBonusRollPercentage(float skillLevel, float target) + => Math.Clamp((skillLevel - target) / (100f - target) * 100f, min: 0, max: 100); + + public readonly record struct QualityResult(int Quality, float PlusOnePercentage, float PlusTwoPercentage) { - if (user?.Info == null) { return 0; } - if (fabricatedItem.TargetItem.ConfigElement.GetChildElement("Quality") == null) { return 0; } + public static readonly QualityResult Empty = new QualityResult(0, 0, 0); + + public bool HasRandomQualityRollChance => PlusOnePercentage > 0f || PlusTwoPercentage > 0f; + + // The total real world percentage for a roll to succeed, taking into account that +1 needs to succeed for +2 to be attempted and + // that the chance for only +1 goes down as +2 increases since some of the +1's will turn into +2s + public float TotalPlusOnePercentage => Math.Clamp(PlusOnePercentage * (100f - PlusTwoPercentage) / 100f, min: 0, max: 100); + public float TotalPlusTwoPercentage => Math.Clamp(PlusOnePercentage * PlusTwoPercentage / 100f, min: 0, max: 100); + + public int RollQuality() + { + int additionalQuality = 0; + if (Roll(PlusOnePercentage)) + { + additionalQuality++; + if (Roll(PlusTwoPercentage)) + { + additionalQuality++; + } + } + + return Quality + additionalQuality; + + static bool Roll(float percentage) + => percentage >= Rand.Range(0, 100, Rand.RandSync.Unsynced); + } + } + + public const int PlusOneQualityBonusThreshold = 50, + PlusTwoQualityBonusThreshold = 75; + + public const int PlusOneTarget = 100, + PlusTwoTarget = 125; + + public const float PlusOneLerp = 0.2f, + PlusTwoLerp = 0.4f; + + private static QualityResult GetFabricatedItemQuality(FabricationRecipe fabricatedItem, Character user) + { + if (user?.Info == null) { return QualityResult.Empty; } + if (fabricatedItem.TargetItem.ConfigElement.GetChildElement("Quality") == null) { return QualityResult.Empty; } int quality = 0; float floatQuality = 0.0f; floatQuality += user.GetStatValue(StatTypes.IncreaseFabricationQuality, includeSaved: false); @@ -587,34 +633,63 @@ namespace Barotrauma.Items.Components } quality = (int)floatQuality; - const int MaxCraftingSkill = 100; + // Use Option here instead of 0 because we want the lowest value and a value of 0 would always be lower than any other chance + Option plusOne = Option.None, + plusTwo = Option.None; - //having a higher-than-100 skill (e.g. due to talents) gives +1 quality - quality += fabricatedItem.RequiredSkills.All(s => user.GetSkillLevel(s.Identifier) >= MaxCraftingSkill) ? 1 : 0; foreach (var skill in fabricatedItem.RequiredSkills) { - //+1 quality if the character's skill level is >20% from the min requirement towards max skill - //e.g. if the skill requirement is 10 -> 28 - //40 -> 52 - //90 -> 92 - float skillRequirement = MathHelper.Lerp(skill.Level, MaxCraftingSkill, 0.2f); - if (user.GetSkillLevel(skill.Identifier) > skillRequirement) + float skillLevel = user.GetSkillLevel(skill.Identifier); + + if (skillLevel >= PlusOneQualityBonusThreshold) { - quality += 1; + //+1 quality chance if the character's skill level is >20% from the min requirement towards max skill as well as higher than 50 + //e.g. if the skill requirement is 10 -> 28 (but minimum 50 threshold) + //40 -> 52 + //90 -> 92 + var bonusChance1 = CalculateBonusRollPercentage(skillLevel, MathHelper.Lerp(skill.Level, PlusOneTarget, PlusOneLerp)); + plusOne = OverrideChanceIfLess(plusOne, bonusChance1); + + if (skillLevel >= PlusTwoQualityBonusThreshold) + { + var bonusChance2 = CalculateBonusRollPercentage(skillLevel, MathHelper.Lerp(skill.Level, PlusTwoTarget, PlusTwoLerp)); + plusTwo = OverrideChanceIfLess(plusTwo, bonusChance2); + } + else + { + break; + } + } + else + { + break; + } + + static Option OverrideChanceIfLess(Option original, float bonusChance) + { + if (original.TryUnwrap(out var originalChance)) + { + return originalChance > bonusChance ? Option.Some(bonusChance) : original; + } + + return Option.Some(bonusChance); } } - return quality; + + return new QualityResult(quality, + PlusOnePercentage: plusOne.Match(some: static f => f, none: static () => 0f), + PlusTwoPercentage: plusTwo.Match(some: static f => f, none: static () => 0f)); } partial void UpdateRequiredTimeProjSpecific(); private static bool AnyOneHasRecipeForItem(Character user, ItemPrefab item) { - return + return (user != null && user.HasRecipeForItem(item.Identifier)) || GameSession.GetSessionCrewCharacters(CharacterType.Bot).Any(c => c.HasRecipeForItem(item.Identifier)); } - + private bool CanBeFabricated(FabricationRecipe fabricableItem, IReadOnlyDictionary> availableIngredients, Character character) { if (fabricableItem == null) { return false; } @@ -692,7 +767,7 @@ namespace Barotrauma.Items.Components //fabricating takes 100 times longer if degree of success is close to 0 //characters with a higher skill than required can fabricate up to 100% faster - float time = fabricableItem.RequiredTime / item.StatManager.GetAdjustedValue(ItemTalentStats.FabricationSpeed, FabricationSpeed) / MathHelper.Clamp(t, 0.01f, 2.0f); + float time = fabricableItem.RequiredTime / item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.FabricationSpeed, FabricationSpeed) / MathHelper.Clamp(t, 0.01f, 2.0f); if (user?.Info is { } info && fabricableItem.TargetItem is { } it) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index a6c4704cc..6d51be758 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -138,14 +138,14 @@ namespace Barotrauma.Items.Components float powerFactor = Math.Min(currPowerConsumption <= 0.0f || MinVoltage <= 0.0f ? 1.0f : Voltage, MaxOverVoltageFactor); - currFlow = flowPercentage / 100.0f * item.StatManager.GetAdjustedValue(ItemTalentStats.PumpMaxFlow, MaxFlow) * powerFactor; + currFlow = flowPercentage / 100.0f * item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.PumpMaxFlow, MaxFlow) * powerFactor; if (item.GetComponent() is { IsTinkering: true } repairable) { currFlow *= 1f + repairable.TinkeringStrength * TinkeringSpeedIncrease; } - currFlow = item.StatManager.GetAdjustedValue(ItemTalentStats.PumpSpeed, currFlow); + currFlow = item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.PumpSpeed, currFlow); //less effective when in a bad condition currFlow *= MathHelper.Lerp(0.5f, 1.0f, item.Condition / item.MaxCondition); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs index 74aec6ffc..84c5d4d11 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs @@ -875,7 +875,7 @@ namespace Barotrauma.Items.Components } } - private float GetMaxOutput() => item.StatManager.GetAdjustedValue(ItemTalentStats.ReactorMaxOutput, MaxPowerOutput); - private float GetFuelConsumption() => item.StatManager.GetAdjustedValue(ItemTalentStats.ReactorFuelConsumption, fuelConsumptionRate); + private float GetMaxOutput() => item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.ReactorMaxOutput, MaxPowerOutput); + private float GetFuelConsumption() => item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.ReactorFuelConsumption, fuelConsumptionRate); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index f2c4b506b..9784e92b7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -336,8 +336,7 @@ namespace Barotrauma.Items.Components { showIceSpireWarning = false; if (user != null && user.Info != null && - user.SelectedItem == item && - controlledSub != null && controlledSub.Velocity.LengthSquared() > 0.01f) + user.SelectedItem == item) { IncreaseSkillLevel(user, deltaTime); } @@ -402,14 +401,15 @@ namespace Barotrauma.Items.Components private void IncreaseSkillLevel(Character user, float deltaTime) { + if (controlledSub == null) { return; } + if (controlledSub.Velocity.LengthSquared() < 0.01f) { return; } if (user?.Info == null) { return; } // Do not increase the helm skill when "steering" the sub while docked into something static (e.g. outpost or wreck) - if (GameMain.GameSession?.Campaign != null && controlledSub != null && controlledSub.DockedTo.Any(d => d.PhysicsBody.BodyType == BodyType.Static)) { return; } + if (GameMain.GameSession?.Campaign != null&& controlledSub.DockedTo.Any(d => d.PhysicsBody.BodyType == BodyType.Static)) { return; } - float userSkill = Math.Max(user.GetSkillLevel("helm"), 1.0f) / 100.0f; - user.Info.IncreaseSkillLevel( - "helm".ToIdentifier(), - SkillSettings.Current.SkillIncreasePerSecondWhenSteering / userSkill * deltaTime); + float speedMultiplier = MathHelper.Clamp(TargetVelocity.Length() / 100.0f, 0.0f, 1.0f); + user.Info.ApplySkillGain(Tags.HelmSkill, + SkillSettings.Current.SkillIncreasePerSecondWhenSteering * speedMultiplier * deltaTime); } private void UpdateAutoPilot(float deltaTime) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs index 8881ce000..8e17e7a44 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs @@ -395,6 +395,6 @@ namespace Barotrauma.Items.Components } } - public float GetCapacity() => item.StatManager.GetAdjustedValue(ItemTalentStats.BatteryCapacity, Capacity); + public float GetCapacity() => item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.BatteryCapacity, Capacity); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs index f023ff870..93d47b5c7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs @@ -407,7 +407,7 @@ namespace Barotrauma.Items.Components } } - if (!(this is RelayComponent)) + if (this is not RelayComponent) { if (PowerConnections.Any(p => !p.IsOutput) && PowerConnections.Any(p => p.IsOutput)) { @@ -454,6 +454,7 @@ namespace Barotrauma.Items.Components base.RemoveComponentSpecific(); connectedRecipients?.Clear(); connectionDirty?.Clear(); + recipientsToRefresh.Clear(); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index 2af67598e..9b3444649 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -306,7 +306,8 @@ namespace Barotrauma.Items.Components if (item.body == null) { - DebugConsole.ThrowError($"Error in projectile definition ({item.Name}): No body defined!"); + DebugConsole.ThrowError($"Error in projectile definition ({item.Name}): No body defined!", + contentPackage: element.ContentPackage); return; } @@ -1016,9 +1017,10 @@ namespace Barotrauma.Items.Components { attackResult = Attack.DoDamage(User ?? Attacker, targetItem, item.WorldPosition, 1.0f); #if CLIENT - if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar) + if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar && Character.Controlled != null && + (User == Character.Controlled || Character.Controlled.CanSeeTarget(item))) { - Character.Controlled?.UpdateHUDProgressBar(targetItem, + Character.Controlled.UpdateHUDProgressBar(targetItem, targetItem.WorldPosition, targetItem.Condition / targetItem.MaxCondition, emptyColor: GUIStyle.HealthBarColorLow, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs index a41e1e1c8..03b727bc3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs @@ -60,7 +60,8 @@ namespace Barotrauma.Items.Components string statTypeString = subElement.GetAttributeString("stattype", ""); if (!Enum.TryParse(statTypeString, true, out StatType statType)) { - DebugConsole.ThrowError("Invalid stat type type \"" + statTypeString + "\" in item (" + ((MapEntity)item).Prefab.Identifier + ")"); + DebugConsole.ThrowError("Invalid stat type type \"" + statTypeString + "\" in item (" + ((MapEntity)item).Prefab.Identifier + ")", + contentPackage: element.ContentPackage); } float statValue = subElement.GetAttributeFloat("value", 0f); statValues.TryAdd(statType, statValue); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index 93721c36f..873325957 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -495,9 +495,7 @@ namespace Barotrauma.Items.Components { foreach (Skill skill in requiredSkills) { - float characterSkillLevel = CurrentFixer.GetSkillLevel(skill.Identifier); - CurrentFixer.Info?.IncreaseSkillLevel(skill.Identifier, - SkillSettings.Current.SkillIncreasePerRepair / Math.Max(characterSkillLevel, 1.0f)); + CurrentFixer.Info?.ApplySkillGain(skill.Identifier, SkillSettings.Current.SkillIncreasePerRepair); } SteamAchievementManager.OnItemRepaired(item, CurrentFixer); CurrentFixer.CheckTalents(AbilityEffectType.OnRepairComplete, new AbilityRepairable(item)); @@ -570,7 +568,7 @@ namespace Barotrauma.Items.Components if (item.ConditionPercentage > MinDeteriorationCondition) { - float deteriorationSpeed = item.StatManager.GetAdjustedValue(ItemTalentStats.DetoriationSpeed, DeteriorationSpeed); + float deteriorationSpeed = item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.DetoriationSpeed, DeteriorationSpeed); if (ForceDeteriorationTimer > 0.0f) { deteriorationSpeed = Math.Max(deteriorationSpeed, 1.0f); } item.Condition -= deteriorationSpeed * deltaTime; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Scanner.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Scanner.cs index 1d592e015..bd84d052f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Scanner.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Scanner.cs @@ -82,7 +82,8 @@ namespace Barotrauma.Items.Components Holdable = item.GetComponent(); if (Holdable == null || !Holdable.Attachable) { - DebugConsole.ThrowError("Error in initializing a Scanner component: an attachable Holdable component is required on the same item and none was found"); + DebugConsole.ThrowError("Error in initializing a Scanner component: an attachable Holdable component is required on the same item and none was found", + contentPackage: item.Prefab.ContentPackage); IsActive = false; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs index c7fd99f51..6902b3b5e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs @@ -26,7 +26,8 @@ namespace Barotrauma.Items.Components RequiredSignalCount = element.GetChildElements("TerminalButton").Count(c => c.GetAttribute("style") != null); if (RequiredSignalCount < 1) { - DebugConsole.ThrowError($"Error in item \"{item.Name}\": no TerminalButton elements defined for the ButtonTerminal component!"); + DebugConsole.ThrowError($"Error in item \"{item.Name}\": no TerminalButton elements defined for the ButtonTerminal component!", + contentPackage: element.ContentPackage); } InitProjSpecific(element); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs index 2cfd5b3da..69c395e3c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs @@ -29,6 +29,9 @@ namespace Barotrauma.Items.Components // We don't want the components and wires to transfer between subs as it would cause issues. public override bool DontTransferInventoryBetweenSubs => true; + // We don't want to sell the components and wires inside the circuit box + public override bool DisallowSellingItemsFromContainer => true; + public Option FindInputOutputConnection(Identifier connectionName) { foreach (CircuitBoxInputConnection input in Inputs) @@ -154,7 +157,7 @@ namespace Barotrauma.Items.Components delayedElementToLoad = Option.None; } - private void LoadFromXML(ContentXElement loadElement) + public void LoadFromXML(ContentXElement loadElement) { foreach (var subElement in loadElement.Elements()) { @@ -395,8 +398,8 @@ namespace Barotrauma.Items.Components { Components.Add(new CircuitBoxComponent(id, spawnedItem, pos, this, usedResource)); onItemSpawned?.Invoke(spawnedItem); + OnViewUpdateProjSpecific(); }); - OnViewUpdateProjSpecific(); return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs index 82c21a973..b1424074a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs @@ -383,6 +383,12 @@ namespace Barotrauma.Items.Components wire.RemoveConnection(item); } } + c.Grid = null; + } + foreach (var connection in Connections) + { + Powered.ChangedConnections.Remove(connection); + connection.Recipients.Clear(); } Connections.Clear(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index f405963c9..ec26fafb9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -305,16 +305,17 @@ namespace Barotrauma.Items.Components (statusEffectLists == null || !statusEffectLists.ContainsKey(ActionType.OnActive)) && (IsActiveConditionals == null || IsActiveConditionals.Count == 0)) { - if (item.body != null && !item.body.Enabled) - { - lightBrightness = 0.0f; - SetLightSourceState(false, 0.0f); - } - else + if (item.body == null || item.body.Enabled || + (item.ParentInventory is ItemInventory itemInventory && !itemInventory.Container.HideItems)) { lightBrightness = 1.0f; SetLightSourceState(true, lightBrightness); } + else + { + lightBrightness = 0.0f; + SetLightSourceState(false, 0.0f); + } isOn = true; SetLightSourceTransformProjSpecific(); base.IsActive = false; @@ -341,8 +342,22 @@ namespace Barotrauma.Items.Components #if CLIENT Light.ParentSub = item.Submarine; #endif + + + bool visibleInContainer; var ownerCharacter = item.GetRootInventoryOwner() as Character; - if ((item.Container != null && ownerCharacter == null) || + if (ownerCharacter != null && item.RootContainer?.GetComponent() is not { IsActive: true }) + { + //if the item is in a character inventory, the light should only be visible if the character is holding the item + //(not if it's e.q. inside a wearable item, or in a rifle worn on the back) + visibleInContainer = false; + } + else + { + visibleInContainer = item.FindParentInventory(static it => it is ItemInventory { Container.HideItems: true }) == null; + } + + if ((item.Container != null && !visibleInContainer && ownerCharacter == null) || (ownerCharacter != null && ownerCharacter.InvisibleTimer > 0.0f)) { lightBrightness = 0.0f; @@ -352,7 +367,7 @@ namespace Barotrauma.Items.Components SetLightSourceTransformProjSpecific(); PhysicsBody body = ParentBody ?? item.body; - if (body != null && !body.Enabled) + if (body != null && !body.Enabled && !visibleInContainer) { lightBrightness = 0.0f; SetLightSourceState(false, 0.0f); @@ -432,6 +447,11 @@ namespace Barotrauma.Items.Components target.SightRange = Math.Max(target.SightRange, target.MaxSightRange * lightBrightness); } + public override void Drop(Character dropper, bool setTransform = true) + { + SetLightSourceTransform(); + } + partial void SetLightSourceState(bool enabled, float brightness); public void SetLightSourceTransform() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs index 85e995e5f..8f1228208 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs @@ -97,7 +97,8 @@ namespace Barotrauma.Items.Components string triggeredByAttribute = element.GetAttributeString("triggeredby", "Character"); if (!Enum.TryParse(triggeredByAttribute, out triggeredBy)) { - DebugConsole.ThrowError($"Error in ForceComponent config: \"{triggeredByAttribute}\" is not a valid triggerer type."); + DebugConsole.ThrowError($"Error in ForceComponent config: \"{triggeredByAttribute}\" is not a valid triggerer type.", + contentPackage: element.ContentPackage); } triggerOnce = element.GetAttributeBool("triggeronce", false); string parentDebugName = $"TriggerComponent in {item.Name}"; @@ -274,6 +275,15 @@ namespace Barotrauma.Items.Components } } + protected override void RemoveComponentSpecific() + { + if (PhysicsBody != null) + { + PhysicsBody.Remove(); + PhysicsBody = null; + } + } + public override void ReceiveSignal(Signal signal, Connection connection) { base.ReceiveSignal(signal, connection); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index 0ad0227b2..c89a073aa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -61,6 +61,22 @@ namespace Barotrauma.Items.Components private const float CrewAiFindTargetMaxInterval = 1.0f; private const float CrewAIFindTargetMinInverval = 0.2f; + /// + /// Bots consider the projectile to move at least this fast when calculating how far ahead a moving target they need to aim. + /// Aiming ahead doesn't work reliably with very slow projectiles, because we'd need to take into account drag and gravity, + /// and the target would most likely move in a different direction anyway before the projectile reaches it. + /// + private const float MinimumProjectileVelocityForAimAhead = 20.0f; + + /// + /// Bots don't try to aim ahead a moving target by more than this amount. If the target is very fast and/or the projectile very slow, + /// we'd need to aim so far ahead it'd most likely fail anyway. + /// + private const float MaximumAimAhead = 10.0f; + + private float projectileSpeed; + private Item previousAmmo; + private int currentLoaderIndex; private const float TinkeringPowerCostReduction = 0.2f; @@ -563,8 +579,9 @@ namespace Barotrauma.Items.Components // Do not increase the weapons skill when operating a turret in an outpost level if (user?.Info != null && (GameMain.GameSession?.Campaign == null || !Level.IsLoadedFriendlyOutpost)) { - user.Info.IncreaseSkillLevel("weapons".ToIdentifier(), - SkillSettings.Current.SkillIncreasePerSecondWhenOperatingTurret * deltaTime / Math.Max(user.GetSkillLevel("weapons"), 1.0f)); + user.Info.ApplySkillGain( + Tags.WeaponsSkill, + SkillSettings.Current.SkillIncreasePerSecondWhenOperatingTurret * deltaTime); } float rotMidDiff = MathHelper.WrapAngle(rotation - (minRotation + maxRotation) / 2.0f); @@ -664,6 +681,11 @@ namespace Barotrauma.Items.Components return GetAvailableInstantaneousBatteryPower() >= GetPowerRequiredToShoot(); } + private Vector2 GetBarrelDir() + { + return new Vector2((float)Math.Cos(rotation), -(float)Math.Sin(rotation)); + } + private bool TryLaunch(float deltaTime, Character character = null, bool ignorePower = false) { tryingToCharge = true; @@ -709,7 +731,8 @@ namespace Barotrauma.Items.Components ItemContainer projectileContainer = projectiles.First().Item.Container?.GetComponent(); if (projectileContainer != null && projectileContainer.Item != item) { - projectileContainer?.Item.Use(deltaTime); + //user needs to be null because the ammo boxes shouldn't be directly usable by characters + projectileContainer?.Item.Use(deltaTime, user: null, userForOnUsedEvent: user); } } else @@ -735,7 +758,7 @@ namespace Barotrauma.Items.Components ItemContainer projectileContainer = containerItem.GetComponent(); if (projectileContainer != null) { - containerItem.Use(deltaTime); + containerItem.Use(deltaTime, user: null, userForOnUsedEvent: user); projectiles = GetLoadedProjectiles(); if (projectiles.Any()) { return true; } } @@ -930,6 +953,7 @@ namespace Barotrauma.Items.Components Projectile projectileComponent = projectile.GetComponent(); if (projectileComponent != null) { + TryDetermineProjectileSpeed(projectileComponent); projectileComponent.Launcher = item; projectileComponent.Attacker = projectileComponent.User = user; if (projectileComponent.Attack != null) @@ -960,6 +984,16 @@ namespace Barotrauma.Items.Components LaunchProjSpecific(); } + private void TryDetermineProjectileSpeed(Projectile projectile) + { + if (projectile != null && !projectile.Hitscan) + { + projectileSpeed = + ConvertUnits.ToDisplayUnits( + MathHelper.Clamp((projectile.LaunchImpulse + LaunchImpulse) / projectile.Item.body.Mass, MinimumProjectileVelocityForAimAhead, NetConfig.MaxPhysicsBodyVelocity)); + } + } + partial void LaunchProjSpecific(); private static void ShiftItemsInProjectileContainer(ItemContainer container) @@ -1143,7 +1177,7 @@ namespace Barotrauma.Items.Components if (target is Hull targetHull) { - Vector2 barrelDir = new Vector2((float)Math.Cos(rotation), -(float)Math.Sin(rotation)); + Vector2 barrelDir = GetBarrelDir(); if (!MathUtils.GetLineRectangleIntersection(item.WorldPosition, item.WorldPosition + barrelDir * AIRange, targetHull.WorldRect, out _)) { return; @@ -1244,8 +1278,24 @@ namespace Barotrauma.Items.Components if (container != null) { maxProjectileCount += container.Capacity; - int projectiles = projectileContainer.ContainedItems.Count(it => it.Condition > 0.0f); - usableProjectileCount += projectiles; + var projectiles = projectileContainer.ContainedItems.Where(it => it.Condition > 0.0f); + var firstProjectile = projectiles.FirstOrDefault(); + + if (firstProjectile?.Prefab != previousAmmo?.Prefab) + { + //assume the projectiles are infinitely fast (no aiming ahead of the target) if we can't find projectiles to calculate the speed based on, + //and if the projectile type isn't the same as before + projectileSpeed = float.PositiveInfinity; + } + previousAmmo = firstProjectile; + if (projectiles.Any()) + { + var projectile = + firstProjectile.GetComponent() ?? + firstProjectile.ContainedItems.FirstOrDefault()?.GetComponent(); + TryDetermineProjectileSpeed(projectile); + usableProjectileCount += projectiles.Count(); + } } } } @@ -1409,6 +1459,7 @@ namespace Barotrauma.Items.Components targetPos = currentTarget.WorldPosition; } bool iceSpireSpotted = false; + Vector2 targetVelocity = Vector2.Zero; // Adjust the target character position (limb or submarine) if (currentTarget is Character targetCharacter) { @@ -1424,20 +1475,39 @@ namespace Barotrauma.Items.Components else { // Target the closest limb. Doesn't make much difference with smaller creatures, but enables the bots to shoot longer abyss creatures like the endworm. Otherwise they just target the main body = head. - float closestDist = closestDistance; + float closestDistSqr = closestDistance; foreach (Limb limb in targetCharacter.AnimController.Limbs) { if (limb.IsSevered) { continue; } if (limb.Hidden) { continue; } if (!IsWithinAimingRadius(limb.WorldPosition)) { continue; } - float dist = Vector2.DistanceSquared(limb.WorldPosition, item.WorldPosition); - if (dist < closestDist) + float distSqr = Vector2.DistanceSquared(limb.WorldPosition, item.WorldPosition); + if (distSqr < closestDistSqr) { - closestDist = dist; + closestDistSqr = distSqr; + if (limb == targetCharacter.AnimController.MainLimb) + { + //prefer main limb (usually a much better target than the extremities that are often the closest limbs) + closestDistSqr *= 0.5f; + } targetPos = limb.WorldPosition; } } - if (closestDist > shootDistance * shootDistance) + if (projectileSpeed < float.PositiveInfinity && targetPos.HasValue) + { + //lead the target (aim where the target will be in the future) + float dist = MathF.Sqrt(closestDistSqr); + float projectileMovementTime = dist / projectileSpeed; + + targetVelocity = targetCharacter.AnimController.Collider.LinearVelocity; + Vector2 movementAmount = targetVelocity * projectileMovementTime; + //don't try to compensate more than 10 meters - if the target is so fast or the projectile so slow we need to go beyond that, + //it'd most likely fail anyway + movementAmount = ConvertUnits.ToDisplayUnits(movementAmount.ClampLength(MaximumAimAhead)); + Vector2 futurePosition = targetPos.Value + movementAmount; + targetPos = Vector2.Lerp(targetPos.Value, futurePosition, DegreeOfSuccess(character)); + } + if (closestDistSqr > shootDistance * shootDistance) { aiFindTargetTimer = CrewAIFindTargetMinInverval; ResetTarget(); @@ -1512,7 +1582,9 @@ namespace Barotrauma.Items.Components if (targetPos == null) { return false; } // Force the highest priority so that we don't change the objective while targeting enemies. objective.ForceHighestPriority = true; - +#if CLIENT + debugDrawTargetPos = targetPos.Value; +#endif if (closestEnemy != null && character.AIController.SelectedAiTarget != closestEnemy.AiTarget) { if (character.IsOnPlayerTeam) @@ -1563,8 +1635,28 @@ namespace Barotrauma.Items.Components if (IsPointingTowards(targetPos.Value)) { - Vector2 start = ConvertUnits.ToSimUnits(item.WorldPosition); - Vector2 end = ConvertUnits.ToSimUnits(targetPos.Value); + Vector2 barrelDir = GetBarrelDir(); + Vector2 aimStartPos = item.WorldPosition; + Vector2 aimEndPos = item.WorldPosition + barrelDir * shootDistance; + bool allowShootingIfNothingInWay = false; + if (currentTarget != null) + { + Vector2 targetStartPos = currentTarget.WorldPosition; + Vector2 targetEndPos = currentTarget.WorldPosition + targetVelocity * ConvertUnits.ToDisplayUnits(MaximumAimAhead); + + //if there's nothing in the way (not even the target we're trying to aim towards), + //shooting should only be allowed if we're aiming ahead of the target, in which case it's to be expected that we're aiming at "thin air" + allowShootingIfNothingInWay = + targetVelocity.LengthSquared() > 0.001f && + MathUtils.LineSegmentsIntersect( + aimStartPos, aimEndPos, + targetStartPos, targetEndPos) && + //target needs to be moving roughly perpendicular to us for aiming ahead of it to make sense + Math.Abs(Vector2.Dot(Vector2.Normalize(aimEndPos - aimStartPos), Vector2.Normalize(targetEndPos - targetStartPos))) < 0.5f; + } + + Vector2 start = ConvertUnits.ToSimUnits(aimStartPos); + Vector2 end = ConvertUnits.ToSimUnits(aimEndPos); // Check that there's not other entities that shouldn't be targeted (like a friendly sub) between us and the target. Body worldTarget = CheckLineOfSight(start, end); if (closestEnemy != null && closestEnemy.Submarine != null) @@ -1572,11 +1664,13 @@ namespace Barotrauma.Items.Components start -= closestEnemy.Submarine.SimPosition; end -= closestEnemy.Submarine.SimPosition; Body transformedTarget = CheckLineOfSight(start, end); - canShoot = CanShoot(transformedTarget, character) && (worldTarget == null || CanShoot(worldTarget, character)); + canShoot = + CanShoot(transformedTarget, character, allowShootingIfNothingInWay: allowShootingIfNothingInWay) && + (worldTarget == null || CanShoot(worldTarget, character, allowShootingIfNothingInWay: allowShootingIfNothingInWay)); } else { - canShoot = CanShoot(worldTarget, character); + canShoot = CanShoot(worldTarget, character, allowShootingIfNothingInWay: allowShootingIfNothingInWay); } if (!canShoot) { return false; } if (character.IsOnPlayerTeam) @@ -1666,9 +1760,13 @@ namespace Barotrauma.Items.Components } } - private bool CanShoot(Body targetBody, Character user = null, Identifier friendlyTag = default, bool targetSubmarines = true) + private bool CanShoot(Body targetBody, Character user = null, Identifier friendlyTag = default, bool targetSubmarines = true, bool allowShootingIfNothingInWay = false) { - if (targetBody == null) { return false; } + if (targetBody == null) + { + //nothing in the way (not even the target we're trying to shoot) -> no point in firing at thin air + return allowShootingIfNothingInWay; + } Character targetCharacter = null; if (targetBody.UserData is Character c) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index c94cc73e0..180487e83 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -362,7 +362,8 @@ namespace Barotrauma.Items.Components case "sprite": if (subElement.GetAttribute("texture") == null) { - DebugConsole.ThrowError("Item \"" + item.Name + "\" doesn't have a texture specified!"); + DebugConsole.ThrowError("Item \"" + item.Name + "\" doesn't have a texture specified!", + contentPackage: element.ContentPackage); return; } @@ -558,6 +559,7 @@ namespace Barotrauma.Items.Components foreach (WearableSprite wearableSprite in wearableSprites) { wearableSprite?.Sprite?.Remove(); + wearableSprite.Picker = null; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index 4c4c9c0a8..078765cf1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -230,11 +230,19 @@ namespace Barotrauma protected readonly int capacity; protected readonly ItemSlot[] slots; - + public bool Locked; protected float syncItemsDelay; + + private int extraStackSize; + public int ExtraStackSize + { + get => extraStackSize; + set => extraStackSize = MathHelper.Max(value, 0); + } + /// /// All items contained in the inventory. Stacked items are returned as individual instances. DO NOT modify the contents of the inventory while enumerating this list. /// @@ -1018,7 +1026,6 @@ namespace Barotrauma visualSlots[n].ShowBorderHighlight(Color.White, 0.1f, 0.4f); if (selectedSlot?.Inventory == this) { selectedSlot.ForceTooltipRefresh = true; } } - syncItemsDelay = 1.0f; #endif CharacterHUD.RecreateHudTextsIfFocused(item); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 379a616a7..c44369a96 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -240,7 +240,21 @@ namespace Barotrauma } } - public Item RootContainer { get; private set; } + private Item rootContainer; + public Item RootContainer + { + get { return rootContainer; } + private set + { + if (value == this) + { + DebugConsole.ThrowError($"Attempted to set the item \"{Prefab.Identifier}\" as it's own root container!\n{Environment.StackTrace.CleanupStackTrace()}"); + rootContainer = null; + return; + } + rootContainer = value; + } + } private bool inWaterProofContainer; @@ -369,7 +383,7 @@ namespace Barotrauma public float RotationRad { get; private set; } - [ConditionallyEditable(ConditionallyEditable.ConditionType.AllowRotating, MinValueFloat = 0.0f, MaxValueFloat = 360.0f, DecimalCount = 1, ValueStep = 1f), Serialize(0.0f, IsPropertySaveable.Yes)] + [ConditionallyEditable(ConditionallyEditable.ConditionType.AllowRotating, DecimalCount = 3, ForceShowPlusMinusButtons = true, ValueStep = 0.1f), Serialize(0.0f, IsPropertySaveable.Yes)] public float Rotation { get @@ -379,7 +393,7 @@ namespace Barotrauma set { if (!Prefab.AllowRotatingInEditor) { return; } - RotationRad = MathHelper.ToRadians(value); + RotationRad = MathUtils.WrapAnglePi(MathHelper.ToRadians(value)); #if CLIENT if (Screen.Selected == GameMain.SubEditorScreen) { @@ -443,7 +457,7 @@ namespace Barotrauma set { if (scale == value) { return; } - scale = MathHelper.Clamp(value, 0.01f, 10.0f); + scale = MathHelper.Clamp(value, Prefab.MinScale, Prefab.MaxScale); float relativeScale = scale / base.Prefab.Scale; @@ -1077,7 +1091,8 @@ namespace Barotrauma { if (!Physics.TryParseCollisionCategory(collisionCategoryStr, out Category cat)) { - DebugConsole.ThrowError("Invalid collision category in item \"" + Name + "\" (" + collisionCategoryStr + ")"); + DebugConsole.ThrowError("Invalid collision category in item \"" + Name + "\" (" + collisionCategoryStr + ")", + contentPackage: element.ContentPackage); } else { @@ -1232,7 +1247,8 @@ namespace Barotrauma var holdables = components.Where(c => c is Holdable); if (holdables.Count() > 1) { - DebugConsole.AddWarning($"Item {Prefab.Identifier} has multiple {nameof(Holdable)} components ({string.Join(", ", holdables.Select(h => h.GetType().Name))})."); + DebugConsole.AddWarning($"Item {Prefab.Identifier} has multiple {nameof(Holdable)} components ({string.Join(", ", holdables.Select(h => h.GetType().Name))}).", + Prefab.ContentPackage); } InsertToList(); @@ -1311,8 +1327,11 @@ namespace Barotrauma } } - if (FlippedX) clone.FlipX(false); - if (FlippedY) clone.FlipY(false); + if (FlippedX) { clone.FlipX(false); } + if (FlippedY) { clone.FlipY(false); } + + // Flipping an item tampers with its rotation, so restore it + clone.Rotation = Rotation; foreach (ItemComponent component in clone.components) { @@ -1624,6 +1643,9 @@ namespace Barotrauma return transformedRect; } + public override Quad2D GetTransformedQuad() + => Quad2D.FromSubmarineRectangle(rect).Rotated(-RotationRad); + /// /// goes through every item and re-checks which hull they are in /// @@ -1676,6 +1698,12 @@ namespace Barotrauma while (rootContainer.Container != null) { rootContainer = rootContainer.Container; + if (rootContainer == this) + { + DebugConsole.ThrowError($"Invalid container hierarchy: \"{Prefab.Identifier}\" was contained inside itself!\n{Environment.StackTrace.CleanupStackTrace()}"); + rootContainer = null; + break; + } inWaterProofContainer |= rootContainer.WaterProof; } newRootContainer = rootContainer; @@ -1938,7 +1966,7 @@ namespace Barotrauma } - public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = true) + public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, Vector2 impulseDirection, float deltaTime,bool playSound = true) { if (Indestructible || InvulnerableToDamage) { return new AttackResult(); } @@ -2494,7 +2522,7 @@ namespace Barotrauma if (Prefab.AllowRotatingInEditor) { - RotationRad = MathUtils.WrapAngleTwoPi(-RotationRad); + RotationRad = MathUtils.WrapAnglePi(-RotationRad); } #if CLIENT if (Prefab.CanSpriteFlipX) @@ -2521,6 +2549,10 @@ namespace Barotrauma return; } + if (Prefab.AllowRotatingInEditor) + { + RotationRad = MathUtils.WrapAngleTwoPi(-RotationRad); + } #if CLIENT if (Prefab.CanSpriteFlipY) { @@ -3021,7 +3053,10 @@ namespace Barotrauma return -1; } - public void Use(float deltaTime, Character user = null, Limb targetLimb = null, Entity useTarget = null) + /// User to pass to the OnUsed event. May need to be different than the user in cases like loaders using ammo boxes: + /// the box is technically being used by the loader, and doesn't allow a character to use it, but we may still need to know which character caused + /// the box to be used. + public void Use(float deltaTime, Character user = null, Limb targetLimb = null, Entity useTarget = null, Character userForOnUsedEvent = null) { if (RequireAimToUse && (user == null || !user.IsKeyDown(InputType.Aim))) { @@ -3046,7 +3081,7 @@ namespace Barotrauma ic.PlaySound(ActionType.OnUse, user); #endif ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, user, targetLimb, useTarget: useTarget, user: user); - ic.OnUsed.Invoke(new ItemComponent.ItemUseInfo(this, user)); + ic.OnUsed.Invoke(new ItemComponent.ItemUseInfo(this, user ?? userForOnUsedEvent)); if (ic.DeleteOnUse) { remove = true; } } } @@ -3504,7 +3539,26 @@ namespace Barotrauma if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { - if (!CanClientAccess(sender) || !(property.GetAttribute()?.IsEditable(this) ?? true)) + bool conditionAllowsEditing = true; + if (property.GetAttribute() is { } condition) + { + conditionAllowsEditing = condition.IsEditable(this); + } + + bool canAccess = false; + if (Container?.GetComponent() != null && + Container.CanClientAccess(sender)) + { + //items inside circuit boxes are inaccessible by "normal" means, + //but the properties can still be edited through the circuit box UI + canAccess = true; + } + else + { + canAccess = CanClientAccess(sender); + } + + if (!canAccess || !conditionAllowsEditing) { allowEditing = false; } @@ -3777,6 +3831,11 @@ namespace Barotrauma } break; } + case "itemstats": + { + item.StatManager.Load(subElement); + break; + } default: { ItemComponent component = unloadedComponents.Find(x => x.Name == subElement.Name.ToString()); @@ -3902,7 +3961,7 @@ namespace Barotrauma foreach (ItemComponent component in item.components) { - if (component.Parent != null) { component.IsActive = component.Parent.IsActive; } + if (component.Parent != null && component.InheritParentIsActive) { component.IsActive = component.Parent.IsActive; } component.OnItemLoaded(); } @@ -3972,6 +4031,8 @@ namespace Barotrauma upgrade.Save(element); } + statManager?.Save(element); + element.Add(new XAttribute("conditionpercentage", ConditionPercentage.ToString("G", CultureInfo.InvariantCulture))); var conditionAttribute = element.GetAttribute("condition"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs index fecfef994..e0bf2763e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs @@ -23,9 +23,10 @@ namespace Barotrauma Upgrade = 8, ItemStat = 9, DroppedStack = 10, + SetHighlight = 11, MinValue = 0, - MaxValue = 10 + MaxValue = 11 } public interface IEventData : NetEntityEvent.IData diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index 42cefb7bb..1b6d9aff5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -229,7 +229,7 @@ namespace Barotrauma /// public readonly int FabricationLimitMin, FabricationLimitMax; - public FabricationRecipe(XElement element, Identifier itemPrefab) + public FabricationRecipe(ContentXElement element, Identifier itemPrefab) { TargetItemPrefabIdentifier = itemPrefab; var displayNameIdentifier = element.GetAttributeIdentifier("displayname", ""); @@ -245,7 +245,8 @@ namespace Barotrauma OutCondition = element.GetAttributeFloat("outcondition", 1.0f); if (OutCondition > 1.0f) { - DebugConsole.AddWarning($"Error in \"{itemPrefab}\"'s fabrication recipe: out condition is above 100% ({OutCondition * 100})."); + DebugConsole.AddWarning($"Error in \"{itemPrefab}\"'s fabrication recipe: out condition is above 100% ({OutCondition * 100}).", + element.ContentPackage); } var requiredItems = new List(); RequiresRecipe = element.GetAttributeBool("requiresrecipe", false); @@ -267,9 +268,10 @@ namespace Barotrauma switch (subElement.Name.ToString().ToLowerInvariant()) { case "requiredskill": - if (subElement.Attribute("name") != null) + if (subElement.GetAttribute("name") != null) { - DebugConsole.ThrowError("Error in fabricable item " + itemPrefab + "! Use skill identifiers instead of names."); + DebugConsole.ThrowError("Error in fabricable item " + itemPrefab + "! Use skill identifiers instead of names.", + contentPackage: element.ContentPackage); continue; } @@ -283,7 +285,8 @@ namespace Barotrauma Identifier requiredItemTag = subElement.GetAttributeIdentifier("tag", Identifier.Empty); if (requiredItemIdentifier == Identifier.Empty && requiredItemTag == Identifier.Empty) { - DebugConsole.ThrowError("Error in fabricable item " + itemPrefab + "! One of the required items has no identifier or tag."); + DebugConsole.ThrowError("Error in fabricable item " + itemPrefab + "! One of the required items has no identifier or tag.", + contentPackage: element.ContentPackage); continue; } @@ -819,7 +822,7 @@ namespace Barotrauma [Serialize(null, IsPropertySaveable.No)] public string EquipConfirmationText { get; set; } - [Serialize(true, IsPropertySaveable.No, description: "Can the item be rotated in the submarine editor.")] + [Serialize(true, IsPropertySaveable.No, description: "Can the item be rotated in the submarine editor?")] public bool AllowRotatingInEditor { get; set; } [Serialize(false, IsPropertySaveable.No)] @@ -830,7 +833,13 @@ namespace Barotrauma [Serialize(true, IsPropertySaveable.No)] public bool CanFlipY { get; private set; } - + + [Serialize(0.1f, IsPropertySaveable.No)] + public float MinScale { get; private set; } + + [Serialize(10.0f, IsPropertySaveable.No)] + public float MaxScale { get; private set; } + [Serialize(false, IsPropertySaveable.No)] public bool IsDangerous { get; private set; } @@ -862,24 +871,43 @@ namespace Barotrauma public int GetMaxStackSize(Inventory inventory) { + int extraStackSize = inventory switch + { + ItemInventory { Owner: Item it } i => (int)it.StatManager.GetAdjustedValueAdditive(ItemTalentStats.ExtraStackSize, i.ExtraStackSize), + CharacterInventory { Owner: Character { Info: { } info } } i => i.ExtraStackSize + (int)info.GetSavedStatValueWithAll(StatTypes.InventoryExtraStackSize, Category.ToIdentifier()), + not null => inventory.ExtraStackSize, + null => 0 + }; + if (inventory is CharacterInventory && maxStackSizeCharacterInventory > 0) { - return maxStackSizeCharacterInventory; + return MaxStackWithExtra(maxStackSizeCharacterInventory, extraStackSize); } else if (inventory?.Owner is Item item && (item.GetComponent() is { Attachable: false } || item.GetComponent() != null)) { if (maxStackSizeHoldableOrWearableInventory > 0) { - return maxStackSizeHoldableOrWearableInventory; + return MaxStackWithExtra(maxStackSizeHoldableOrWearableInventory, extraStackSize); } else if (maxStackSizeCharacterInventory > 0) { //if maxStackSizeHoldableOrWearableInventory is not set, it defaults to maxStackSizeCharacterInventory - return maxStackSizeCharacterInventory; + return MaxStackWithExtra(maxStackSizeCharacterInventory, extraStackSize); } } - return maxStackSize; + + return MaxStackWithExtra(maxStackSize, extraStackSize); + + static int MaxStackWithExtra(int maxStackSize, int extraStackSize) + { + extraStackSize = Math.Max(extraStackSize, 0); + if (maxStackSize == 1) + { + return Math.Min(maxStackSize, Inventory.MaxPossibleStackSize); + } + return Math.Min(maxStackSize + extraStackSize, Inventory.MaxPossibleStackSize); + } } [Serialize(false, IsPropertySaveable.No)] @@ -1016,7 +1044,8 @@ namespace Barotrauma if (ConfigElement.GetAttribute("cargocontainername") != null) { - DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\" - cargo container should be configured using the item's identifier, not the name."); + DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\" - cargo container should be configured using the item's identifier, not the name.", + contentPackage: ConfigElement.ContentPackage); } SerializableProperty.DeserializeProperties(this, ConfigElement); @@ -1054,7 +1083,8 @@ namespace Barotrauma if (subElement.GetAttribute("sourcerect") == null && subElement.GetAttribute("sheetindex") == null) { - DebugConsole.ThrowError($"Warning - sprite sourcerect not configured for item \"{ToString()}\"!"); + DebugConsole.ThrowError($"Warning - sprite sourcerect not configured for item \"{ToString()}\"!", + contentPackage: ConfigElement.ContentPackage); } Size = Sprite.size; @@ -1072,7 +1102,8 @@ namespace Barotrauma if (priceInfo.StoreIdentifier.IsEmpty) { continue; } if (storePrices.ContainsKey(priceInfo.StoreIdentifier)) { - DebugConsole.AddWarning($"Error in item prefab \"{this}\": price for the store \"{priceInfo.StoreIdentifier}\" defined more than once."); + DebugConsole.AddWarning($"Error in item prefab \"{this}\": price for the store \"{priceInfo.StoreIdentifier}\" defined more than once.", + ContentPackage); storePrices[priceInfo.StoreIdentifier] = priceInfo; } else @@ -1085,7 +1116,8 @@ namespace Barotrauma { if (storePrices.ContainsKey(locationType)) { - DebugConsole.AddWarning($"Error in item prefab \"{this}\": price for the location type \"{locationType}\" defined more than once."); + DebugConsole.AddWarning($"Error in item prefab \"{this}\": price for the location type \"{locationType}\" defined more than once.", + ContentPackage); storePrices[locationType] = new PriceInfo(subElement); } else @@ -1103,13 +1135,15 @@ namespace Barotrauma { if (itemElement.Attribute("name") != null) { - DebugConsole.ThrowError($"Error in item config \"{ToString()}\" - use item identifiers instead of names to configure the deconstruct items."); + DebugConsole.ThrowError($"Error in item config \"{ToString()}\" - use item identifiers instead of names to configure the deconstruct items.", + contentPackage: ConfigElement.ContentPackage); continue; } var deconstructItem = new DeconstructItem(itemElement, Identifier); if (deconstructItem.ItemIdentifier.IsEmpty) { - DebugConsole.ThrowError($"Error in item config \"{ToString()}\" - deconstruction output contains an item with no identifier."); + DebugConsole.ThrowError($"Error in item config \"{ToString()}\" - deconstruction output contains an item with no identifier.", + contentPackage: ConfigElement.ContentPackage); continue; } deconstructItems.Add(deconstructItem); @@ -1122,12 +1156,18 @@ namespace Barotrauma var newRecipe = new FabricationRecipe(subElement, Identifier); if (fabricationRecipes.TryGetValue(newRecipe.RecipeHash, out var prevRecipe)) { + //the errors below may be caused by a mod overriding a base item instead of this one, log the package of the base item in that case + var packageToLog = + (variantOf.ContentPackage != null && variantOf.ContentPackage != ContentPackageManager.VanillaCorePackage) ? + variantOf.ContentPackage : + GetParentModPackageOrThisPackage(); + int prevRecipeIndex = loadedRecipes.IndexOf(prevRecipe); - DebugConsole.ThrowError( + DebugConsole.AddWarning( $"Error in item prefab \"{ToString()}\": " + - $"Fabrication recipe #{loadedRecipes.Count + 1} has the same hash as recipe #{prevRecipeIndex + 1}. This is most likely caused by identical, duplicate recipes." + - $"This will cause issues with fabrication." - ); + $"Fabrication recipe #{loadedRecipes.Count + 1} has the same hash as recipe #{prevRecipeIndex + 1}. This is most likely caused by identical, duplicate recipes. " + + $"This will cause issues with fabrication.", + contentPackage: packageToLog); } else { @@ -1142,7 +1182,8 @@ namespace Barotrauma //it's ok for variants to clear the primary and secondary containers to disable the PreferredContainer element if (variantOf == null) { - DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\": preferred container has no preferences defined ({subElement})."); + DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\": preferred container has no preferences defined ({subElement}).", + contentPackage: ConfigElement.ContentPackage); } } else @@ -1192,7 +1233,8 @@ namespace Barotrauma case "suitabletreatment": if (subElement.GetAttribute("name") != null) { - DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\" - suitable treatments should be defined using item identifiers, not item names."); + DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\" - suitable treatments should be defined using item identifiers, not item names.", + contentPackage: ConfigElement.ContentPackage); } Identifier treatmentIdentifier = subElement.GetAttributeIdentifier("identifier", subElement.GetAttributeIdentifier("type", Identifier.Empty)); float suitability = subElement.GetAttributeFloat("suitability", 0.0f); @@ -1201,6 +1243,8 @@ namespace Barotrauma } } + Size = ConfigElement.GetAttributeVector2(nameof(Size), Size); + #if CLIENT ParseSubElementsClient(ConfigElement, variantOf); #endif @@ -1231,7 +1275,7 @@ namespace Barotrauma if (Sprite == null) { - DebugConsole.ThrowError($"Item \"{ToString()}\" has no sprite!"); + DebugConsole.ThrowError($"Item \"{ToString()}\" has no sprite!", contentPackage: ConfigElement.ContentPackage); #if SERVER this.sprite = new Sprite("", Vector2.Zero); this.sprite.SourceRect = new Rectangle(0, 0, 32, 32); @@ -1248,7 +1292,8 @@ namespace Barotrauma if (Identifier == Identifier.Empty) { DebugConsole.ThrowError( - $"Item prefab \"{ToString()}\" has no identifier. All item prefabs have a unique identifier string that's used to differentiate between items during saving and loading."); + $"Item prefab \"{ToString()}\" has no identifier. All item prefabs have a unique identifier string that's used to differentiate between items during saving and loading.", + contentPackage: ConfigElement.ContentPackage); } #if DEBUG @@ -1256,7 +1301,8 @@ namespace Barotrauma { if (!string.IsNullOrEmpty(OriginalName)) { - DebugConsole.AddWarning($"Item \"{(Identifier == Identifier.Empty ? Name : Identifier.Value)}\" has a hard-coded name, and won't be localized to other languages."); + DebugConsole.AddWarning($"Item \"{(Identifier == Identifier.Empty ? Name : Identifier.Value)}\" has a hard-coded name, and won't be localized to other languages.", + ContentPackage); } } #endif @@ -1316,9 +1362,9 @@ namespace Barotrauma { string message = $"Tried to get price info for \"{Identifier}\" with a null store parameter!\n{Environment.StackTrace.CleanupStackTrace()}"; #if DEBUG - DebugConsole.LogError(message); + DebugConsole.LogError(message, contentPackage: ContentPackage); #else - DebugConsole.AddWarning(message); + DebugConsole.AddWarning(message, ContentPackage); GameAnalyticsManager.AddErrorEventOnce("ItemPrefab.GetPriceInfo:StoreParameterNull", GameAnalyticsManager.ErrorSeverity.Error, message); #endif return null; @@ -1525,6 +1571,9 @@ namespace Barotrauma void CheckXML(XElement originalElement, XElement variantElement, XElement result) { + //if either the parent or the variant are non-vanilla, assume the error is coming from that package + var packageToLog = parent.ContentPackage != GameMain.VanillaContent ? parent.ContentPackage : ContentPackage; + if (result == null) { return; } if (result.Name.ToIdentifier() == "RequiredItem" && result.Parent?.Name.ToIdentifier() == "Fabricate") @@ -1538,7 +1587,8 @@ namespace Barotrauma { DebugConsole.AddWarning($"Potential error in item variant \"{Identifier}\": " + $"the item inherits the fabrication requirement of x{originalAmount} \"{originalIdentifier}\" from the base item \"{parent.Identifier}\". " + - $"If this is not intentional, you can use empty elements in the item variant to remove any excess inherited fabrication requirements."); + $"If this is not intentional, you can use empty elements in the item variant to remove any excess inherited fabrication requirements.", + packageToLog); } return; } @@ -1549,7 +1599,8 @@ namespace Barotrauma DebugConsole.AddWarning($"Potential error in item variant \"{Identifier}\": " + $"the base item \"{parent.Identifier}\" requires x{originalAmount} \"{originalIdentifier}\" to fabricate. " + $"The variant only overrides the required item, not the amount, resulting in a requirement of x{originalAmount} \"{resultIdentifier}\". "+ - "Specify the amount in the variant to fix this."); + "Specify the amount in the variant to fix this.", + packageToLog); } } if (originalElement?.Name.ToIdentifier() == "Deconstruct" && @@ -1559,18 +1610,35 @@ namespace Barotrauma variantElement.Elements().Any(e => e.Name.ToIdentifier() == "RequiredItem")) { DebugConsole.AddWarning($"Potential error in item variant \"{Identifier}\": " + - $"the item defines deconstruction recipes using 'RequiredItem' instead of 'Item'. Overriding the base recipe may not work correctly."); + $"the item defines deconstruction recipes using 'RequiredItem' instead of 'Item'. Overriding the base recipe may not work correctly.", + packageToLog); } if (variantElement.Elements().Any(e => e.Name.ToIdentifier() == "Item") && originalElement.Elements().Any(e => e.Name.ToIdentifier() == "RequiredItem")) { DebugConsole.AddWarning($"Potential error in item \"{parent.Identifier}\": " + - $"the item defines deconstruction recipes using 'RequiredItem' instead of 'Item'. The item variant \"{Identifier}\" may not override the base recipe correctly."); + $"the item defines deconstruction recipes using 'RequiredItem' instead of 'Item'. The item variant \"{Identifier}\" may not override the base recipe correctly.", + packageToLog); } } } } + /// + /// If the base prefab this one is a variant of is defined in a non-vanilla package, returns that non-vanilla package. + /// Otherwise returns the package of this prefab. Can be useful for logging errors that may have been caused by a mod overriding + /// the base item. + /// + public ContentPackage GetParentModPackageOrThisPackage() + { + if (ParentPrefab != null && + ParentPrefab.ContentPackage != ContentPackageManager.VanillaCorePackage) + { + return ParentPrefab.ContentPackage; + } + return ContentPackage; + } + public override string ToString() { return $"{Name} (identifier: {Identifier})"; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemStatManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemStatManager.cs index ba7aef6a9..d1b4ace8f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemStatManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemStatManager.cs @@ -2,24 +2,46 @@ using System; using System.Collections.Generic; +using System.Xml.Linq; namespace Barotrauma { [NetworkSerialize] - internal readonly record struct TalentStatIdentifier(ItemTalentStats Stat, Identifier TalentIdentifier, Option UniqueCharacterId) : INetSerializableStruct + internal readonly record struct TalentStatIdentifier(ItemTalentStats Stat, Identifier TalentIdentifier, Option UniqueCharacterId, bool Save) : INetSerializableStruct { /// /// Stackable identifiers feature a unique ID to allow multiple stats applied by the same talent from different characters to coexist. /// public static TalentStatIdentifier CreateStackable(ItemTalentStats stat, Identifier talentIdentifier, UInt32 characterId) - => new(stat, talentIdentifier, Option.Some(characterId)); + => new(stat, talentIdentifier, Option.Some(characterId), Save: false); /// /// Unstackable identifiers do not have a unique ID causing them to be identical to other stats applied by the same talent from different characters and thus only one of them will be applied. /// will always use the highest value for unstackable stats. /// - public static TalentStatIdentifier CreateUnstackable(ItemTalentStats stat, Identifier talentIdentifier) - => new(stat, talentIdentifier, Option.None); + public static TalentStatIdentifier CreateUnstackable(ItemTalentStats stat, Identifier talentIdentifier, bool Save) + => new(stat, talentIdentifier, Option.None, Save); + + public XElement Serialize() + => new XElement("Stat", + new XAttribute("type", Stat), + new XAttribute("talent", TalentIdentifier)); + + public static Option TryLoadFromXML(XElement element) + { + var stat = element.GetAttributeEnum("type", ItemTalentStats.None); + var talentIdentifier = element.GetAttributeIdentifier("talent", Identifier.Empty); + + if (stat == ItemTalentStats.None || talentIdentifier == Identifier.Empty) + { + var error = $"Failed to load talent stat identifier from XML {element}"; + DebugConsole.ThrowError(error); + GameAnalyticsManager.AddErrorEventOnce("ItemStatManager.TryLoadFromXML:Invalid", GameAnalyticsManager.ErrorSeverity.Error, error); + return Option.None; + } + + return Option.Some(CreateUnstackable(stat, talentIdentifier, true)); + } } internal sealed class ItemStatManager @@ -29,14 +51,14 @@ namespace Barotrauma public ItemStatManager(Item item) => this.item = item; - public void ApplyStat(ItemTalentStats stat, bool stackable, float value, CharacterTalent talent) + public void ApplyStat(ItemTalentStats stat, bool stackable, bool save, float value, CharacterTalent talent) { if (talent.Character?.ID is not { } characterId || talent.Prefab?.Identifier is not { } talentIdentifier) { return; } var identifier = stackable ? TalentStatIdentifier.CreateStackable(stat, talentIdentifier, characterId) - : TalentStatIdentifier.CreateUnstackable(stat, talentIdentifier); + : TalentStatIdentifier.CreateUnstackable(stat, talentIdentifier, save); if (!stackable) { @@ -57,12 +79,45 @@ namespace Barotrauma #endif } + public void Save(XElement parent) + { + var element = new XElement("itemstats"); + + foreach (var (key, value) in talentStats) + { + if (!key.Save) { continue; } + + var statElement = key.Serialize(); + statElement.Add(new XAttribute("value", value)); + + element.Add(statElement); + } + + parent.Add(element); + } + + public void Load(XElement element) + { + foreach (XElement statElement in element.Elements()) + { + if (!TalentStatIdentifier.TryLoadFromXML(statElement).TryUnwrap(out var identifier)) { continue; } + + var value = statElement.GetAttributeFloat("value", 0f); + + ApplyStatDirect(identifier, value); + } + } + /// /// Used for setting the value value from network packet; bypassing all validity checks. /// - public void ApplyStatDirect(TalentStatIdentifier identifier, float value) => talentStats[identifier] = value; + public void ApplyStatDirect(TalentStatIdentifier identifier, float value) + => talentStats[identifier] = value; - public float GetAdjustedValue(ItemTalentStats stat, float originalValue) + /// + /// Adjusts the value by multiplying it with the value of the talent stat + /// + public float GetAdjustedValueMultiplicative(ItemTalentStats stat, float originalValue) { float total = originalValue; @@ -74,5 +129,21 @@ namespace Barotrauma return total; } + + /// + /// Adjusts the value by adding the value of the talent stat instead of multiplying it + /// + public float GetAdjustedValueAdditive(ItemTalentStats stat, float originalValue) + { + float total = originalValue; + + foreach (var (key, value) in talentStats) + { + if (key.Stat != stat) { continue; } + total += value; + } + + return total; + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs index 4e65d9b65..f868f7d3b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs @@ -222,7 +222,7 @@ namespace Barotrauma if (element.GetAttribute("name") != null) { //backwards compatibility + a console warning - DebugConsole.ThrowError($"Error in RelatedItem config (" + (string.IsNullOrEmpty(parentDebugName) ? element.ToString() : parentDebugName) + ") - use item tags or identifiers instead of names."); + DebugConsole.ThrowError($"Error in RelatedItem config (" + (string.IsNullOrEmpty(parentDebugName) ? element.ToString() : parentDebugName) + ") - use item tags or identifiers instead of names.", contentPackage: element.ContentPackage); Identifier[] itemNames = element.GetAttributeIdentifierArray("name", Array.Empty()); //attempt to convert to identifiers and tags List convertedIdentifiers = new List(); @@ -299,7 +299,7 @@ namespace Barotrauma } if (!Enum.TryParse(typeStr, true, out type)) { - DebugConsole.ThrowError("Error in RelatedItem config (" + parentDebugName + ") - \"" + typeStr + "\" is not a valid relation type."); + DebugConsole.ThrowError("Error in RelatedItem config (" + parentDebugName + ") - \"" + typeStr + "\" is not a valid relation type.", contentPackage: element.ContentPackage); type = RelationType.Invalid; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/DummyFireSource.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/DummyFireSource.cs index a50646860..80428221a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/DummyFireSource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/DummyFireSource.cs @@ -9,7 +9,8 @@ namespace Barotrauma public bool CausedByPsychosis; - public DummyFireSource(Vector2 maxSize, Vector2 worldPosition, Hull spawningHull = null, bool isNetworkMessage = false) : base(worldPosition, spawningHull, isNetworkMessage) + public DummyFireSource(Vector2 maxSize, Vector2 worldPosition, Hull spawningHull = null, bool isNetworkMessage = false) : + base(worldPosition, spawningHull, sourceCharacter: null, isNetworkMessage: isNetworkMessage) { this.maxSize = maxSize; DamagesItems = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs index 6d9577b89..3fa6c8e18 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs @@ -91,7 +91,12 @@ namespace Barotrauma get { return hull; } } - public FireSource(Vector2 worldPosition, Hull spawningHull = null, bool isNetworkMessage = false) + /// + /// Which character caused this fire (if any)? + /// + public readonly Character SourceCharacter; + + public FireSource(Vector2 worldPosition, Hull spawningHull = null, Character sourceCharacter = null, bool isNetworkMessage = false) { hull = Hull.FindHull(worldPosition, spawningHull); if (hull == null || worldPosition.Y < hull.WorldSurface) { return; } @@ -109,6 +114,8 @@ namespace Barotrauma position -= Submarine.Position; } + SourceCharacter = sourceCharacter; + #if CLIENT lightSource = new LightSource(this.position, 50.0f, new Color(1.0f, 0.9f, 0.7f), hull?.Submarine); #endif @@ -306,8 +313,12 @@ namespace Barotrauma foreach (Limb limb in c.AnimController.Limbs) { if (limb.IsSevered) { continue; } - c.LastDamageSource = null; - c.DamageLimb(WorldPosition, limb, AfflictionPrefab.Burn.Instantiate(dmg).ToEnumerable(), 0.0f, false, 0.0f); + c.LastDamageSource = SourceCharacter; + c.DamageLimb(WorldPosition, limb, AfflictionPrefab.Burn.Instantiate(dmg).ToEnumerable(), + stun: 0.0f, + playSound: false, + attackImpulse: Vector2.Zero, + attacker: SourceCharacter); } #if CLIENT //let clients display the client-side damage immediately, otherwise they may not be able to react to the damage fast enough diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 7f899899a..049d7f621 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -442,6 +442,11 @@ namespace Barotrauma public List FakeFireSources { get; private set; } + /// + /// Can be used by conditionals + /// + public int FireCount => FireSources?.Count ?? 0; + public BallastFloraBehavior BallastFlora { get; set; } public Hull(Rectangle rectangle) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/IDamageable.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/IDamageable.cs index 1aeb8ddd4..bc01ab05d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/IDamageable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/IDamageable.cs @@ -9,7 +9,7 @@ namespace Barotrauma Vector2 WorldPosition { get; } float Health { get; } - AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = true); + AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, Vector2 impulseDirection, float deltaTime, bool playSound = true); public readonly struct AttackEventData diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/DestructibleLevelWall.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/DestructibleLevelWall.cs index f513fb078..4731ca9a0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/DestructibleLevelWall.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/DestructibleLevelWall.cs @@ -89,7 +89,7 @@ namespace Barotrauma partial void AddDamageProjSpecific(float damage, Vector2 worldPosition); - public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = true) + public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, Vector2 impulseDirection, float deltaTime, bool playSound = true) { AddDamage(attack.StructureDamage, worldPosition); return new AttackResult(attack.StructureDamage); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 2c0a60a8a..5db866ad8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -115,6 +115,20 @@ namespace Barotrauma Ruin = null; Cave = cave; } + + /// + /// Caves, ruins, outposts and similar enclosed areas + /// + /// + public bool IsEnclosedArea() + { + return + PositionType == PositionType.Cave || + PositionType == PositionType.Ruin || + PositionType == PositionType.Outpost || + PositionType == PositionType.BeaconStation || + PositionType == PositionType.AbyssCave; + } } public enum TunnelType @@ -619,14 +633,14 @@ namespace Barotrauma { endHole = new Tunnel( TunnelType.SidePath, - new List() { startPosition, startExitPosition, new Point(0, Size.Y) }, + new List() { startPosition, new Point(0, startPosition.Y) }, minWidth, parentTunnel: mainPath); } else { endHole = new Tunnel( TunnelType.SidePath, - new List() { endPosition, endExitPosition, Size }, + new List() { endPosition, new Point(Size.X, endPosition.Y) }, minWidth, parentTunnel: mainPath); } Tunnels.Add(endHole); @@ -930,6 +944,7 @@ namespace Barotrauma foreach (AbyssIsland abyssIsland in AbyssIslands) { + abyssIsland.Cells.RemoveAll(c => c.CellType == CellType.Path); cells.AddRange(abyssIsland.Cells); } @@ -1726,7 +1741,9 @@ namespace Barotrauma { bool tooClose = false; - if (cell.IsPointInsideAABB(position, margin: minDistance)) + //if the cell is very large, the position can be far away from the edges while being inside the cell + //so we need to check that here too + if (cell.IsPointInside(position)) { tooClose = true; } @@ -3257,13 +3274,13 @@ namespace Barotrauma } Vector2 position = Vector2.Zero; - int tries = 0; do { - TryGetInterestingPosition(true, spawnPosType, minDistFromSubs, out Vector2 startPos, filter); + TryGetInterestingPosition(true, spawnPosType, minDistFromSubs, out InterestingPosition potentialPos, filter); Vector2 offset = Rand.Vector(Rand.Range(0.0f, randomSpread, Rand.RandSync.ServerAndClient), Rand.RandSync.ServerAndClient); + Vector2 startPos = potentialPos.Position.ToVector2(); if (!IsPositionInsideWall(startPos + offset)) { startPos += offset; @@ -3271,14 +3288,18 @@ namespace Barotrauma Vector2 endPos = startPos - Vector2.UnitY * Size.Y; - if (Submarine.PickBody( - ConvertUnits.ToSimUnits(startPos), - ConvertUnits.ToSimUnits(endPos), - ExtraWalls.Where(w => w.Body?.BodyType == BodyType.Dynamic || w is DestructibleLevelWall).Select(w => w.Body).Union(Submarine.Loaded.Where(s => s.Info.Type == SubmarineType.Player).Select(s => s.PhysicsBody.FarseerBody)), - Physics.CollisionLevel | Physics.CollisionWall) != null) + //try to find a level wall below the position unless the position is indoors + if (!potentialPos.IsEnclosedArea()) { - position = ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) + Vector2.Normalize(startPos - endPos) * offsetFromWall; - break; + if (Submarine.PickBody( + ConvertUnits.ToSimUnits(startPos), + ConvertUnits.ToSimUnits(endPos), + ExtraWalls.Where(w => w.Body?.BodyType == BodyType.Dynamic || w is DestructibleLevelWall).Select(w => w.Body).Union(Submarine.Loaded.Where(s => s.Info.Type == SubmarineType.Player).Select(s => s.PhysicsBody.FarseerBody)), + Physics.CollisionLevel | Physics.CollisionWall)?.UserData is VoronoiCell) + { + position = ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) + Vector2.Normalize(startPos - endPos) * offsetFromWall; + break; + } } tries++; @@ -3293,25 +3314,25 @@ namespace Barotrauma return position; } - public bool TryGetInterestingPositionAwayFromPoint(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out Vector2 position, Vector2 awayPoint, float minDistFromPoint, Func filter = null) + public bool TryGetInterestingPositionAwayFromPoint(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out InterestingPosition position, Vector2 awayPoint, float minDistFromPoint, Func filter = null) { - bool success = TryGetInterestingPosition(useSyncedRand, positionType, minDistFromSubs, out Point pos, awayPoint, minDistFromPoint, filter); - position = pos.ToVector2(); + position = default; + bool success = TryGetInterestingPosition(useSyncedRand, positionType, minDistFromSubs, out position, awayPoint, minDistFromPoint, filter); return success; } - public bool TryGetInterestingPosition(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out Vector2 position, Func filter = null, bool suppressWarning = false) + public bool TryGetInterestingPosition(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out InterestingPosition position, Func filter = null, bool suppressWarning = false) { - bool success = TryGetInterestingPosition(useSyncedRand, positionType, minDistFromSubs, out Point pos, Vector2.Zero, minDistFromPoint: 0, filter, suppressWarning); - position = pos.ToVector2(); + position = default; + bool success = TryGetInterestingPosition(useSyncedRand, positionType, minDistFromSubs, out position, Vector2.Zero, minDistFromPoint: 0, filter, suppressWarning); return success; } - public bool TryGetInterestingPosition(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out Point position, Vector2 awayPoint, float minDistFromPoint = 0f, Func filter = null, bool suppressWarning = false) + public bool TryGetInterestingPosition(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out InterestingPosition position, Vector2 awayPoint, float minDistFromPoint = 0f, Func filter = null, bool suppressWarning = false) { if (!PositionsOfInterest.Any()) { - position = new Point(Size.X / 2, Size.Y / 2); + position = default; return false; } @@ -3323,7 +3344,20 @@ namespace Barotrauma if (positionType.HasFlag(PositionType.MainPath) || positionType.HasFlag(PositionType.SidePath) || positionType.HasFlag(PositionType.Abyss) || positionType.HasFlag(PositionType.Cave) || positionType.HasFlag(PositionType.AbyssCave)) { - suitablePositions.RemoveAll(p => IsPositionInsideWall(p.Position.ToVector2())); +#if DEBUG + for (int i = 0; i < PositionsOfInterest.Count; i++) + { + var pos = PositionsOfInterest[i]; + if (!suitablePositions.Contains(pos)) { continue; } + if (IsInvalid(pos)) + { + pos.IsValid = false; + PositionsOfInterest[i] = pos; + } + } +#endif + suitablePositions.RemoveAll(p => IsInvalid(p)); + bool IsInvalid(InterestingPosition p) => IsPositionInsideWall(p.Position.ToVector2()); } if (!suitablePositions.Any()) { @@ -3335,7 +3369,7 @@ namespace Barotrauma DebugConsole.ThrowError(errorMsg); #endif } - position = PositionsOfInterest[Rand.Int(PositionsOfInterest.Count, (useSyncedRand ? Rand.RandSync.ServerAndClient : Rand.RandSync.Unsynced))].Position; + position = PositionsOfInterest[Rand.Int(PositionsOfInterest.Count, (useSyncedRand ? Rand.RandSync.ServerAndClient : Rand.RandSync.Unsynced))]; return false; } @@ -3361,14 +3395,14 @@ namespace Barotrauma DebugConsole.ThrowError(errorMsg); #endif float maxDist = 0.0f; - position = suitablePositions.First().Position; + position = suitablePositions.First(); foreach (InterestingPosition pos in suitablePositions) { float dist = Submarine.Loaded.Sum(s => Submarine.MainSubs.Contains(s) ? Vector2.DistanceSquared(s.WorldPosition, pos.Position.ToVector2()) : 0.0f); if (dist > maxDist) { - position = pos.Position; + position = pos; maxDist = dist; } } @@ -3376,7 +3410,7 @@ namespace Barotrauma return false; } - position = farEnoughPositions[Rand.Int(farEnoughPositions.Count, useSyncedRand ? Rand.RandSync.ServerAndClient : Rand.RandSync.Unsynced)].Position; + position = farEnoughPositions[Rand.Int(farEnoughPositions.Count, useSyncedRand ? Rand.RandSync.ServerAndClient : Rand.RandSync.Unsynced)]; return true; } @@ -3933,12 +3967,28 @@ namespace Barotrauma { var totalSW = new Stopwatch(); totalSW.Start(); + var wreckFiles = ContentPackageManager.EnabledPackages.All .SelectMany(p => p.GetFiles()) .OrderBy(f => f.UintIdentifier).ToList(); + + for (int i = wreckFiles.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) + { + if (Difficulty < wreckInfo.MinLevelDifficulty || Difficulty > wreckInfo.MaxLevelDifficulty) + { + wreckFiles.RemoveAt(i); + } + } + } if (wreckFiles.None()) { - DebugConsole.ThrowError("No wreck files found in the selected content packages!"); + DebugConsole.ThrowError($"No wreck files found for the level difficulty {LevelData.Difficulty}!"); Wrecks = new List(); return; } @@ -4072,7 +4122,7 @@ namespace Barotrauma if (location != null) { - DebugConsole.NewMessage($"Generating an outpost for the {(isStart ? "start" : "end")} of the level... (Location: {location.Name}, level type: {LevelData.Type})"); + DebugConsole.NewMessage($"Generating an outpost for the {(isStart ? "start" : "end")} of the level... (Location: {location.DisplayName}, level type: {LevelData.Type})"); outpost = OutpostGenerator.Generate(outpostGenerationParams, location, onlyEntrance: LevelData.Type != LevelData.LevelType.Outpost, LevelData.AllowInvalidOutpost); } else @@ -4180,7 +4230,20 @@ namespace Barotrauma } } - spawnPos = outpost.FindSpawnPos(i == 0 ? StartPosition : EndPosition, minSize, outpostDockingPortOffset != null ? subDockingPortOffset - outpostDockingPortOffset.Value : 0.0f, verticalMoveDir: 1); + Vector2 preferredSpawnPos = i == 0 ? StartPosition : EndPosition; + //if we're placing the outpost at the end of the level, close to the bottom-right, + //and there's a hole leading out the right side of the level, move the spawn position towards that hole. + //Makes outpost placement a little nicer in levels with lots of verticality: if there's a tall vertical + //shaft leading down to the end position, we don't want the outpost to be placed all the way up to wherever the + //ceiling is at the top of that shaft. + if (i == 1 && GenerationParams.CreateHoleNextToEnd && + preferredSpawnPos.X > Size.X * 0.75f && + preferredSpawnPos.Y < Size.Y * 0.25f) + { + preferredSpawnPos.X = (preferredSpawnPos.X + Size.X) / 2; + } + + spawnPos = outpost.FindSpawnPos(preferredSpawnPos, minSize, outpostDockingPortOffset != null ? subDockingPortOffset - outpostDockingPortOffset.Value : 0.0f, verticalMoveDir: 1); if (Type == LevelData.LevelType.Outpost) { spawnPos.Y = Math.Min(Size.Y - outpost.Borders.Height * 0.6f, spawnPos.Y + outpost.Borders.Height / 2); @@ -4204,7 +4267,7 @@ namespace Barotrauma if (StartLocation != null) { outpost.TeamID = StartLocation.Type.OutpostTeam; - outpost.Info.Name = StartLocation.Name; + outpost.Info.Name = StartLocation.DisplayName.Value; } } else @@ -4213,7 +4276,7 @@ namespace Barotrauma if (EndLocation != null) { outpost.TeamID = EndLocation.Type.OutpostTeam; - outpost.Info.Name = EndLocation.Name; + outpost.Info.Name = EndLocation.DisplayName.Value; } } } @@ -4235,7 +4298,8 @@ namespace Barotrauma ContentFile contentFile = null; if (!string.IsNullOrEmpty(GenerationParams.ForceBeaconStation)) { - contentFile = beaconStationFiles.OrderBy(b => b.UintIdentifier).FirstOrDefault(f => f.Path == GenerationParams.ForceBeaconStation); + var contentPath = ContentPath.FromRaw(GenerationParams.ContentPackage, GenerationParams.ForceBeaconStation); + contentFile = beaconStationFiles.OrderBy(b => b.UintIdentifier).FirstOrDefault(f => f.Path == contentPath); if (contentFile == null) { DebugConsole.ThrowError($"Failed to find the beacon station \"{GenerationParams.ForceBeaconStation}\". Using a random one instead..."); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index 77c3a7cf0..25e106aba 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -241,7 +241,7 @@ namespace Barotrauma /// public LevelData(Location location, Map map, float difficulty) { - Seed = location.BaseName + map.Locations.IndexOf(location); + Seed = location.NameIdentifier.Value + map.Locations.IndexOf(location); Biome = location.Biome; Type = LevelType.Outpost; Difficulty = difficulty; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObject.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObject.cs index 1975dc33d..6d90bcda0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObject.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObject.cs @@ -160,7 +160,7 @@ namespace Barotrauma partial void InitProjSpecific(); - public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = true) + public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, Vector2 impulseDirection, float deltaTime, bool playSound = true) { if (Health <= 0.0f) { return new AttackResult(0.0f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs index 462235bed..98b36532c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -526,12 +526,13 @@ namespace Barotrauma { List spawnPosTypes = new List(4); List availableSpawnPositions = new List(); + bool requireCaveSpawnPos = spawnPosType == LevelObjectPrefab.SpawnPosType.CaveWall; foreach (var cell in cells) { foreach (var edge in cell.Edges) { if (!edge.IsSolid || edge.OutsideLevel) { continue; } - if (spawnPosType != LevelObjectPrefab.SpawnPosType.CaveWall && edge.NextToCave) { continue; } + if (requireCaveSpawnPos != edge.NextToCave) { continue; } Vector2 normal = edge.GetNormal(cell); Alignment edgeAlignment = 0; @@ -638,6 +639,7 @@ namespace Barotrauma public override void Remove() { + objectsInRange.Clear(); if (objects != null) { foreach (LevelObject obj in objects) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index 03ebcb980..552b93ade 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -55,8 +55,17 @@ namespace Barotrauma public readonly List Connections = new List(); - private string baseName; + public LocalizedString DisplayName { get; private set; } + + public Identifier NameIdentifier => nameIdentifier; + private int nameFormatIndex; + private Identifier nameIdentifier; + + /// + /// For backwards compatibility: a non-localizable name from the old text files. + /// + private string rawName; private LocationType addInitialMissionsForType; @@ -75,10 +84,6 @@ namespace Barotrauma public bool DisallowLocationTypeChanges; - public string BaseName { get => baseName; } - - public string Name { get; private set; } - public Biome Biome { get; set; } public Vector2 MapPosition { get; private set; } @@ -309,7 +314,7 @@ namespace Barotrauma if (!faction.IsEmpty && GameMain.GameSession.Campaign.GetFactionAffiliation(faction) is FactionAffiliation.Positive) { price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplierAffiliated, includeSaved: false)); - price *= 1f - characters.Max(static c => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplierAffiliated, new Identifier("all"))); + price *= 1f - characters.Max(static c => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplierAffiliated, Tags.StatIdentifierTargetAll)); price *= 1f - characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplierAffiliated, tag))); } price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplier, includeSaved: false)); @@ -484,7 +489,7 @@ namespace Barotrauma { if (missionIndex < 0 || missionIndex >= availableMissions.Count) { - DebugConsole.ThrowError($"Failed to select a mission in location \"{Name}\". Mission index out of bounds ({missionIndex}, available missions: {availableMissions.Count})"); + DebugConsole.ThrowError($"Failed to select a mission in location \"{DisplayName}\". Mission index out of bounds ({missionIndex}, available missions: {availableMissions.Count})"); break; } selectedMissions.Add(availableMissions[missionIndex]); @@ -536,15 +541,15 @@ namespace Barotrauma public override string ToString() { - return $"Location ({Name ?? "null"})"; + return $"Location ({DisplayName ?? "null"})"; } public Location(Vector2 mapPosition, int? zone, Random rand, bool requireOutpost = false, LocationType forceLocationType = null, IEnumerable existingLocations = null) { Type = OriginalType = forceLocationType ?? LocationType.Random(rand, zone, requireOutpost); - Name = RandomName(Type, rand, existingLocations); + CreateRandomName(Type, rand, existingLocations); MapPosition = mapPosition; - PortraitId = ToolBox.StringToInt(Name); + PortraitId = ToolBox.StringToInt(nameIdentifier.Value); Connections = new List(); } @@ -561,9 +566,21 @@ namespace Barotrauma GetTypeOrFallback(originalLocationTypeId, out LocationType originalType); OriginalType = originalType; - baseName = element.GetAttributeString("basename", ""); - Name = element.GetAttributeString("name", ""); - MapPosition = element.GetAttributeVector2("position", Vector2.Zero); + nameIdentifier = element.GetAttributeIdentifier(nameof(nameIdentifier), ""); + if (nameIdentifier.IsEmpty) + { + //backwards compatibility + rawName = element.GetAttributeString("basename", ""); + nameIdentifier = rawName.ToIdentifier(); + DisplayName = element.GetAttributeString("name", ""); + } + else + { + nameFormatIndex = element.GetAttributeInt(nameof(nameFormatIndex), 0); + DisplayName = GetName(Type, nameFormatIndex, nameIdentifier); + } + + MapPosition = element.GetAttributeVector2("position", Vector2.Zero); PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 1.0f); IsGateBetweenBiomes = element.GetAttributeBool("isgatebetweenbiomes", false); @@ -639,9 +656,9 @@ namespace Barotrauma System.Diagnostics.Debug.Assert(Type != null, $"Could not find the location type \"{locationTypeId}\"!"); Type ??= LocationType.Prefabs.First(); - LevelData = new LevelData(element.Element("Level"), clampDifficultyToBiome: true); + LevelData = new LevelData(element.GetChildElement("Level"), clampDifficultyToBiome: true); - PortraitId = ToolBox.StringToInt(Name); + PortraitId = ToolBox.StringToInt(!rawName.IsNullOrEmpty() ? rawName : nameIdentifier.Value); LoadStores(element); LoadMissions(element); @@ -659,15 +676,12 @@ namespace Barotrauma if (type == null) { DebugConsole.AddWarning($"Could not find location type \"{identifier}\". Using location type \"None\" instead."); - LocationType.Prefabs.TryGet("None".ToIdentifier(), out type); - if (type == null) - { - type = LocationType.Prefabs.First(); - } + LocationType.Prefabs.TryGet("None".ToIdentifier(), out type); + type ??= LocationType.Prefabs.First(); } if (type != null) { - element.SetAttributeValue("type", type.Identifier); + element.SetAttributeValue("type", type.Identifier.ToString()); } return false; } @@ -690,7 +704,7 @@ namespace Barotrauma int locationTypeChangeIndex = subElement.GetAttributeInt("index", 0); if (locationTypeChangeIndex < 0 || locationTypeChangeIndex >= Type.CanChangeTo.Count) { - DebugConsole.AddWarning($"Failed to activate a location type change in the location \"{Name}\". Location index out of bounds ({locationTypeChangeIndex})."); + DebugConsole.AddWarning($"Failed to activate a location type change in the location \"{DisplayName}\". Location index out of bounds ({locationTypeChangeIndex})."); continue; } PendingLocationTypeChange = (Type.CanChangeTo[locationTypeChangeIndex], timer, null); @@ -701,7 +715,7 @@ namespace Barotrauma var mission = MissionPrefab.Prefabs[missionIdentifier]; if (mission == null) { - DebugConsole.AddWarning($"Failed to activate a location type change from the mission \"{missionIdentifier}\" in location \"{Name}\". Matching mission not found."); + DebugConsole.AddWarning($"Failed to activate a location type change from the mission \"{missionIdentifier}\" in location \"{DisplayName}\". Matching mission not found."); continue; } PendingLocationTypeChange = (mission.LocationTypeChangeOnCompleted, timer, mission); @@ -738,14 +752,27 @@ namespace Barotrauma if (newType == null) { - DebugConsole.ThrowError($"Failed to change the type of the location \"{Name}\" to null.\n" + Environment.StackTrace.CleanupStackTrace()); + DebugConsole.ThrowError($"Failed to change the type of the location \"{DisplayName}\" to null.\n" + Environment.StackTrace.CleanupStackTrace()); return; } - DebugConsole.Log("Location " + baseName + " changed it's type from " + Type + " to " + newType); - Type = newType; - Name = Type.NameFormats == null || !Type.NameFormats.Any() ? baseName : Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", baseName); + if (rawName != null) + { + DebugConsole.Log($"Location {rawName} changed it's type from {Type} to {newType}"); + DisplayName = + Type.NameFormats == null || !Type.NameFormats.Any() ? + rawName : + Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", rawName); + } + else + { + DebugConsole.Log($"Location {DisplayName.Value} changed it's type from {Type} to {newType}"); + DisplayName = + Type.NameFormats == null || !Type.NameFormats.Any() ? + TextManager.Get(nameIdentifier) : + Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", TextManager.Get(nameIdentifier).Value); + } if (Type.HasOutpost && Type.OutpostTeam == CharacterTeamType.FriendlyNPC) { @@ -776,11 +803,11 @@ namespace Barotrauma { if (Type.MissionIdentifiers.Any()) { - UnlockMissionByIdentifier(Type.MissionIdentifiers.GetRandom(randSync)); + UnlockMissionByIdentifier(Type.MissionIdentifiers.GetRandom(randSync), invokingContentPackage: Type.ContentPackage); } if (Type.MissionTags.Any()) { - UnlockMissionByTag(Type.MissionTags.GetRandom(randSync)); + UnlockMissionByTag(Type.MissionTags.GetRandom(randSync), invokingContentPackage: Type.ContentPackage); } } @@ -798,7 +825,7 @@ namespace Barotrauma AddMission(InstantiateMission(missionPrefab)); } - public Mission UnlockMissionByIdentifier(Identifier identifier) + public Mission UnlockMissionByIdentifier(Identifier identifier, ContentPackage invokingContentPackage = null) { if (AvailableMissions.Any(m => m.Prefab.Identifier == identifier)) { return null; } if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return null; } @@ -806,7 +833,8 @@ namespace Barotrauma var missionPrefab = MissionPrefab.Prefabs.Find(mp => mp.Identifier == identifier); if (missionPrefab == null) { - DebugConsole.ThrowError($"Failed to unlock a mission with the identifier \"{identifier}\": matching mission not found."); + DebugConsole.ThrowError($"Failed to unlock a mission with the identifier \"{identifier}\": matching mission not found.", + contentPackage: invokingContentPackage); } else { @@ -823,13 +851,13 @@ namespace Barotrauma return null; } - public Mission UnlockMissionByTag(Identifier tag, Random random = null) + public Mission UnlockMissionByTag(Identifier tag, Random random = null, ContentPackage invokingContentPackage = null) { if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return null; } var matchingMissions = MissionPrefab.Prefabs.Where(mp => mp.Tags.Contains(tag)); if (matchingMissions.None()) { - DebugConsole.ThrowError($"Failed to unlock a mission with the tag \"{tag}\": no matching missions found."); + DebugConsole.ThrowError($"Failed to unlock a mission with the tag \"{tag}\": no matching missions found.", contentPackage: invokingContentPackage); } else { @@ -841,7 +869,16 @@ namespace Barotrauma { suitableMissions = unusedMissions; } - + var filteredMissions = suitableMissions.Where(m => LevelData.Difficulty >= m.MinLevelDifficulty && LevelData.Difficulty <= m.MaxLevelDifficulty); + if (filteredMissions.None()) + { + DebugConsole.AddWarning($"No suitable mission matching the level difficulty {LevelData.Difficulty} found with the tag \"{tag}\". Ignoring the restriction.", + contentPackage: invokingContentPackage); + } + else + { + suitableMissions = filteredMissions; + } MissionPrefab missionPrefab = random != null ? ToolBox.SelectWeightedRandom(suitableMissions.OrderBy(m => m.Identifier), m => m.Commonness, random) : @@ -854,12 +891,13 @@ namespace Barotrauma return null; } AddMission(mission); - DebugConsole.NewMessage($"Unlocked a random mission by \"{tag}\".", debugOnly: true); + DebugConsole.NewMessage($"Unlocked a random mission by \"{tag}\": {mission.Prefab.Identifier} (difficulty level: {LevelData.Difficulty})", debugOnly: true); return mission; } else { - DebugConsole.AddWarning($"Failed to unlock a mission with the tag \"{tag}\": all available missions have already been unlocked."); + DebugConsole.AddWarning($"Failed to unlock a mission with the tag \"{tag}\": all available missions have already been unlocked.", + contentPackage: invokingContentPackage); } } @@ -988,11 +1026,11 @@ namespace Barotrauma { if (addInitialMissionsForType.MissionIdentifiers.Any()) { - UnlockMissionByIdentifier(addInitialMissionsForType.MissionIdentifiers.GetRandomUnsynced()); + UnlockMissionByIdentifier(addInitialMissionsForType.MissionIdentifiers.GetRandomUnsynced(), invokingContentPackage: Type.ContentPackage); } if (addInitialMissionsForType.MissionTags.Any()) { - UnlockMissionByTag(addInitialMissionsForType.MissionTags.GetRandomUnsynced()); + UnlockMissionByTag(addInitialMissionsForType.MissionTags.GetRandomUnsynced(), invokingContentPackage: Type.ContentPackage); } addInitialMissionsForType = null; } @@ -1050,12 +1088,12 @@ namespace Barotrauma { if (!Type.HasHireableCharacters) { - DebugConsole.ThrowError("Cannot hire a character from location \"" + Name + "\" - the location has no hireable characters.\n" + Environment.StackTrace.CleanupStackTrace()); + DebugConsole.ThrowError("Cannot hire a character from location \"" + DisplayName + "\" - the location has no hireable characters.\n" + Environment.StackTrace.CleanupStackTrace()); return; } if (HireManager == null) { - DebugConsole.ThrowError("Cannot hire a character from location \"" + Name + "\" - hire manager has not been instantiated.\n" + Environment.StackTrace.CleanupStackTrace()); + DebugConsole.ThrowError("Cannot hire a character from location \"" + DisplayName + "\" - hire manager has not been instantiated.\n" + Environment.StackTrace.CleanupStackTrace()); return; } @@ -1078,22 +1116,52 @@ namespace Barotrauma return HireManager.AvailableCharacters; } - private string RandomName(LocationType type, Random rand, IEnumerable existingLocations) + private void CreateRandomName(LocationType type, Random rand, IEnumerable existingLocations) { - if (!type.ForceLocationName.IsNullOrEmpty()) + if (!type.ForceLocationName.IsEmpty) { - baseName = type.ForceLocationName.Value; - return baseName; + nameIdentifier = type.ForceLocationName; + DisplayName = TextManager.Get(nameIdentifier).Fallback(nameIdentifier.Value); + return; + } + nameIdentifier = type.GetRandomNameId(rand, existingLocations); + if (nameIdentifier.IsEmpty) + { + rawName = type.GetRandomRawName(rand, existingLocations); + if (rawName.IsNullOrEmpty()) + { + DebugConsole.ThrowError($"Failed to generate a name for a location of the type {type.Identifier}. No names found in localization files or the .txt files."); + rawName = "none"; + } + nameIdentifier = rawName.ToIdentifier(); + DisplayName = rawName; + } + else + { + if (type.NameFormats == null || !type.NameFormats.Any()) + { + DisplayName = TextManager.Get(nameIdentifier).Fallback(nameIdentifier.Value); + return; + } + nameFormatIndex = rand.Next() % type.NameFormats.Count; + DisplayName = GetName(Type, nameFormatIndex, nameIdentifier); } - baseName = type.GetRandomName(rand, existingLocations); - if (type.NameFormats == null || !type.NameFormats.Any()) { return baseName; } - nameFormatIndex = rand.Next() % type.NameFormats.Count; - return type.NameFormats[nameFormatIndex].Replace("[name]", baseName); } - public void ForceName(string name) + private static LocalizedString GetName(LocationType type, int nameFormatIndex, Identifier nameId) { - baseName = Name = name; + if (type?.NameFormats == null || !type.NameFormats.Any()) + { + return TextManager.Get(nameId); + } + return type.NameFormats[nameFormatIndex % type.NameFormats.Count].Replace("[name]", TextManager.Get(nameId).Value); + } + + public void ForceName(Identifier nameId) + { + rawName = string.Empty; + nameIdentifier = nameId; + DisplayName = TextManager.Get(nameId).Fallback(nameId.Value); } public void LoadStores(XElement locationElement) @@ -1117,7 +1185,7 @@ namespace Barotrauma } else { - string msg = $"Error loading store info for \"{identifier}\" at location {Name} of type \"{Type.Identifier}\": duplicate identifier."; + string msg = $"Error loading store info for \"{identifier}\" at location {DisplayName} of type \"{Type.Identifier}\": duplicate identifier."; DebugConsole.ThrowError(msg); GameAnalyticsManager.AddErrorEventOnce("Location.LoadStore:DuplicateStoreInfo", GameAnalyticsManager.ErrorSeverity.Error, msg); continue; @@ -1125,7 +1193,7 @@ namespace Barotrauma } else { - string msg = $"Error loading store info for \"{identifier}\" at location {Name} of type \"{Type.Identifier}\": location shouldn't contain a store with this identifier."; + string msg = $"Error loading store info for \"{identifier}\" at location {DisplayName} of type \"{Type.Identifier}\": location shouldn't contain a store with this identifier."; DebugConsole.ThrowError(msg); GameAnalyticsManager.AddErrorEventOnce("Location.LoadStore:IncorrectStoreIdentifier", GameAnalyticsManager.ErrorSeverity.Error, msg); continue; @@ -1436,8 +1504,9 @@ namespace Barotrauma var locationElement = new XElement("location", new XAttribute("type", Type.Identifier), new XAttribute("originaltype", (Type ?? OriginalType).Identifier), - new XAttribute("basename", BaseName), - new XAttribute("name", Name), + /*not used currently (we load the nameIdentifier instead), + * but could make sense to include still for backwards compatibility reasons*/ + new XAttribute("name", DisplayName), new XAttribute("biome", Biome?.Identifier.Value ?? string.Empty), new XAttribute("position", XMLExtensions.Vector2ToString(MapPosition)), new XAttribute("pricemultiplier", PriceMultiplier), @@ -1447,6 +1516,16 @@ namespace Barotrauma new XAttribute(nameof(TurnsInRadiation).ToLower(), TurnsInRadiation), new XAttribute("stepssincespecialsupdated", StepsSinceSpecialsUpdated)); + if (!rawName.IsNullOrEmpty()) + { + locationElement.Add(new XAttribute(nameof(rawName), rawName)); + } + else + { + locationElement.Add(new XAttribute(nameof(nameIdentifier), nameIdentifier)); + locationElement.Add(new XAttribute(nameof(nameFormatIndex), nameFormatIndex)); + } + if (Faction != null) { locationElement.Add(new XAttribute("faction", Faction.Prefab.Identifier)); @@ -1483,7 +1562,7 @@ namespace Barotrauma changeElement.Add(new XAttribute("index", index)); if (index == -1) { - DebugConsole.AddWarning($"Invalid location type change in the location \"{Name}\". Unknown type change ({PendingLocationTypeChange.Value.typeChange.ChangeToType})."); + DebugConsole.AddWarning($"Invalid location type change in the location \"{DisplayName}\". Unknown type change ({PendingLocationTypeChange.Value.typeChange.ChangeToType})."); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs index 475fd10e0..b300c4d02 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs @@ -1,4 +1,5 @@ -using Barotrauma.IO; +using Barotrauma.Extensions; +using Barotrauma.IO; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -13,7 +14,7 @@ namespace Barotrauma { public static readonly PrefabCollection Prefabs = new PrefabCollection(); - private readonly ImmutableArray names; + private readonly ImmutableArray rawNames; private readonly ImmutableArray portraits; // @@ -26,7 +27,7 @@ namespace Barotrauma public readonly LocalizedString Name; public readonly LocalizedString Description; - public readonly LocalizedString ForceLocationName; + public readonly Identifier ForceLocationName; public readonly float BeaconStationChance; @@ -54,12 +55,20 @@ namespace Barotrauma private set; } + private readonly ImmutableArray? nameIdentifiers = null; + + private LanguageIdentifier nameFormatLanguage; + private ImmutableArray? nameFormats = null; public IReadOnlyList NameFormats { get { - nameFormats ??= TextManager.GetAll($"LocationNameFormat.{Identifier}").ToImmutableArray(); + if (nameFormats == null || GameSettings.CurrentConfig.Language != nameFormatLanguage) + { + nameFormats = TextManager.GetAll($"LocationNameFormat.{Identifier}").ToImmutableArray(); + nameFormatLanguage = GameSettings.CurrentConfig.Language; + } return nameFormats; } } @@ -143,29 +152,37 @@ namespace Barotrauma if (element.GetAttribute("name") != null) { - ForceLocationName = TextManager.Get(element.GetAttributeString("name", string.Empty)); + ForceLocationName = element.GetAttributeIdentifier("name", string.Empty); } else { - string[] rawNamePaths = element.GetAttributeStringArray("namefile", new string[] { "Content/Map/locationNames.txt" }); var names = new List(); - foreach (string rawPath in rawNamePaths) + //backwards compatibility for location names defined in a text file + string[] rawNamePaths = element.GetAttributeStringArray("namefile", Array.Empty()); + if (rawNamePaths.Any()) { - try + foreach (string rawPath in rawNamePaths) { - var path = ContentPath.FromRaw(element.ContentPackage, rawPath.Trim()); - names.AddRange(File.ReadAllLines(path.Value).ToList()); + try + { + var path = ContentPath.FromRaw(element.ContentPackage, rawPath.Trim()); + names.AddRange(File.ReadAllLines(path.Value).ToList()); + } + catch (Exception e) + { + DebugConsole.ThrowError($"Failed to read name file \"rawPath\" for location type \"{Identifier}\"!", e); + } } - catch (Exception e) + if (!names.Any()) { - DebugConsole.ThrowError($"Failed to read name file \"rawPath\" for location type \"{Identifier}\"!", e); + names.Add("ERROR: No names found"); } + this.rawNames = names.ToImmutableArray(); } - if (!names.Any()) + else { - names.Add("ERROR: No names found"); + nameIdentifiers = element.GetAttributeIdentifierArray("nameidentifiers", new Identifier[] { Identifier }).ToImmutableArray(); } - this.names = names.ToImmutableArray(); } string[] commonnessPerZoneStrs = element.GetAttributeStringArray("commonnessperzone", Array.Empty()); @@ -259,17 +276,64 @@ namespace Barotrauma return portraits[Math.Abs(randomSeed) % portraits.Length]; } - public string GetRandomName(Random rand, IEnumerable existingLocations) + public Identifier GetRandomNameId(Random rand, IEnumerable existingLocations) { + if (nameIdentifiers == null) + { + return Identifier.Empty; + } + List nameIds = new List(); + foreach (var nameId in nameIdentifiers) + { + int index = 0; + while (true) + { + Identifier tag = $"LocationName.{nameId}.{index}".ToIdentifier(); + if (TextManager.ContainsTag(tag, TextManager.DefaultLanguage)) + { + nameIds.Add(tag); + index++; + } + else + { + if (index == 0) + { + DebugConsole.ThrowError($"Could not find any location names for the location type {Identifier}. Name identifier: {nameId}"); + } + break; + } + } + } + if (nameIds.None()) + { + return Identifier.Empty; + } if (existingLocations != null) { - var unusedNames = names.Where(name => !existingLocations.Any(l => l.BaseName == name)).ToList(); + var unusedNameIds = nameIds.FindAll(nameId => existingLocations.None(l => l.NameIdentifier == nameId)); + if (unusedNameIds.Count > 0) + { + return unusedNameIds[rand.Next() % unusedNameIds.Count]; + } + } + return nameIds[rand.Next() % nameIds.Count]; + } + + /// + /// For backwards compatibility. Chooses a random name from the names defined in the .txt name files (). + /// + public string GetRandomRawName(Random rand, IEnumerable existingLocations) + { + if (rawNames == null || rawNames.None()) { return string.Empty; } + if (existingLocations != null) + { + var unusedNames = rawNames.Where(name => !existingLocations.Any(l => l.DisplayName.Value == name)).ToList(); if (unusedNames.Count > 0) { return unusedNames[rand.Next() % unusedNames.Count]; } } - return names[rand.Next() % names.Length]; + return rawNames[rand.Next() % rawNames.Length]; } public static LocationType Random(Random rand, int? zone = null, bool requireOutpost = false, Func predicate = null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs index 8d9e03673..94377cf34 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs @@ -55,7 +55,7 @@ namespace Barotrauma /// public readonly bool RequireHuntingGrounds; - public Requirement(XElement element, LocationTypeChange change) + public Requirement(ContentXElement element, LocationTypeChange change) { RequiredLocations = element.GetAttributeIdentifierArray("requiredlocations", element.GetAttributeIdentifierArray("requiredadjacentlocations", Array.Empty())).ToImmutableArray(); RequiredProximity = Math.Max(element.GetAttributeInt("requiredproximity", 1), 0); @@ -80,13 +80,15 @@ namespace Barotrauma { DebugConsole.AddWarning( $"Invalid location type change in location type \"{change.CurrentType}\". " + - "Probability is configured to increase when near some other type of location, but the RequiredLocations attribute is not set."); + "Probability is configured to increase when near some other type of location, but the RequiredLocations attribute is not set.", + element.ContentPackage); } if (Probability >= 1.0f) { DebugConsole.AddWarning( $"Invalid location type change in location type \"{change.CurrentType}\". " + - "Probability is configured to increase when near some other type of location, but the base probability is already 100%"); + "Probability is configured to increase when near some other type of location, but the base probability is already 100%", + element.ContentPackage); } } } @@ -173,7 +175,7 @@ namespace Barotrauma public readonly Point RequiredDurationRange; - public LocationTypeChange(Identifier currentType, XElement element, bool requireChangeMessages, float defaultProbability = 0.0f) + public LocationTypeChange(Identifier currentType, ContentXElement element, bool requireChangeMessages, float defaultProbability = 0.0f) { CurrentType = currentType; ChangeToType = element.GetAttributeIdentifier("type", element.GetAttributeIdentifier("to", "")); @@ -190,13 +192,13 @@ namespace Barotrauma CooldownAfterChange = Math.Max(element.GetAttributeInt("cooldownafterchange", 0), 0); //backwards compatibility - if (element.Attribute("requiredlocations") != null) + if (element.GetAttribute("requiredlocations") != null) { Requirements.Add(new Requirement(element, this)); } //backwards compatibility - if (element.Attribute("requiredduration") != null) + if (element.GetAttribute("requiredduration") != null) { RequiredDurationRange = new Point(element.GetAttributeInt("requiredduration", 0)); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index bbb70a021..1934e9c69 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -262,9 +262,9 @@ namespace Barotrauma foreach (var endLocation in EndLocations) { if (endLocation.Type?.ForceLocationName != null && - !endLocation.Type.ForceLocationName.IsNullOrEmpty()) + !endLocation.Type.ForceLocationName.IsEmpty) { - endLocation.ForceName(endLocation.Type.ForceLocationName.Value); + endLocation.ForceName(endLocation.Type.ForceLocationName); } } @@ -1005,10 +1005,10 @@ namespace Barotrauma CurrentLocation.CreateStores(); OnLocationChanged?.Invoke(new LocationChangeInfo(prevLocation, CurrentLocation)); - if (GameMain.GameSession is { Campaign: { CampaignMetadata: { } metadata } }) + if (GameMain.GameSession is { Campaign.CampaignMetadata: { } metadata }) { metadata.SetValue("campaign.location.id".ToIdentifier(), CurrentLocationIndex); - metadata.SetValue("campaign.location.name".ToIdentifier(), CurrentLocation.Name); + metadata.SetValue("campaign.location.name".ToIdentifier(), CurrentLocation.NameIdentifier.Value); metadata.SetValue("campaign.location.biome".ToIdentifier(), CurrentLocation.Biome?.Identifier ?? "null".ToIdentifier()); metadata.SetValue("campaign.location.type".ToIdentifier(), CurrentLocation.Type?.Identifier ?? "null".ToIdentifier()); } @@ -1077,7 +1077,7 @@ namespace Barotrauma if (SelectedConnection?.Locked ?? false) { string errorMsg = - $"A locked connection was selected ({SelectedConnection.Locations[0].Name} -> {SelectedConnection.Locations[1].Name}." + + $"A locked connection was selected ({SelectedConnection.Locations[0].DisplayName} -> {SelectedConnection.Locations[1].DisplayName}." + $" Current location: {CurrentLocation}, current display location: {currentDisplayLocation}).\n" + Environment.StackTrace.CleanupStackTrace(); GameAnalyticsManager.AddErrorEventOnce("MapSelectLocation:LockedConnectionSelected", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); @@ -1093,7 +1093,7 @@ namespace Barotrauma { if (!Locations.Contains(location)) { - string errorMsg = "Failed to select a location. " + (location?.Name ?? "null") + " not found in the map."; + string errorMsg = $"Failed to select a location. {location?.DisplayName ?? "null"} not found in the map."; DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("Map.SelectLocation:LocationNotFound", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; @@ -1301,11 +1301,11 @@ namespace Barotrauma private bool ChangeLocationType(CampaignMode campaign, Location location, LocationTypeChange change) { - string prevName = location.Name; + LocalizedString prevName = location.DisplayName; if (!LocationType.Prefabs.TryGet(change.ChangeToType, out var newType)) { - DebugConsole.ThrowError($"Failed to change the type of the location \"{location.Name}\". Location type \"{change.ChangeToType}\" not found."); + DebugConsole.ThrowError($"Failed to change the type of the location \"{location.DisplayName}\". Location type \"{change.ChangeToType}\" not found."); return false; } @@ -1372,7 +1372,7 @@ namespace Barotrauma } - partial void ChangeLocationTypeProjSpecific(Location location, string prevName, LocationTypeChange change); + partial void ChangeLocationTypeProjSpecific(Location location, LocalizedString prevName, LocationTypeChange change); partial void ClearAnimQueue(); @@ -1498,7 +1498,7 @@ namespace Barotrauma } Identifier locationType = subElement.GetAttributeIdentifier("type", Identifier.Empty); - string prevLocationName = location.Name; + LocalizedString prevLocationName = location.DisplayName; LocationType prevLocationType = location.Type; LocationType newLocationType = LocationType.Prefabs.Find(lt => lt.Identifier == locationType) ?? LocationType.Prefabs.First(); location.ChangeType(campaign, newLocationType); @@ -1619,7 +1619,7 @@ namespace Barotrauma //this should not be possible, you can't enter non-outpost locations (= natural formations) if (CurrentLocation != null && !CurrentLocation.Type.HasOutpost && SelectedConnection == null) { - DebugConsole.AddWarning($"Error while loading campaign map state. Submarine in a location with no outpost ({CurrentLocation.Name}). Loading the first adjacent connection..."); + DebugConsole.AddWarning($"Error while loading campaign map state. Submarine in a location with no outpost ({CurrentLocation.DisplayName}). Loading the first adjacent connection..."); SelectLocation(CurrentLocation.Connections[0].OtherLocation(CurrentLocation)); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index 39f35865a..657c3c83f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -576,15 +576,10 @@ namespace Barotrauma base.Remove(); MapEntityList.Remove(this); - #if CLIENT Submarine.ForceRemoveFromVisibleEntities(this); - if (SelectedList.Contains(this)) - { - SelectedList = SelectedList.Where(e => e != this).ToHashSet(); - } + SelectedList.Remove(this); #endif - if (aiTarget != null) { aiTarget.Remove(); @@ -686,6 +681,9 @@ namespace Barotrauma Move(-relative * 2.0f); } + public virtual Quad2D GetTransformedQuad() + => Quad2D.FromSubmarineRectangle(rect); + public static List LoadAll(Submarine submarine, XElement parentElement, string filePath, int idOffset) { IdRemap idRemap = new IdRemap(parentElement, idOffset); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/BeaconStationInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/ExtraSubmarineInfo.cs similarity index 54% rename from Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/BeaconStationInfo.cs rename to Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/ExtraSubmarineInfo.cs index feec0ea3d..c7b6155c7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/BeaconStationInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/ExtraSubmarineInfo.cs @@ -3,13 +3,11 @@ using System.Xml.Linq; namespace Barotrauma { - class BeaconStationInfo : ISerializableEntity + abstract class ExtraSubmarineInfo : ISerializableEntity { - [Serialize(true, IsPropertySaveable.Yes), Editable] - public bool AllowDamagedWalls { get; set; } + public string Name { get; protected set; } - [Serialize(true, IsPropertySaveable.Yes), Editable] - public bool AllowDisconnectedWires { get; set; } + public Dictionary SerializableProperties { get; protected set; } [Serialize(0.0f, IsPropertySaveable.Yes), Editable] public float MinLevelDifficulty { get; set; } @@ -17,26 +15,19 @@ namespace Barotrauma [Serialize(100.0f, IsPropertySaveable.Yes), Editable] public float MaxLevelDifficulty { get; set; } - [Serialize(Level.PlacementType.Bottom, IsPropertySaveable.Yes), Editable] - public Level.PlacementType Placement { get; set; } - - public string Name { get; private set; } - - public Dictionary SerializableProperties { get; private set; } - - public BeaconStationInfo(SubmarineInfo submarineInfo, XElement element) + public ExtraSubmarineInfo(SubmarineInfo submarineInfo, XElement element) { - Name = $"BeaconStationInfo ({submarineInfo.Name})"; + Name = $"{nameof(ExtraSubmarineInfo)} ({submarineInfo.Name})"; SerializableProperties = SerializableProperty.DeserializeProperties(this, element); } - public BeaconStationInfo(SubmarineInfo submarineInfo) + public ExtraSubmarineInfo(SubmarineInfo submarineInfo) { - Name = $"BeaconStationInfo ({submarineInfo.Name})"; + Name = $"{nameof(ExtraSubmarineInfo)} ({submarineInfo.Name})"; SerializableProperties = SerializableProperty.DeserializeProperties(this); } - public BeaconStationInfo(BeaconStationInfo original) + public ExtraSubmarineInfo(ExtraSubmarineInfo original) { Name = original.Name; SerializableProperties = new Dictionary(); @@ -55,4 +46,43 @@ namespace Barotrauma SerializableProperty.SerializeProperties(this, element); } } + + class BeaconStationInfo : ExtraSubmarineInfo + { + [Serialize(true, IsPropertySaveable.Yes), Editable] + public bool AllowDamagedWalls { get; set; } + + [Serialize(true, IsPropertySaveable.Yes), Editable] + public bool AllowDisconnectedWires { get; set; } + + [Serialize(Level.PlacementType.Bottom, IsPropertySaveable.Yes), Editable] + public Level.PlacementType Placement { get; set; } + + public BeaconStationInfo(SubmarineInfo submarineInfo, XElement element) : base(submarineInfo, element) + { + Name = $"{nameof(BeaconStationInfo)} ({submarineInfo.Name})"; + } + + public BeaconStationInfo(SubmarineInfo submarineInfo) : base(submarineInfo) + { + Name = $"{nameof(BeaconStationInfo)} ({submarineInfo.Name})"; + } + + public BeaconStationInfo(BeaconStationInfo original) : base(original) { } + } + + class WreckInfo : ExtraSubmarineInfo + { + public WreckInfo(SubmarineInfo submarineInfo, XElement element) : base(submarineInfo, element) + { + Name = $"{nameof(WreckInfo)} ({submarineInfo.Name})"; + } + + public WreckInfo(SubmarineInfo submarineInfo) : base(submarineInfo) + { + Name = $"{nameof(WreckInfo)} ({submarineInfo.Name})"; + } + + public WreckInfo(WreckInfo original) : base(original) { } + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs index b55acd04e..7bb55ccd6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs @@ -249,7 +249,7 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"Error in outpost generation parameters \"{Identifier}\". \"{levelTypeStr}\" is not a valid level type."); + DebugConsole.ThrowError($"Error in outpost generation parameters \"{Identifier}\". \"{levelTypeStr}\" is not a valid level type.", contentPackage: element.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index 2d4ce70e3..afbdb5271 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -111,7 +111,7 @@ namespace Barotrauma set; } - public bool IsHorizontal { get; private set; } + public bool IsHorizontal { get; } public int SectionCount { @@ -240,6 +240,25 @@ namespace Barotrauma } } + protected float rotationRad = 0f; + [ConditionallyEditable(ConditionallyEditable.ConditionType.AllowRotating, DecimalCount = 3, ForceShowPlusMinusButtons = true, ValueStep = 0.1f), Serialize(0.0f, IsPropertySaveable.Yes)] + public float Rotation + { + get => MathHelper.ToDegrees(rotationRad); + set + { + rotationRad = MathHelper.WrapAngle(MathHelper.ToRadians(value)); + if (StairDirection != Direction.None) + { + CreateStairBodies(); + } + else if (Prefab.Body) + { + CreateSections(); + UpdateSections(); + } + } + } protected Vector2 textureScale = Vector2.One; @@ -336,9 +355,18 @@ namespace Barotrauma { get { - float rotation = MathHelper.ToRadians(Prefab.BodyRotation); - if (FlippedX) rotation = -MathHelper.Pi - rotation; - if (FlippedY) rotation = -rotation; + float rotation = MathHelper.ToRadians(Prefab.BodyRotation) + this.rotationRad; + if (IsHorizontal) + { + if (FlippedX) { rotation = -MathHelper.Pi - rotation; } + if (FlippedY) { rotation = -rotation; } + } + else + { + if (FlippedX) { rotation = -rotation; } + if (FlippedY) { rotation = -MathHelper.Pi -rotation; } + } + rotation = MathHelper.WrapAngle(rotation); return rotation; } } @@ -350,6 +378,10 @@ namespace Barotrauma get { Vector2 bodyOffset = Prefab.BodyOffset; + if (rotationRad != 0f) + { + bodyOffset = MathUtils.RotatePoint(bodyOffset, -rotationRad); + } if (FlippedX) { bodyOffset.X = -bodyOffset.X; } if (FlippedY) { bodyOffset.Y = -bodyOffset.Y; } return bodyOffset; @@ -567,9 +599,14 @@ namespace Barotrauma Body newBody = GameMain.World.CreateRectangle(bodyWidth, bodyHeight, 1.5f); + var rotationWithFlip = FlippedX ^ FlippedY ? -rotationRad : rotationRad; + newBody.BodyType = BodyType.Static; - Vector2 stairPos = new Vector2(Position.X, rect.Y - rect.Height + stairHeight / 2.0f); - newBody.Rotation = (StairDirection == Direction.Right) ? stairAngle : -stairAngle; + Vector2 stairRectHeightDiff = new Vector2(0f, stairHeight / 2.0f - rect.Height / 2.0f); + stairRectHeightDiff = MathUtils.RotatePoint(stairRectHeightDiff, -rotationWithFlip); + if (FlippedY) { stairRectHeightDiff = -stairRectHeightDiff; } + Vector2 stairPos = new Vector2(Position.X, rect.Y - rect.Height / 2.0f) + stairRectHeightDiff; + newBody.Rotation = ((StairDirection == Direction.Right) ? stairAngle : -stairAngle) - rotationWithFlip; newBody.CollisionCategories = Physics.CollisionStairs; newBody.Friction = 0.8f; newBody.UserData = this; @@ -696,16 +733,11 @@ namespace Barotrauma } } - private static Vector2[] CalculateExtremes(Rectangle sectionRect) - { - Vector2[] corners = new Vector2[4]; - corners[0] = new Vector2(sectionRect.X, sectionRect.Y - sectionRect.Height); - corners[1] = new Vector2(sectionRect.X, sectionRect.Y); - corners[2] = new Vector2(sectionRect.Right, sectionRect.Y); - corners[3] = new Vector2(sectionRect.Right, sectionRect.Y - sectionRect.Height); - - return corners; - } + public override Quad2D GetTransformedQuad() + => Quad2D.FromSubmarineRectangle(rect).Rotated( + FlippedX != FlippedY + ? rotationRad + : -rotationRad); /// /// Checks if there's a structure items can be attached to at the given position and returns it. @@ -727,8 +759,6 @@ namespace Barotrauma public override bool IsMouseOn(Vector2 position) { - if (!base.IsMouseOn(position)) { return false; } - if (StairDirection == Direction.None) { Vector2 rectSize = rect.Size.ToVector2(); @@ -745,14 +775,19 @@ namespace Barotrauma } else { + Vector2 transformedMousePos = MathUtils.RotatePointAroundTarget( + position, + WorldRect.Location.ToVector2() + WorldRect.Size.ToVector2().FlipY() * 0.5f, + BodyRotation); + if (!Submarine.RectContains(WorldRect, position)) { return false; } if (StairDirection == Direction.Left) { - return MathUtils.LineToPointDistanceSquared(new Vector2(WorldRect.X, WorldRect.Y), new Vector2(WorldRect.Right, WorldRect.Y - WorldRect.Height), position) < 1600.0f; + return MathUtils.LineToPointDistanceSquared(new Vector2(WorldRect.X, WorldRect.Y), new Vector2(WorldRect.Right, WorldRect.Y - WorldRect.Height), transformedMousePos) < 1600.0f; } else { - return MathUtils.LineToPointDistanceSquared(new Vector2(WorldRect.X, WorldRect.Y - rect.Height), new Vector2(WorldRect.Right, WorldRect.Y), position) < 1600.0f; + return MathUtils.LineToPointDistanceSquared(new Vector2(WorldRect.X, WorldRect.Y - rect.Height), new Vector2(WorldRect.Right, WorldRect.Y), transformedMousePos) < 1600.0f; } } } @@ -883,6 +918,12 @@ namespace Barotrauma return Sections[sectionIndex].damage >= MaxHealth * LeakThreshold; } + public bool SectionIsLeakingFromOutside(int sectionIndex) + { + if (sectionIndex < 0 || sectionIndex >= Sections.Length) { return false; } + return SectionIsLeaking(sectionIndex) && !Sections[sectionIndex].gap.IsRoomToRoom; + } + public int SectionLength(int sectionIndex) { if (sectionIndex < 0 || sectionIndex >= Sections.Length) return 0; @@ -934,11 +975,19 @@ namespace Barotrauma for (int i = 1; i <= particleAmount; i++) { var worldRect = section.WorldRect; + var directionUnitX = MathUtils.RotatedUnitXRadians(BodyRotation); + var directionUnitY = directionUnitX.YX().FlipX(); Vector2 particlePos = new Vector2( - Rand.Range(worldRect.X, worldRect.Right + 1), - Rand.Range(worldRect.Y - worldRect.Height, worldRect.Y + 1)); + Rand.Range(0, worldRect.Width + 1), + Rand.Range(-worldRect.Height, 1)); + particlePos -= worldRect.Size.ToVector2().FlipY() * 0.5f; - var particle = GameMain.ParticleManager.CreateParticle(Prefab.DamageParticle, particlePos, Rand.Vector(Rand.Range(1.0f, 50.0f)), collisionIgnoreTimer: 1f); + var particlePosFinal = SectionPosition(sectionIndex, world: true); + particlePosFinal += particlePos.X * directionUnitX + particlePos.Y * directionUnitY; + + var particle = GameMain.ParticleManager.CreateParticle(Prefab.DamageParticle, + position: particlePosFinal, + velocity: Rand.Vector(Rand.Range(1.0f, 50.0f)), collisionIgnoreTimer: 1f); if (particle == null) break; } } @@ -960,14 +1009,23 @@ namespace Barotrauma //if the sub has been flipped horizontally, the first section may be smaller than wallSectionSize //and we need to adjust the position accordingly - if (Sections[0].rect.Width < WallSectionSize) + if (IsHorizontal) { - displayPos.X += WallSectionSize - Sections[0].rect.Width; + if (Sections[0].rect.Width < WallSectionSize) + { + displayPos += DirectionUnit * (WallSectionSize - Sections[0].rect.Width); + } + } + else + { + if (Sections[0].rect.Height < WallSectionSize) + { + displayPos += DirectionUnit * (WallSectionSize - Sections[0].rect.Height); + } } - int index = IsHorizontal ? - (int)Math.Floor((displayPos.X - rect.X) / WallSectionSize) : - (int)Math.Floor((rect.Y - displayPos.Y) / WallSectionSize); + var leftmostPos = Position - DirectionUnit * (IsHorizontal ? Rect.Width : Rect.Height) * 0.5f; + int index = (int)Math.Floor(Vector2.Dot(DirectionUnit, displayPos - leftmostPos) / WallSectionSize); if (clamp) { @@ -987,6 +1045,17 @@ namespace Barotrauma return Sections[sectionIndex].damage; } + protected Vector2 DirectionUnit + { + get + { + var rotation = IsHorizontal ? -BodyRotation : -MathHelper.PiOver2 - BodyRotation; + if (IsHorizontal && FlippedX) { rotation += MathF.PI; } + if (!IsHorizontal && FlippedY) { rotation += MathF.PI; } + return MathUtils.RotatedUnitXRadians(rotation); + } + } + public Vector2 SectionPosition(int sectionIndex, bool world = false) { if (sectionIndex < 0 || sectionIndex >= Sections.Length) @@ -994,7 +1063,7 @@ namespace Barotrauma return Vector2.Zero; } - if (Prefab.BodyRotation == 0.0f) + if (MathUtils.NearlyEqual(BodyRotation, 0f)) { Vector2 sectionPos = new Vector2( Sections[sectionIndex].rect.X + Sections[sectionIndex].rect.Width / 2.0f, @@ -1017,15 +1086,10 @@ namespace Barotrauma else { diffFromCenter = ((sectionRect.Y - sectionRect.Height / 2) - (rect.Y - rect.Height / 2)) / (float)rect.Height * BodyHeight; - } - if (FlippedX) - { diffFromCenter = -diffFromCenter; } - Vector2 sectionPos = Position + new Vector2( - (float)Math.Cos(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation), - (float)Math.Sin(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation)) * diffFromCenter; + Vector2 sectionPos = Position + DirectionUnit * diffFromCenter; if (world && Submarine != null) { @@ -1035,13 +1099,22 @@ namespace Barotrauma } } - public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = false) + public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, Vector2 impulseDirection, float deltaTime, bool playSound = false) { if (Submarine != null && Submarine.GodMode) { return new AttackResult(0.0f, null); } if (!Prefab.Body || Prefab.Platform || Indestructible) { return new AttackResult(0.0f, null); } Vector2 transformedPos = worldPosition; - if (Submarine != null) transformedPos -= Submarine.Position; + if (Submarine != null) { transformedPos -= Submarine.Position; } + + if (!MathUtils.NearlyEqual(BodyRotation, 0f)) + { + var center = Rect.Location.ToVector2() + Rect.Size.ToVector2().FlipY() * 0.5f; + var rotation = BodyRotation; + if (IsHorizontal && FlippedX) { rotation += MathF.PI; } + if (!IsHorizontal && FlippedY) { rotation += MathF.PI; } + transformedPos = MathUtils.RotatePointAroundTarget(transformedPos, center, rotation); + } float damageAmount = 0.0f; for (int i = 0; i < SectionCount; i++) @@ -1143,6 +1216,7 @@ namespace Barotrauma gapRect.Y = (gapRect.Y - gapRect.Height / 2) + (int)(BodyHeight / 2 + BodyOffset.Y * scale); gapRect.Height = (int)BodyHeight; } + if (FlippedX) { diffFromCenter = -diffFromCenter; } } else { @@ -1153,8 +1227,8 @@ namespace Barotrauma gapRect.Width = (int)BodyWidth; } if (BodyHeight > 0.0f) { gapRect.Height = (int)(BodyHeight * (gapRect.Height / (float)this.rect.Height)); } + if (FlippedY) { diffFromCenter = -diffFromCenter; } } - if (FlippedX) { diffFromCenter = -diffFromCenter; } if (Math.Abs(BodyRotation) > 0.01f) { @@ -1170,14 +1244,26 @@ namespace Barotrauma gapRect.Width += 20; gapRect.Height += 20; - bool horizontalGap = !IsHorizontal; + bool rotatedEnoughToChangeOrientation = (MathUtils.WrapAngleTwoPi(rotationRad - MathHelper.PiOver4) % MathHelper.Pi < MathHelper.PiOver2); + if (rotatedEnoughToChangeOrientation) + { + var center = gapRect.Location + gapRect.Size.FlipY() / new Point(2); + var topLeft = gapRect.Location; + var diff = topLeft - center; + diff = diff.FlipY().YX().FlipY(); + var newTopLeft = diff + center; + gapRect = new Rectangle(newTopLeft, gapRect.Size.YX()); + } + bool horizontalGap = rotatedEnoughToChangeOrientation + ? IsHorizontal + : !IsHorizontal; bool diagonalGap = false; - if (Prefab.BodyRotation != 0.0f) + if (!MathUtils.NearlyEqual(BodyRotation, 0f)) { //rotation within a 90 deg sector (e.g. 100 -> 10, 190 -> 10, -10 -> 80) float sectorizedRotation = MathUtils.WrapAngleTwoPi(BodyRotation) % MathHelper.PiOver2; //diagonal if 30 < angle < 60 - diagonalGap = sectorizedRotation > MathHelper.Pi / 6 && sectorizedRotation < MathHelper.Pi / 3; + diagonalGap = sectorizedRotation is > MathHelper.Pi / 6 and < MathHelper.Pi / 3; //gaps on the lower half of a diagonal wall are horizontal, ones on the upper half are vertical if (diagonalGap) { @@ -1230,8 +1316,8 @@ namespace Barotrauma { if (damageDiff < 0.0f) { - attacker.Info?.IncreaseSkillLevel("mechanical".ToIdentifier(), - -damageDiff * SkillSettings.Current.SkillIncreasePerRepairedStructureDamage / Math.Max(attacker.GetSkillLevel("mechanical"), 1.0f)); + attacker.Info?.ApplySkillGain(Barotrauma.Tags.MechanicalSkill, + -damageDiff * SkillSettings.Current.SkillIncreasePerRepairedStructureDamage); } } } @@ -1337,7 +1423,7 @@ namespace Barotrauma { hasHoles = true; - if (!mergedSections.Any()) continue; + if (!mergedSections.Any()) { continue; } var mergedRect = GenerateMergedRect(mergedSections); mergedSections.Clear(); CreateRectBody(mergedRect, createConvexHull: true); @@ -1373,18 +1459,17 @@ namespace Barotrauma diffFromCenter = (rect.Center.X - this.rect.Center.X) / (float)this.rect.Width * BodyWidth; if (BodyWidth > 0.0f) rect.Width = Math.Max((int)Math.Round(BodyWidth * (rect.Width / (float)this.rect.Width)), 1); if (BodyHeight > 0.0f) rect.Height = (int)BodyHeight; + if (FlippedX) { diffFromCenter = -diffFromCenter; } } else { diffFromCenter = ((rect.Y - rect.Height / 2) - (this.rect.Y - this.rect.Height / 2)) / (float)this.rect.Height * BodyHeight; if (BodyWidth > 0.0f) rect.Width = (int)BodyWidth; if (BodyHeight > 0.0f) rect.Height = Math.Max((int)Math.Round(BodyHeight * (rect.Height / (float)this.rect.Height)), 1); + if (FlippedY) { diffFromCenter = -diffFromCenter; } } - if (FlippedX) { diffFromCenter = -diffFromCenter; } - Vector2 bodyOffset = ConvertUnits.ToSimUnits(Prefab.BodyOffset) * scale; - if (FlippedX) { bodyOffset.X = -bodyOffset.X; } - if (FlippedY) { bodyOffset.Y = -bodyOffset.Y; } + Vector2 bodyOffset = ConvertUnits.ToSimUnits(BodyOffset) * scale; Body newBody = GameMain.World.CreateRectangle( ConvertUnits.ToSimUnits(rect.Width), @@ -1398,7 +1483,7 @@ namespace Barotrauma newBody.UserData = this; Vector2 structureCenter = ConvertUnits.ToSimUnits(Position); - if (BodyRotation != 0.0f) + if (!MathUtils.NearlyEqual(BodyRotation, 0f)) { Vector2 pos = structureCenter + bodyOffset + new Vector2( (float)Math.Cos(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation), diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs index 941ddab07..a089421ab 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs @@ -67,6 +67,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.No, description: "Can items like signal components be attached on this structure? Should be enabled on structures like decorative background walls.")] public bool AllowAttachItems { get; private set; } + [Serialize(true, IsPropertySaveable.No, description: "Can the structure be rotated in the submarine editor?")] + public bool AllowRotatingInEditor { get; set; } + [Serialize(0.0f, IsPropertySaveable.No)] public float MinHealth { get; private set; } @@ -297,14 +300,16 @@ namespace Barotrauma if (Identifier == Identifier.Empty) { DebugConsole.ThrowError( - "Structure prefab \"" + Name + "\" has no identifier. All structure prefabs have a unique identifier string that's used to differentiate between items during saving and loading."); + "Structure prefab \"" + Name.Value + "\" has no identifier. All structure prefabs have a unique identifier string that's used to differentiate between items during saving and loading.", + contentPackage: ContentPackage); } #if DEBUG if (!Category.HasFlag(MapEntityCategory.Legacy) && !HideInMenus) { if (!string.IsNullOrEmpty(OriginalName)) { - DebugConsole.AddWarning($"Structure \"{(Identifier == Identifier.Empty ? Name : Identifier.Value)}\" has a hard-coded name, and won't be localized to other languages."); + DebugConsole.AddWarning($"Structure \"{(Identifier == Identifier.Empty ? Name : Identifier.Value)}\" has a hard-coded name, and won't be localized to other languages.", + ContentPackage); } } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index fad619258..edce76390 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -505,51 +505,58 @@ namespace Barotrauma minWidth += padding; minHeight += padding; - Vector2 limits = GetHorizontalLimits(spawnPos, minWidth, minHeight, 0); - if (verticalMoveDir != 0) + int iterations = 0; + const int maxIterations = 5; + do { - verticalMoveDir = Math.Sign(verticalMoveDir); - //do a raycast towards the top/bottom of the level depending on direction - Vector2 potentialPos = new Vector2(spawnPos.X, verticalMoveDir > 0 ? Level.Loaded.Size.Y : 0); - - //3 raycasts (left, middle and right side of the sub, so we don't accidentally raycast up a passage too narrow for the sub) - for (int x = -1; x <= 1; x++) + Vector2 potentialPos = spawnPos; + if (verticalMoveDir != 0) { - Vector2 xOffset = Vector2.UnitX * minWidth / 2 * x; - if (PickBody( - ConvertUnits.ToSimUnits(spawnPos + xOffset), - ConvertUnits.ToSimUnits(potentialPos + xOffset), - collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) != null) + verticalMoveDir = Math.Sign(verticalMoveDir); + //do a raycast towards the top/bottom of the level depending on direction + Vector2 rayEnd = new Vector2(potentialPos.X, verticalMoveDir > 0 ? Level.Loaded.Size.Y : 0); + + Vector2 closestPickedPos = rayEnd; + //multiple raycast across the width of the sub (so we don't accidentally raycast up a passage too narrow for the sub) + for (float x = -1; x <= 1; x += 0.2f) { - int offsetFromWall = 10 * -verticalMoveDir; - //if the raycast hit a wall, attempt to place the spawnpos there - if (verticalMoveDir > 0) + Vector2 xOffset = Vector2.UnitX * minWidth / 2 * x; + xOffset.X += subDockingPortOffset; + if (PickBody( + ConvertUnits.ToSimUnits(potentialPos + xOffset), + ConvertUnits.ToSimUnits(rayEnd + xOffset), + collisionCategory: Physics.CollisionLevel | Physics.CollisionWall, + customPredicate: (Fixture f) => + { + return f.UserData is not VoronoiCell { IsDestructible: true }; + }) != null) { - potentialPos.Y = Math.Min(potentialPos.Y, ConvertUnits.ToDisplayUnits(LastPickedPosition.Y) + offsetFromWall); - } - else - { - potentialPos.Y = Math.Max(potentialPos.Y, ConvertUnits.ToDisplayUnits(LastPickedPosition.Y) + offsetFromWall); + //if the raycast hit a wall, attempt to place the spawnpos there + int offsetFromWall = 10 * -verticalMoveDir; + float pickedPos = ConvertUnits.ToDisplayUnits(LastPickedPosition.Y) + offsetFromWall; + closestPickedPos.Y = + verticalMoveDir > 0 ? + Math.Min(closestPickedPos.Y, pickedPos) : + Math.Max(closestPickedPos.Y, pickedPos); } } + potentialPos.Y = closestPickedPos.Y; } - //step away from the top/bottom of the level, or from whatever wall the raycast hit, - //until we found a spot where there's enough room to place the sub - float dist = Math.Abs(potentialPos.Y - spawnPos.Y); - for (float d = dist; d > 0; d -= 100.0f) + Vector2 limits = GetHorizontalLimits(new Vector2(potentialPos.X, potentialPos.Y - (dockedBorders.Height * 0.5f * verticalMoveDir)), + maxHorizontalMoveAmount: minWidth, minHeight, verticalMoveDir, padding); + if (limits.Y - limits.X >= minWidth) { - float y = spawnPos.Y + verticalMoveDir * d; - limits = GetHorizontalLimits(new Vector2(spawnPos.X, y), minWidth, minHeight, verticalMoveDir); - if (limits.Y - limits.X > minWidth) - { - spawnPos = new Vector2(spawnPos.X, y - (dockedBorders.Height * 0.5f * verticalMoveDir)); - break; - } - } - } + Vector2 newSpawnPos = new Vector2(spawnPos.X, potentialPos.Y - (dockedBorders.Height * 0.5f * verticalMoveDir)); + bool couldMoveInVerticalMoveDir = Math.Sign(newSpawnPos.Y - spawnPos.Y) == Math.Sign(verticalMoveDir); + if (!couldMoveInVerticalMoveDir) { break; } + spawnPos = ClampToHorizontalLimits(newSpawnPos, limits); + } - static Vector2 GetHorizontalLimits(Vector2 spawnPos, float minWidth, float minHeight, int verticalMoveDir) + iterations++; + } while (iterations < maxIterations); + + Vector2 GetHorizontalLimits(Vector2 spawnPos, float maxHorizontalMoveAmount, float minHeight, int verticalMoveDir, int padding) { Vector2 refPos = spawnPos - Vector2.UnitY * minHeight * 0.5f * Math.Sign(verticalMoveDir); @@ -580,34 +587,44 @@ namespace Barotrauma if (Math.Abs(ruin.Area.Center.Y - refPos.Y) > (minHeight + ruin.Area.Height) * 0.5f) { continue; } if (ruin.Area.Center.X < refPos.X) { - minX = Math.Max(minX, ruin.Area.Right + 100.0f); + minX = Math.Max(minX, ruin.Area.Right + padding); } else { - maxX = Math.Min(maxX, ruin.Area.X - 100.0f); + maxX = Math.Min(maxX, ruin.Area.X - padding); } } - return new Vector2(Math.Max(minX, spawnPos.X - minWidth), Math.Min(maxX, spawnPos.X + minWidth)); + + minX += subDockingPortOffset; + maxX += subDockingPortOffset; + + return new Vector2( + Math.Max(Math.Max(minX, spawnPos.X - maxHorizontalMoveAmount - padding), 0), + Math.Min(Math.Min(maxX, spawnPos.X + maxHorizontalMoveAmount + padding), Level.Loaded.Size.X)); } - if (limits.X < 0.0f && limits.Y > Level.Loaded.Size.X) + Vector2 ClampToHorizontalLimits(Vector2 spawnPos, Vector2 limits) { - //no walls found at either side, just use the initial spawnpos and hope for the best - } - else if (limits.X < 0) - { - //no wall found at the left side, spawn to the left from the right-side wall - spawnPos.X = limits.Y - minWidth * 0.5f - 100.0f + subDockingPortOffset; - } - else if (limits.Y > Level.Loaded.Size.X) - { - //no wall found at right side, spawn to the right from the left-side wall - spawnPos.X = limits.X + minWidth * 0.5f + 100.0f + subDockingPortOffset; - } - else - { - //walls found at both sides, use their midpoint - spawnPos.X = (limits.X + limits.Y) / 2 + subDockingPortOffset; + if (limits.X < 0.0f && limits.Y > Level.Loaded.Size.X) + { + //no walls found at either side, just use the initial spawnpos and hope for the best + } + else if (limits.X < 0) + { + //no wall found at the left side, spawn to the left from the right-side wall + spawnPos.X = limits.Y - minWidth * 0.5f - 100.0f + subDockingPortOffset; + } + else if (limits.Y > Level.Loaded.Size.X) + { + //no wall found at right side, spawn to the right from the left-side wall + spawnPos.X = limits.X + minWidth * 0.5f + 100.0f + subDockingPortOffset; + } + else + { + //walls found at both sides, use their midpoint + spawnPos.X = (limits.X + limits.Y) / 2 + subDockingPortOffset; + } + return spawnPos; } spawnPos.Y = MathHelper.Clamp(spawnPos.Y, dockedBorders.Height / 2 + 10, Level.Loaded.Size.Y - dockedBorders.Height / 2 - padding * 2); @@ -624,7 +641,7 @@ namespace Barotrauma //math/physics stuff ---------------------------------------------------- - public static Vector2 VectorToWorldGrid(Vector2 position, bool round = false) + public static Vector2 VectorToWorldGrid(Vector2 position, Submarine sub = null, bool round = false) { if (round) { @@ -636,6 +653,12 @@ namespace Barotrauma position.X = MathF.Floor(position.X / GridSize.X) * GridSize.X; position.Y = MathF.Ceiling(position.Y / GridSize.Y) * GridSize.Y; } + + if (sub != null) + { + position.X += sub.Position.X % GridSize.X; + position.Y += sub.Position.Y % GridSize.Y; + } return position; } @@ -923,11 +946,17 @@ namespace Barotrauma return true; } + /// - /// check visibility between two points (in sim units) + /// Check visibility between two points (in sim units). /// - /// a physics body that was between the points (or null) - public static Body CheckVisibility(Vector2 rayStart, Vector2 rayEnd, bool ignoreLevel = false, bool ignoreSubs = false, bool ignoreSensors = true, bool ignoreDisabledWalls = true, bool ignoreBranches = true) + /// + + /// Should plants' branches be ignored? + /// If the predicate returns false, the fixture is ignored even if it would normally block visibility. + /// A physics body that was between the points (or null) + public static Body CheckVisibility(Vector2 rayStart, Vector2 rayEnd, bool ignoreLevel = false, bool ignoreSubs = false, bool ignoreSensors = true, bool ignoreDisabledWalls = true, bool ignoreBranches = true, + Predicate blocksVisibilityPredicate = null) { Body closestBody = null; float closestFraction = 1.0f; @@ -962,7 +991,10 @@ namespace Barotrauma if (sectionIndex > -1 && structure.SectionBodyDisabled(sectionIndex)) { return -1; } } } - + if (blocksVisibilityPredicate != null && !blocksVisibilityPredicate(fixture)) + { + return -1; + } if (fraction < closestFraction) { closestBody = fixture.Body; @@ -1840,6 +1872,7 @@ namespace Barotrauma FilePath = filePath, OutpostModuleInfo = Info.OutpostModuleInfo != null ? new OutpostModuleInfo(Info.OutpostModuleInfo) : null, BeaconStationInfo = Info.BeaconStationInfo != null ? new BeaconStationInfo(Info.BeaconStationInfo) : null, + WreckInfo = Info.WreckInfo != null ? new WreckInfo(Info.WreckInfo) : null, Name = Path.GetFileNameWithoutExtension(filePath) }; #if CLIENT @@ -1878,12 +1911,11 @@ namespace Barotrauma Unloading = true; try { - #if CLIENT RoundSound.RemoveAllRoundSounds(); GameMain.LightManager?.ClearLights(); + depthSortedDamageable.Clear(); #endif - var _loaded = new List(loaded); foreach (Submarine sub in _loaded) { @@ -1918,9 +1950,11 @@ namespace Barotrauma Ragdoll.RemoveAll(); PhysicsBody.RemoveAll(); + StatusEffect.StopAll(); GameMain.World = null; Powered.Grids.Clear(); + Powered.ChangedConnections.Clear(); GC.Collect(); @@ -1940,6 +1974,7 @@ namespace Barotrauma outdoorNodes?.Clear(); outdoorNodes = null; + obstructedNodes.Clear(); GameMain.GameSession?.Campaign?.UpgradeManager?.OnUpgradesChanged?.TryDeregister(upgradeEventIdentifier); @@ -1951,11 +1986,17 @@ namespace Barotrauma visibleEntities = null; + bodyDist.Clear(); + bodies.Clear(); + if (MainSub == this) { MainSub = null; } if (MainSubs[1] == this) { MainSubs[1] = null; } ConnectedDockingPorts?.Clear(); + Powered.ChangedConnections.Clear(); + Powered.Grids.Clear(); + loaded.Remove(this); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index e0d060c04..c4948fc15 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -169,7 +169,12 @@ namespace Barotrauma bool hasCollider = wall.HasBody && !wall.IsPlatform && wall.StairDirection == Direction.None; Rectangle rect = wall.Rect; - SetExtents(new Vector2(rect.X, rect.Y - rect.Height), new Vector2(rect.Right, rect.Y), hasCollider); + + var transformedQuad = wall.GetTransformedQuad(); + AddPointToExtents(transformedQuad.A, hasCollider: hasCollider); + AddPointToExtents(transformedQuad.B, hasCollider: hasCollider); + AddPointToExtents(transformedQuad.C, hasCollider: hasCollider); + AddPointToExtents(transformedQuad.D, hasCollider: hasCollider); if (hasCollider) { farseerBody.CreateRectangle( @@ -188,7 +193,8 @@ namespace Barotrauma if (hull.Submarine != submarine || hull.IdFreed) { continue; } Rectangle rect = hull.Rect; - SetExtents(new Vector2(rect.X, rect.Y - rect.Height), new Vector2(rect.Right, rect.Y), hasCollider: true); + AddPointToExtents(new Vector2(rect.X, rect.Y - rect.Height), hasCollider: true); + AddPointToExtents(new Vector2(rect.Right, rect.Y), hasCollider: true); farseerBody.CreateRectangle( ConvertUnits.ToSimUnits(rect.Width), @@ -221,33 +227,42 @@ namespace Barotrauma float simWidth = ConvertUnits.ToSimUnits(width); float simHeight = ConvertUnits.ToSimUnits(height); + if (radius > 0f || (width > 0f && height > 0f)) + { + var transformedQuad = item.GetTransformedQuad(); + AddPointToExtents(transformedQuad.A, hasCollider: true); + AddPointToExtents(transformedQuad.B, hasCollider: true); + AddPointToExtents(transformedQuad.C, hasCollider: true); + AddPointToExtents(transformedQuad.D, hasCollider: true); + } + if (width > 0.0f && height > 0.0f) { item.StaticFixtures.Add(farseerBody.CreateRectangle(simWidth, simHeight, 5.0f, simPos, collisionCategory, collidesWith)); - SetExtents(item.Position - new Vector2(width, height) / 2, item.Position + new Vector2(width, height) / 2, hasCollider: true); + AddPointToExtents(item.Position - new Vector2(width, height) / 2, hasCollider: true); + AddPointToExtents(item.Position + new Vector2(width, height) / 2, hasCollider: true); } else if (radius > 0.0f && width > 0.0f) { item.StaticFixtures.Add(farseerBody.CreateRectangle(simWidth, simRadius * 2, 5.0f, simPos, collisionCategory, collidesWith)); item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitX * simWidth / 2, collisionCategory, collidesWith)); item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitX * simWidth / 2, collisionCategory, collidesWith)); - SetExtents(item.Position - new Vector2(width / 2 + radius, height / 2), item.Position + new Vector2(width / 2 + radius, height / 2), hasCollider: true); + AddPointToExtents(item.Position - new Vector2(width / 2 + radius, height / 2), hasCollider: true); + AddPointToExtents(item.Position + new Vector2(width / 2 + radius, height / 2), hasCollider: true); } else if (radius > 0.0f && height > 0.0f) { item.StaticFixtures.Add(farseerBody.CreateRectangle(simRadius * 2, height, 5.0f, simPos, collisionCategory, collidesWith)); item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitY * simHeight / 2, collisionCategory, collidesWith)); item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitY * simHeight / 2, collisionCategory, collidesWith)); - SetExtents(item.Position - new Vector2(width / 2, height / 2 + radius), item.Position + new Vector2(width / 2, height / 2 + radius), hasCollider: true); + AddPointToExtents(item.Position - new Vector2(width / 2, height / 2 + radius), hasCollider: true); + AddPointToExtents(item.Position + new Vector2(width / 2, height / 2 + radius), hasCollider: true); } else if (radius > 0.0f) { item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos, collisionCategory, collidesWith)); - visibleMinExtents.X = Math.Min(item.Position.X - radius, visibleMinExtents.X); - visibleMinExtents.Y = Math.Min(item.Position.Y - radius, visibleMinExtents.Y); - visibleMaxExtents.X = Math.Max(item.Position.X + radius, visibleMaxExtents.X); - visibleMaxExtents.Y = Math.Max(item.Position.Y + radius, visibleMaxExtents.Y); - SetExtents(item.Position - new Vector2(radius, radius), item.Position + new Vector2(radius, radius), hasCollider: true); + AddPointToExtents(item.Position - new Vector2(radius, radius), hasCollider: true); + AddPointToExtents(item.Position + new Vector2(radius, radius), hasCollider: true); } item.StaticFixtures.ForEach(f => f.UserData = item); } @@ -268,18 +283,18 @@ namespace Barotrauma Body = new PhysicsBody(farseerBody); - void SetExtents(Vector2 min, Vector2 max, bool hasCollider) + void AddPointToExtents(Vector2 point, bool hasCollider) { - visibleMinExtents.X = Math.Min(min.X, visibleMinExtents.X); - visibleMinExtents.Y = Math.Min(min.Y, visibleMinExtents.Y); - visibleMaxExtents.X = Math.Max(max.X, visibleMaxExtents.X); - visibleMaxExtents.Y = Math.Max(max.Y, visibleMaxExtents.Y); + visibleMinExtents.X = Math.Min(point.X, visibleMinExtents.X); + visibleMinExtents.Y = Math.Min(point.Y, visibleMinExtents.Y); + visibleMaxExtents.X = Math.Max(point.X, visibleMaxExtents.X); + visibleMaxExtents.Y = Math.Max(point.Y, visibleMaxExtents.Y); if (hasCollider) { - minExtents.X = Math.Min(min.X, minExtents.X); - minExtents.Y = Math.Min(min.Y, minExtents.Y); - maxExtents.X = Math.Max(max.X, maxExtents.X); - maxExtents.Y = Math.Max(max.Y, maxExtents.Y); + minExtents.X = Math.Min(point.X, minExtents.X); + minExtents.Y = Math.Min(point.Y, minExtents.Y); + maxExtents.X = Math.Max(point.X, maxExtents.X); + maxExtents.Y = Math.Max(point.Y, maxExtents.Y); } } } @@ -800,7 +815,10 @@ namespace Barotrauma float damageAmount = contactDot * Body.Mass / limb.character.Mass; limb.character.LastDamageSource = submarine; limb.character.DamageLimb(ConvertUnits.ToDisplayUnits(collision.ImpactPos), limb, - AfflictionPrefab.ImpactDamage.Instantiate(damageAmount).ToEnumerable(), 0.0f, true, 0.0f); + AfflictionPrefab.ImpactDamage.Instantiate(damageAmount).ToEnumerable(), + stun: 0.0f, + playSound: true, + attackImpulse: Vector2.Zero); if (limb.character.IsDead) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index a83578cfa..a71c4f3fb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -122,6 +122,9 @@ namespace Barotrauma public OutpostModuleInfo OutpostModuleInfo { get; set; } public BeaconStationInfo BeaconStationInfo { get; set; } + public WreckInfo WreckInfo { get; set; } + + public ExtraSubmarineInfo GetExtraSubmarineInfo => BeaconStationInfo ?? WreckInfo as ExtraSubmarineInfo; public bool IsOutpost => Type == SubmarineType.Outpost || Type == SubmarineType.OutpostModule; @@ -320,10 +323,14 @@ namespace Barotrauma { OutpostModuleInfo = new OutpostModuleInfo(original.OutpostModuleInfo); } - if (original.BeaconStationInfo != null) + else if (original.BeaconStationInfo != null) { BeaconStationInfo = new BeaconStationInfo(original.BeaconStationInfo); } + else if (original.WreckInfo != null) + { + WreckInfo = new WreckInfo(original.WreckInfo); + } #if CLIENT PreviewImage = original.PreviewImage != null ? new Sprite(original.PreviewImage) : null; #endif @@ -410,6 +417,10 @@ namespace Barotrauma { BeaconStationInfo = new BeaconStationInfo(this, SubmarineElement); } + else if (Type == SubmarineType.Wreck) + { + WreckInfo = new WreckInfo(this, SubmarineElement); + } } } @@ -589,6 +600,11 @@ namespace Barotrauma BeaconStationInfo.Save(newElement); BeaconStationInfo = new BeaconStationInfo(this, newElement); } + else if (Type == SubmarineType.Wreck) + { + WreckInfo.Save(newElement); + WreckInfo = new WreckInfo(this, newElement); + } XDocument doc = new XDocument(newElement); doc.Root.Add(new XAttribute("name", Name)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/BanList.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/BanList.cs index dbb4c9276..c19ff9637 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/BanList.cs @@ -14,6 +14,16 @@ namespace Barotrauma.Networking public readonly string Reason; public Option ExpirationTime; public readonly UInt32 UniqueIdentifier; + + public bool MatchesClient(Client client) + { + if (client == null) { return false; } + if (AddressOrAccountId.TryGet(out AccountId bannedAccountId) && client.AccountId.TryUnwrap(out AccountId? accountId)) + { + return bannedAccountId.Equals(accountId); + } + return false; + } } partial class BanList diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs index 4f8157b45..9f7dafe82 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs @@ -131,6 +131,7 @@ namespace Barotrauma.Networking catch (AggregateException aggregateException) { if (aggregateException.InnerException is OperationCanceledException) { return Option.None(); } + CheckPipeConnected(nameof(readStream), readStream); throw; } catch (OperationCanceledException) @@ -161,7 +162,18 @@ namespace Barotrauma.Networking { if (status is StatusEnum.Active && pipe is not { IsConnected: true }) { - throw new Exception($"{name} was disconnected unexpectedly"); + string exceptionMsg = $"{name} was disconnected unexpectedly."; +#if CLIENT + if (Process is { HasExited: true, ExitCode: var exitCode }) + { + exceptionMsg += $" Child process exit code was {(uint)exitCode:X8}."; + } + else if (Process is { HasExited: false }) + { + exceptionMsg += " Child process has not exited."; + } +#endif + throw new Exception(exceptionMsg); } } @@ -256,7 +268,11 @@ namespace Barotrauma.Networking { case ObjectDisposedException _: case System.IO.IOException _: - if (!HasShutDown) { throw; } + if (!HasShutDown) + { + CheckPipeConnected(nameof(writeStream), writeStream); + throw; + } break; default: throw; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs index b3b47676e..34af27022 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs @@ -218,7 +218,7 @@ namespace Barotrauma.Networking msg.WriteUInt16((UInt16)PermittedConsoleCommands.Count); foreach (DebugConsole.Command command in PermittedConsoleCommands) { - msg.WriteString(command.names[0]); + msg.WriteIdentifier(command.Names[0]); } } } @@ -240,8 +240,8 @@ namespace Barotrauma.Networking UInt16 commandCount = inc.ReadUInt16(); for (int i = 0; i < commandCount; i++) { - string commandName = inc.ReadString(); - var consoleCommand = DebugConsole.Commands.Find(c => c.names.Contains(commandName)); + Identifier commandName = inc.ReadIdentifier(); + var consoleCommand = DebugConsole.Commands.Find(c => c.Names.Contains(commandName)); if (consoleCommand != null) { permittedCommands.Add(consoleCommand); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs index a5067d7c6..1e45b7d94 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs @@ -1,11 +1,10 @@ -using Barotrauma.Extensions; +using Barotrauma.Extensions; using Barotrauma.Items.Components; using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using Steamworks.ServerList; namespace Barotrauma { @@ -111,7 +110,7 @@ namespace Barotrauma public void OnSpawned(Entity spawnedItem) { - if (!(spawnedItem is Item item)) { throw new ArgumentException($"The entity passed to ItemSpawnInfo.OnSpawned must be an Item (value was {spawnedItem?.ToString() ?? "null"})."); } + if (spawnedItem is not Item item) { throw new ArgumentException($"The entity passed to ItemSpawnInfo.OnSpawned must be an Item (value was {spawnedItem?.ToString() ?? "null"})."); } onSpawned?.Invoke(item); } } @@ -443,6 +442,7 @@ namespace Barotrauma CreateNetworkEventProjSpecific(new SpawnEntity(spawnedEntity)); } spawnInfo.OnSpawned(spawnedEntity); + GameMain.GameSession?.EventManager?.EntitySpawned(spawnedEntity); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs index 0f33d9599..fd647fcab 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs @@ -195,7 +195,7 @@ namespace Barotrauma.Networking int orderPriority = msg.ReadByte(); OrderTarget orderTargetPosition = null; Order.OrderTargetType orderTargetType = (Order.OrderTargetType)msg.ReadByte(); - int wallSectionIndex = 0; + int? wallSectionIndex = null; if (msg.ReadBoolean()) { float x = msg.ReadSingle(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs index 658fee626..b8e8965dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs @@ -252,6 +252,13 @@ namespace Barotrauma.Networking continue; } +#if CLIENT + foreach (var itemComponent in item.Components) + { + itemComponent.StopLoopingSound(); + } +#endif + //restore other items to full condition and recharge batteries item.Condition = item.MaxCondition; item.GetComponent()?.ResetDeterioration(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index b6dfb0840..c105f27de 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -644,6 +644,13 @@ namespace Barotrauma.Networking set; } + [Serialize(true, IsPropertySaveable.Yes)] + public bool AllowImmediateItemDelivery + { + get; + set; + } + [Serialize(false, IsPropertySaveable.Yes)] public bool LockAllDefaultWires { @@ -841,6 +848,13 @@ namespace Barotrauma.Networking private set; } + [Serialize(10.0f, IsPropertySaveable.Yes)] + public float MinimumMidRoundSyncTimeout + { + get; + private set; + } + private bool karmaEnabled; [Serialize(false, IsPropertySaveable.Yes)] public bool KarmaEnabled diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/Editable/ConditionallyEditable.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/Editable/ConditionallyEditable.cs new file mode 100644 index 000000000..5229bf5b5 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/Editable/ConditionallyEditable.cs @@ -0,0 +1,66 @@ +using System; +using Barotrauma.Items.Components; + +namespace Barotrauma; + +[AttributeUsage(AttributeTargets.Property)] +sealed class ConditionallyEditable : Editable +{ + public ConditionallyEditable(ConditionType conditionType, bool onlyInEditors = true) + { + this.conditionType = conditionType; + this.onlyInEditors = onlyInEditors; + } + private readonly ConditionType conditionType; + + private readonly bool onlyInEditors; + + public enum ConditionType + { + //These need to exist at compile time, so it is a little awkward + //I would love to see a better way to do this + AllowLinkingWifiToChat, + IsSwappableItem, + AllowRotating, + Attachable, + HasBody, + Pickable, + OnlyByStatusEffectsAndNetwork, + HasIntegratedButtons, + IsToggleableController, + HasConnectionPanel + } + + public bool IsEditable(ISerializableEntity entity) + { + if (onlyInEditors && Screen.Selected is { IsEditor: false }) { return false; } + + return conditionType switch + { + ConditionType.AllowLinkingWifiToChat + => GameMain.NetworkMember is not { ServerSettings.AllowLinkingWifiToChat: false }, + ConditionType.IsSwappableItem + => entity is Item item && item.Prefab.SwappableItem != null, + ConditionType.AllowRotating + => (entity is Item { body: null } item && item.Prefab.AllowRotatingInEditor) + || (entity is Structure structure && structure.Prefab.AllowRotatingInEditor), + ConditionType.Attachable + => entity is Holdable { Attachable: true }, + ConditionType.HasBody + => entity is Structure { HasBody: true } or Item { body: not null }, + ConditionType.Pickable + => entity is Item item && item.GetComponent() != null, + ConditionType.OnlyByStatusEffectsAndNetwork + => GameMain.NetworkMember is { IsServer: true }, + ConditionType.HasIntegratedButtons + => entity is Door { HasIntegratedButtons: true }, + ConditionType.IsToggleableController + => entity is Controller { IsToggle: true } controller && controller.Item.GetComponent() != null, + ConditionType.HasConnectionPanel + => (entity is Item item && item.GetComponent() != null) + || (entity is ItemComponent ic && ic.Item.GetComponent() != null), + _ + => false + }; + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/Editable/Editable.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/Editable/Editable.cs new file mode 100644 index 000000000..5786df7b1 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/Editable/Editable.cs @@ -0,0 +1,55 @@ +using System; +using Barotrauma.Items.Components; + +namespace Barotrauma; + +[AttributeUsage(AttributeTargets.Property)] +class Editable : Attribute +{ + public int MaxLength; + public int DecimalCount = 1; + + public int MinValueInt = int.MinValue, MaxValueInt = int.MaxValue; + public float MinValueFloat = float.MinValue, MaxValueFloat = float.MaxValue; + public bool ForceShowPlusMinusButtons = false; + public float ValueStep; + + /// + /// Labels of the components of a vector property (defaults to x,y,z,w) + /// + public string[] VectorComponentLabels; + + /// + /// If a translation can't be found for the property name, this tag is used instead + /// + public string FallBackTextTag; + + /// + /// Currently implemented only for int and bool fields. TODO: implement the remaining types (SerializableEntityEditor) + /// + public bool ReadOnly; + + public Editable(int maxLength = 20) + { + MaxLength = maxLength; + } + + public Editable(int minValue, int maxValue) + { + MinValueInt = minValue; + MaxValueInt = maxValue; + } + + public Editable(float minValue, float maxValue, int decimals = 1) + { + MinValueFloat = minValue; + MaxValueFloat = maxValue; + DecimalCount = decimals; + } +} + +[AttributeUsage(AttributeTargets.Property)] +sealed class InGameEditable : Editable +{ +} + diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty/SerializableProperty.cs similarity index 91% rename from Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs rename to Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty/SerializableProperty.cs index 3e477e0e2..1c26b59df 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty/SerializableProperty.cs @@ -15,135 +15,6 @@ using Barotrauma.Networking; namespace Barotrauma { - [AttributeUsage(AttributeTargets.Property)] - class Editable : Attribute - { - public int MaxLength; - public int DecimalCount = 1; - - public int MinValueInt = int.MinValue, MaxValueInt = int.MaxValue; - public float MinValueFloat = float.MinValue, MaxValueFloat = float.MaxValue; - public float ValueStep; - - /// - /// Labels of the components of a vector property (defaults to x,y,z,w) - /// - public string[] VectorComponentLabels; - - /// - /// If a translation can't be found for the property name, this tag is used instead - /// - public string FallBackTextTag; - - /// - /// Currently implemented only for int and bool fields. TODO: implement the remaining types (SerializableEntityEditor) - /// - public bool ReadOnly; - - public Editable(int maxLength = 20) - { - MaxLength = maxLength; - } - - public Editable(int minValue, int maxValue) - { - MinValueInt = minValue; - MaxValueInt = maxValue; - } - - public Editable(float minValue, float maxValue, int decimals = 1) - { - MinValueFloat = minValue; - MaxValueFloat = maxValue; - DecimalCount = decimals; - } - } - - [AttributeUsage(AttributeTargets.Property)] - class InGameEditable : Editable - { - } - - [AttributeUsage(AttributeTargets.Property)] - class ConditionallyEditable : Editable - { - public ConditionallyEditable(ConditionType conditionType, bool onlyInEditors = true) - { - this.conditionType = conditionType; - this.onlyInEditors = onlyInEditors; - } - private readonly ConditionType conditionType; - - private readonly bool onlyInEditors; - - public enum ConditionType - { - //These need to exist at compile time, so it is a little awkward - //I would love to see a better way to do this - AllowLinkingWifiToChat, - IsSwappableItem, - AllowRotating, - Attachable, - HasBody, - Pickable, - OnlyByStatusEffectsAndNetwork, - HasIntegratedButtons, - IsToggleableController, - HasConnectionPanel - } - - public bool IsEditable(ISerializableEntity entity) - { - if (onlyInEditors && Screen.Selected is { IsEditor: false }) { return false; } - switch (conditionType) - { - case ConditionType.AllowLinkingWifiToChat: - return GameMain.NetworkMember?.ServerSettings?.AllowLinkingWifiToChat ?? true; - case ConditionType.IsSwappableItem: - { - return entity is Item item && item.Prefab.SwappableItem != null; - } - case ConditionType.AllowRotating: - { - return entity is Item item && item.body == null && item.Prefab.AllowRotatingInEditor; - } - case ConditionType.Attachable: - { - return entity is Holdable holdable && holdable.Attachable; - } - case ConditionType.HasBody: - { - return entity is Structure { HasBody: true } || entity is Item { body: not null }; - } - case ConditionType.Pickable: - { - return entity is Item item && item.GetComponent() != null; - } - case ConditionType.HasIntegratedButtons: - { - return entity is Door door && door.HasIntegratedButtons; - } - case ConditionType.OnlyByStatusEffectsAndNetwork: -#if SERVER - return true; -#else - return false; -#endif - case ConditionType.IsToggleableController: - { - return entity is Controller controller && controller.IsToggle && controller.Item.GetComponent() != null; - } - case ConditionType.HasConnectionPanel: - { - return - (entity is Item item && item.GetComponent() != null) || - (entity is ItemComponent ic && ic.Item.GetComponent() != null); - } - } - return false; - } - } - public enum IsPropertySaveable { Yes, @@ -151,7 +22,7 @@ namespace Barotrauma } [AttributeUsage(AttributeTargets.Property)] - public class Serialize : Attribute + public sealed class Serialize : Attribute { public readonly object DefaultValue; public readonly IsPropertySaveable IsSaveable; @@ -182,9 +53,9 @@ namespace Barotrauma } } - public class SerializableProperty + public sealed class SerializableProperty { - private readonly static ImmutableDictionary supportedTypes = new Dictionary + private static readonly ImmutableDictionary supportedTypes = new Dictionary { { typeof(bool), "bool" }, { typeof(int), "int" }, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/StructSerialization.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/StructSerialization.cs index 409e5d5b1..1121e33d9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/StructSerialization.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/StructSerialization.cs @@ -114,7 +114,7 @@ namespace Barotrauma self = (T)boxedSelf; } - public static void TryDeserialize(this object boxedSelf, FieldInfo field, XElement element) + private static void TryDeserialize(this object boxedSelf, FieldInfo field, XElement element) { string fieldName = field.Name.ToLowerInvariant(); string valueStr = element.GetAttributeString(fieldName, field.GetValue(boxedSelf)?.ToString() ?? ""); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index 8c044c67d..0ad15815d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -84,6 +84,7 @@ namespace Barotrauma Graphics = GraphicsSettings.GetDefault(), Audio = AudioSettings.GetDefault(), #if CLIENT + DisableGlobalSpamList = false, KeyMap = KeyMapping.GetDefault(), InventoryKeyMap = InventoryKeyMapping.GetDefault() #endif @@ -156,6 +157,7 @@ namespace Barotrauma public string RemoteMainMenuContentUrl; #if CLIENT public XElement SavedCampaignSettings; + public bool DisableGlobalSpamList; #endif #if DEBUG public bool UseSteamMatchmaking; @@ -548,6 +550,19 @@ namespace Barotrauma currentConfig.Graphics.VSync != newConfig.Graphics.VSync || currentConfig.Graphics.DisplayMode != newConfig.Graphics.DisplayMode; +#if CLIENT + bool keybindsChanged = false; + foreach (var kvp in newConfig.KeyMap.Bindings) + { + if (!currentConfig.KeyMap.Bindings.TryGetValue(kvp.Key, out var existingBinding) || + existingBinding != kvp.Value) + { + keybindsChanged = true; + break; + } + } +#endif + currentConfig = newConfig; #if CLIENT @@ -575,7 +590,19 @@ namespace Barotrauma HUDLayoutSettings.CreateAreas(); GameMain.GameSession?.HUDScaleChanged(); } - + + if (keybindsChanged) + { + foreach (var item in Item.ItemList) + { + foreach (var ic in item.Components) + { + //parse messages because they may contain keybind texts + ic.ParseMsg(); + } + } + } + GameMain.SoundManager?.ApplySettings(); #endif if (languageChanged) { TextManager.ClearCache(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs index bb39a43b5..8d3d19ab4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs @@ -306,7 +306,8 @@ namespace Barotrauma } if (file == "") { - DebugConsole.ThrowError("Sprite " + SourceElement + " doesn't have a texture specified!"); + DebugConsole.ThrowError("Sprite " + SourceElement.Element + " doesn't have a texture specified!", + contentPackage: SourceElement.ContentPackage); return false; } if (!string.IsNullOrEmpty(path)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs index 8d14f4ce7..3c34fd89a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs @@ -196,7 +196,7 @@ namespace Barotrauma /// public readonly bool TargetContainedItem; - public static IEnumerable FromXElement(XElement element, Predicate? predicate = null) + public static IEnumerable FromXElement(ContentXElement element, Predicate? predicate = null) { var targetItemComponent = element.GetAttributeString(nameof(TargetItemComponent), ""); var targetContainer = element.GetAttributeBool(nameof(TargetContainer), false); @@ -218,7 +218,7 @@ namespace Barotrauma var (comparisonOperator, attributeValueString) = ExtractComparisonOperatorFromConditionString(attribute.Value); if (string.IsNullOrWhiteSpace(attributeValueString)) { - DebugConsole.ThrowError($"Conditional attribute value is empty: {element}"); + DebugConsole.ThrowError($"Conditional attribute value is empty: {element}", contentPackage: element.ContentPackage); continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 3af1f3aca..67226d137 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -227,17 +227,17 @@ namespace Barotrauma public bool InheritEventTags { get; private set; } - public ItemSpawnInfo(XElement element, string parentDebugName) + public ItemSpawnInfo(ContentXElement element, string parentDebugName) { - if (element.Attribute("name") != null) + if (element.GetAttribute("name") != null) { //backwards compatibility - DebugConsole.ThrowError("Error in StatusEffect config (" + element.ToString() + ") - use item identifier instead of the name."); + DebugConsole.ThrowError("Error in StatusEffect config (" + element.ToString() + ") - use item identifier instead of the name.", contentPackage: element.ContentPackage); string itemPrefabName = element.GetAttributeString("name", ""); ItemPrefab = ItemPrefab.Prefabs.Find(m => m.NameMatches(itemPrefabName, StringComparison.InvariantCultureIgnoreCase) || m.Tags.Contains(itemPrefabName)); if (ItemPrefab == null) { - DebugConsole.ThrowError("Error in StatusEffect \"" + parentDebugName + "\" - item prefab \"" + itemPrefabName + "\" not found."); + DebugConsole.ThrowError("Error in StatusEffect \"" + parentDebugName + "\" - item prefab \"" + itemPrefabName + "\" not found.", contentPackage: element.ContentPackage); } } else @@ -246,12 +246,12 @@ namespace Barotrauma if (string.IsNullOrEmpty(itemPrefabIdentifier)) itemPrefabIdentifier = element.GetAttributeString("identifiers", ""); if (string.IsNullOrEmpty(itemPrefabIdentifier)) { - DebugConsole.ThrowError("Invalid item spawn in StatusEffect \"" + parentDebugName + "\" - identifier not found in the element \"" + element.ToString() + "\""); + DebugConsole.ThrowError("Invalid item spawn in StatusEffect \"" + parentDebugName + "\" - identifier not found in the element \"" + element.ToString() + "\".", contentPackage: element.ContentPackage); } ItemPrefab = ItemPrefab.Prefabs.Find(m => m.Identifier == itemPrefabIdentifier); if (ItemPrefab == null) { - DebugConsole.ThrowError("Error in StatusEffect config - item prefab with the identifier \"" + itemPrefabIdentifier + "\" not found."); + DebugConsole.ThrowError("Error in StatusEffect config - item prefab with the identifier \"" + itemPrefabIdentifier + "\" not found.", contentPackage: element.ContentPackage); return; } } @@ -332,7 +332,7 @@ namespace Barotrauma /// public readonly bool TriggerTalents; - public GiveSkill(XElement element, string parentDebugName) + public GiveSkill(ContentXElement element, string parentDebugName) { SkillIdentifier = element.GetAttributeIdentifier(nameof(SkillIdentifier), Identifier.Empty); Amount = element.GetAttributeFloat(nameof(Amount), 0); @@ -340,7 +340,7 @@ namespace Barotrauma if (SkillIdentifier == Identifier.Empty) { - DebugConsole.ThrowError($"GiveSkill StatusEffect did not have a skill identifier defined in {parentDebugName}!"); + DebugConsole.ThrowError($"GiveSkill StatusEffect did not have a skill identifier defined in {parentDebugName}!", contentPackage: element.ContentPackage); } } } @@ -410,12 +410,12 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.No)] public bool InheritEventTags { get; private set; } - public CharacterSpawnInfo(XElement element, string parentDebugName) + public CharacterSpawnInfo(ContentXElement element, string parentDebugName) { SerializableProperties = SerializableProperty.DeserializeProperties(this, element); if (SpeciesName.IsEmpty) { - DebugConsole.ThrowError($"Invalid character spawn ({Name}) in StatusEffect \"{parentDebugName}\" - identifier not found in the element \"{element}\""); + DebugConsole.ThrowError($"Invalid character spawn ({Name}) in StatusEffect \"{parentDebugName}\" - identifier not found in the element \"{element}\".", contentPackage: element.ContentPackage); } } } @@ -798,7 +798,7 @@ namespace Barotrauma { if (!Enum.TryParse(s, true, out TargetType targetType)) { - DebugConsole.ThrowError($"Invalid target type \"{s}\" in StatusEffect ({parentDebugName})"); + DebugConsole.ThrowError($"Invalid target type \"{s}\" in StatusEffect ({parentDebugName})", contentPackage: element.ContentPackage); } else { @@ -837,7 +837,7 @@ namespace Barotrauma case "type": if (!Enum.TryParse(attribute.Value, true, out type)) { - DebugConsole.ThrowError($"Invalid action type \"{attribute.Value}\" in StatusEffect ({parentDebugName})"); + DebugConsole.ThrowError($"Invalid action type \"{attribute.Value}\" in StatusEffect ({parentDebugName})", contentPackage: element.ContentPackage); } break; case "targettype": @@ -866,11 +866,11 @@ namespace Barotrauma case "comparison": if (!Enum.TryParse(attribute.Value, ignoreCase: true, out conditionalLogicalOperator)) { - DebugConsole.ThrowError($"Invalid conditional comparison type \"{attribute.Value}\" in StatusEffect ({parentDebugName})"); + DebugConsole.ThrowError($"Invalid conditional comparison type \"{attribute.Value}\" in StatusEffect ({parentDebugName})", contentPackage: element.ContentPackage); } break; case "sound": - DebugConsole.ThrowError($"Error in StatusEffect ({parentDebugName}): sounds should be defined as child elements of the StatusEffect, not as attributes."); + DebugConsole.ThrowError($"Error in StatusEffect ({parentDebugName}): sounds should be defined as child elements of the StatusEffect, not as attributes.", contentPackage: element.ContentPackage); break; case "range": if (!HasTargetType(TargetType.NearbyCharacters) && !HasTargetType(TargetType.NearbyItems)) @@ -951,7 +951,7 @@ namespace Barotrauma RelatedItem newRequiredItem = RelatedItem.Load(subElement, returnEmpty: false, parentDebugName: parentDebugName); if (newRequiredItem == null) { - DebugConsole.ThrowError("Error in StatusEffect config - requires an item with no identifiers."); + DebugConsole.ThrowError("Error in StatusEffect config - requires an item with no identifiers.", contentPackage: element.ContentPackage); continue; } requiredItems.Add(newRequiredItem); @@ -973,12 +973,12 @@ namespace Barotrauma AfflictionPrefab afflictionPrefab; if (subElement.GetAttribute("name") != null) { - DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - define afflictions using identifiers instead of names."); + DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - define afflictions using identifiers instead of names.", contentPackage: element.ContentPackage); string afflictionName = subElement.GetAttributeString("name", ""); afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Name.Equals(afflictionName, StringComparison.OrdinalIgnoreCase)); if (afflictionPrefab == null) { - DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - Affliction prefab \"" + afflictionName + "\" not found."); + DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - Affliction prefab \"" + afflictionName + "\" not found.", contentPackage: element.ContentPackage); continue; } } @@ -988,7 +988,7 @@ namespace Barotrauma afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier == afflictionIdentifier); if (afflictionPrefab == null) { - DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - Affliction prefab with the identifier \"" + afflictionIdentifier + "\" not found."); + DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - Affliction prefab with the identifier \"" + afflictionIdentifier + "\" not found.", contentPackage: element.ContentPackage); continue; } } @@ -1001,7 +1001,7 @@ namespace Barotrauma case "reduceaffliction": if (subElement.GetAttribute("name") != null) { - DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - define afflictions using identifiers or types instead of names."); + DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - define afflictions using identifiers or types instead of names.", contentPackage: element.ContentPackage); ReduceAffliction.Add(( subElement.GetAttributeIdentifier("name", ""), subElement.GetAttributeFloat(1.0f, "amount", "strength", "reduceamount"))); @@ -1016,7 +1016,7 @@ namespace Barotrauma } else { - DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - Affliction prefab with the identifier or type \"" + name + "\" not found."); + DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - Affliction prefab with the identifier or type \"" + name + "\" not found.", contentPackage: element.ContentPackage); } } break; @@ -1374,6 +1374,10 @@ namespace Barotrauma if (OnlyOutside && character.CurrentHull != null) { return false; } if (TargetIdentifiers == null) { return true; } if (TargetIdentifiers.Contains("character")) { return true; } + if (TargetIdentifiers.Contains("monster")) + { + return !character.IsHuman && character.Group != CharacterPrefab.HumanSpeciesName; + } return TargetIdentifiers.Contains(character.SpeciesName); } @@ -1697,7 +1701,7 @@ namespace Barotrauma if (limb.Removed) { continue; } if (limb.IsSevered) { continue; } if (targetLimbs != null && !targetLimbs.Contains(limb.type)) { continue; } - AttackResult result = limb.character.DamageLimb(position, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source, allowStacking: !setValue); + AttackResult result = limb.character.DamageLimb(position, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: Vector2.Zero, attacker: affliction.Source, allowStacking: !setValue); limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability, disableDeltaTime ? result.Damage : result.Damage / deltaTime, allowBeheading: true, attacker: affliction.Source); RegisterTreatmentResults(user, entity as Item, limb, affliction, result); //only apply non-limb-specific afflictions to the first limb @@ -1710,7 +1714,7 @@ namespace Barotrauma if (limb.character.Removed || limb.Removed) { continue; } if (targetLimbs != null && !targetLimbs.Contains(limb.type)) { continue; } newAffliction = GetMultipliedAffliction(affliction, entity, limb.character, deltaTime, multiplyAfflictionsByMaxVitality); - AttackResult result = limb.character.DamageLimb(position, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source, allowStacking: !setValue); + AttackResult result = limb.character.DamageLimb(position, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: Vector2.Zero, attacker: affliction.Source, allowStacking: !setValue); limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability, disableDeltaTime ? result.Damage : result.Damage / deltaTime, allowBeheading: true, attacker: affliction.Source); RegisterTreatmentResults(user, entity as Item, limb, affliction, result); } @@ -1878,7 +1882,7 @@ namespace Barotrauma if (FireSize > 0.0f && entity != null) { - var fire = new FireSource(position, hull); + var fire = new FireSource(position, hull, sourceCharacter: user); fire.Size = new Vector2(FireSize, fire.Size.Y); } @@ -2245,8 +2249,8 @@ namespace Barotrauma { OnItemSpawned(newItem, chosenItemSpawnInfo); }); + break; } - break; } } } @@ -2409,7 +2413,7 @@ namespace Barotrauma { if (limb.character.Removed || limb.Removed) { continue; } newAffliction = element.Parent.GetMultipliedAffliction(affliction, element.Entity, limb.character, deltaTime, element.Parent.multiplyAfflictionsByMaxVitality); - var result = limb.character.DamageLimb(limb.WorldPosition, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: element.User); + var result = limb.character.DamageLimb(limb.WorldPosition, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: Vector2.Zero, attacker: element.User); element.Parent.RegisterTreatmentResults(element.Parent.user, element.Entity as Item, limb, affliction, result); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs index c42374a47..37064ac10 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs @@ -481,6 +481,7 @@ namespace Barotrauma } pathFinder = null; + roundData = null; } private static void UnlockAchievement(Character recipient, Identifier identifier) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Tags.cs b/Barotrauma/BarotraumaShared/SharedSource/Tags.cs index 282c7d503..830c43168 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Tags.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Tags.cs @@ -45,7 +45,7 @@ public static class Tags public static readonly Identifier ToolItem = "tool".ToIdentifier(); public static readonly Identifier LogicItem = "logic".ToIdentifier(); public static readonly Identifier NavTerminal = "navterminal".ToIdentifier(); - public static readonly Identifier IdCard = "identitycard".ToIdentifier(); + public static readonly Identifier IdCardTag = "identitycard".ToIdentifier(); public static readonly Identifier WireItem = "wire".ToIdentifier(); public static readonly Identifier ChairItem = "chair".ToIdentifier(); public static readonly Identifier ArtifactHolder = "artifactholder".ToIdentifier(); @@ -55,6 +55,8 @@ public static class Tags public static readonly Identifier DontSellItems = "dontsellitems".ToIdentifier(); public static readonly Identifier CargoContainer = "cargocontainer".ToIdentifier(); + public static readonly Identifier CargoMissionItem = "cargomission".ToIdentifier(); + public static readonly Identifier ItemIgnoredByAI = "ignorebyai".ToIdentifier(); public static readonly Identifier GuardianShelter = "guardianshelter".ToIdentifier(); @@ -98,5 +100,15 @@ public static class Tags /// public static readonly Identifier DespawnContainer = "despawncontainer".ToIdentifier(); + /// + /// Used by talents to target all stat identifiers + /// + public static readonly Identifier StatIdentifierTargetAll = "all".ToIdentifier(); + + public static readonly Identifier HelmSkill = "helm".ToIdentifier(); + public static readonly Identifier WeaponsSkill = "weapons".ToIdentifier(); + public static readonly Identifier ElectricalSkill = "electrical".ToIdentifier(); + public static readonly Identifier MechanicalSkill = "mechanical".ToIdentifier(); + public static readonly Identifier MedicalSkill = "medical".ToIdentifier(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TagLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TagLString.cs index 7c8ede811..2d978ed4e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TagLString.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TagLString.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; using Barotrauma.Extensions; namespace Barotrauma @@ -32,7 +33,7 @@ namespace Barotrauma (string value, bool loaded) tryLoad(LanguageIdentifier lang) { - IReadOnlyList candidates = Array.Empty(); + IReadOnlyList candidates = Array.Empty(); int tagIndex = 0; if (TextManager.TextPacks.TryGetValue(lang, out var packs)) @@ -50,8 +51,17 @@ namespace Barotrauma } } - bool loaded = candidates.Count > 0; - return (loaded ? candidates.GetRandomUnsynced() : "", loaded); + if (candidates.Count == 0) { return (string.Empty, loaded: false); } + var firstOverride = candidates.FirstOrDefault(c => c.IsOverride); + if (firstOverride != default) + { + //if there's overrides defined, choose from the first pack that defines overrides + return (candidates.Where(static c => c.IsOverride).Where(c => c.TextPack == firstOverride.TextPack).GetRandomUnsynced().String, loaded: true); + } + else + { + return (candidates.GetRandomUnsynced().String, loaded: true); + } } var (value, loaded) = tryLoad(Language); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/RichString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/RichString.cs index f370ddd71..7e8300c5c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/RichString.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/RichString.cs @@ -9,6 +9,7 @@ namespace Barotrauma { protected bool loaded = false; protected LanguageIdentifier language = LanguageIdentifier.None; + private int languageVersion = 0; protected string cachedSanitizedValue = ""; public string SanitizedValue @@ -32,9 +33,9 @@ namespace Barotrauma #if CLIENT private readonly GUIFont? font; private readonly GUIComponentStyle? componentStyle; - private readonly bool forceUpperCase = false; + private bool forceUpperCase = false; - private bool fontOrStyleForceUpperCase + private bool FontOrStyleForceUpperCase => font is { ForceUpperCase: true } || componentStyle is { ForceUpperCase: true }; #endif @@ -91,8 +92,9 @@ namespace Barotrauma { return NestedStr.Loaded != loaded || language != GameSettings.CurrentConfig.Language + || languageVersion != TextManager.LanguageVersion #if CLIENT - || (fontOrStyleForceUpperCase != forceUpperCase) + || (FontOrStyleForceUpperCase != forceUpperCase) #endif ; } @@ -100,9 +102,9 @@ namespace Barotrauma public void RetrieveValue() { #if CLIENT - NestedStr = fontOrStyleForceUpperCase ? originalStr.ToUpper() : originalStr; + NestedStr = FontOrStyleForceUpperCase ? originalStr.ToUpper() : originalStr; + forceUpperCase = FontOrStyleForceUpperCase; #endif - if (shouldParseRichTextData) { RichTextData = Barotrauma.RichTextData.GetRichTextData(NestedStr.Value, out cachedSanitizedValue); @@ -113,6 +115,7 @@ namespace Barotrauma } if (postProcess != null) { cachedSanitizedValue = postProcess(cachedSanitizedValue); } language = GameSettings.CurrentConfig.Language; + languageVersion = TextManager.LanguageVersion; loaded = NestedStr.Loaded; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs index 8fb92ae72..f8b5849f3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs @@ -23,7 +23,7 @@ namespace Barotrauma public static bool DebugDraw; public readonly static LanguageIdentifier DefaultLanguage = "English".ToLanguageIdentifier(); - public readonly static ConcurrentDictionary> TextPacks = new ConcurrentDictionary>(); + public readonly static ConcurrentDictionary> TextPacks = new ConcurrentDictionary>(); public static IEnumerable AvailableLanguages => TextPacks.Keys; private readonly static Dictionary> cachedStrings = @@ -160,22 +160,48 @@ namespace Barotrauma return TextPacks[GameSettings.CurrentConfig.Language].Any(p => p.Texts.ContainsKey(tag)); } + public static bool ContainsTag(Identifier tag, LanguageIdentifier language) + { + return TextPacks[language].Any(p => p.Texts.ContainsKey(tag)); + } public static IEnumerable GetAll(string tag) => GetAll(tag.ToIdentifier()); public static IEnumerable GetAll(Identifier tag) { - return TextPacks[GameSettings.CurrentConfig.Language] + var allTexts = TextPacks[GameSettings.CurrentConfig.Language] .SelectMany(p => p.Texts.TryGetValue(tag, out var value) - ? (IEnumerable)value - : Array.Empty()); + ? (IEnumerable)value + : Array.Empty()); + + var firstOverride = allTexts.FirstOrDefault(t => t.IsOverride); + if (firstOverride != default) + { + return allTexts.Where(t => t.IsOverride && t.TextPack == firstOverride.TextPack).Select(t => t.String); + } + else + { + return allTexts.Select(t => t.String); + } } - + public static IEnumerable> GetAllTagTextPairs() { - return TextPacks[GameSettings.CurrentConfig.Language] - .SelectMany(p => p.Texts) - .SelectMany(kvp => kvp.Value.Select(v => new KeyValuePair(kvp.Key, v))); + var allTexts = TextPacks[GameSettings.CurrentConfig.Language] + .SelectMany(p => p.Texts); + + var firstOverride = allTexts.SelectMany(kvp => kvp.Value).FirstOrDefault(t => t.IsOverride); + if (firstOverride != default) + { + return allTexts + .Where(kvp => kvp.Value.Any(t => t.IsOverride && t.TextPack == firstOverride.TextPack)) + .SelectMany(kvp => kvp.Value.Select(v => new KeyValuePair(kvp.Key, v.String))); + } + else + { + return allTexts + .SelectMany(kvp => kvp.Value.Select(v => new KeyValuePair(kvp.Key, v.String))); + } } public static IEnumerable GetTextFiles() @@ -213,13 +239,21 @@ namespace Barotrauma } public static LocalizedString Get(params Identifier[] tags) + { + if (tags.Length == 1) + { + return Get(tags[0]); + } + return new TagLString(tags); + } + + public static LocalizedString Get(Identifier tag) { TagLString? str = null; lock (cachedStrings) { - if (tags.Length == 1 && !nonCacheableTags.Contains(tags[0])) + if (!nonCacheableTags.Contains(tag)) { - var tag = tags[0]; if (cachedStrings.TryGetValue(tag, out var strRef)) { if (!strRef.TryGetTarget(out str)) @@ -246,15 +280,18 @@ namespace Barotrauma } else { - str = new TagLString(tags); + str = new TagLString(tag); cachedStrings.Add(tag, new WeakReference(str)); } } } } - return str ?? new TagLString(tags); + return str ?? new TagLString(tag); } - + + public static LocalizedString Get(string tag) + => Get(tag.ToIdentifier()); + public static LocalizedString Get(params string[] tags) => Get(tags.ToIdentifiers()); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/TextPack.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/TextPack.cs index 05ead3562..39d447e24 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/TextPack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/TextPack.cs @@ -48,7 +48,13 @@ namespace Barotrauma public readonly LanguageIdentifier Language; - public readonly ImmutableDictionary> Texts; + + public readonly record struct Text( + string String, + bool IsOverride, + TextPack TextPack); + + public readonly ImmutableDictionary> Texts; public readonly string TranslatedName; public readonly bool NoWhitespace; @@ -56,24 +62,47 @@ namespace Barotrauma { ContentFile = file; - var languageName = mainElement.GetAttributeIdentifier("language", TextManager.DefaultLanguage.Value); + var languageName = mainElement.GetAttributeIdentifier("language", Identifier.Empty); + if (languageName.IsEmpty) + { + DebugConsole.AddWarning($"Language not defined in text file \"{file.Path}\". Setting the language as {TextManager.DefaultLanguage}.", + mainElement.ContentPackage); + languageName = TextManager.DefaultLanguage.Value; + } Language = language; TranslatedName = mainElement.GetAttributeString("translatedname", languageName.Value); NoWhitespace = mainElement.GetAttributeBool("nowhitespace", false); - Dictionary> texts = new Dictionary>(); - foreach (var element in mainElement.Elements()) + Dictionary> texts = new Dictionary>(); + LoadElements(mainElement, isOverride: mainElement.IsOverride()); + + void LoadElements(XElement parentElement, bool isOverride) { - Identifier elemName = element.NameAsIdentifier(); - if (!texts.ContainsKey(elemName)) { texts.Add(elemName, new List()); } - texts[elemName].Add(element.ElementInnerText() - .Replace(@"\n", "\n") - .Replace("&", "&") - .Replace("<", "<") - .Replace(">", ">") - .Replace(""", "\"") - .Replace("'", "'")); + foreach (var element in parentElement.Elements()) + { + Identifier elemName = element.NameAsIdentifier(); + + if (element.IsOverride()) + { + LoadElements(element, isOverride: true); + } + else + { + if (!texts.ContainsKey(elemName)) { texts.Add(elemName, new List()); } + + string str = element.ElementInnerText() + .Replace(@"\n", "\n") + .Replace("&", "&") + .Replace("<", "<") + .Replace(">", ">") + .Replace(""", "\"") + .Replace("'", "'"); + + texts[elemName].Add(new Text(str, isOverride, this)); + } + } } + Texts = texts.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray())).ToImmutableDictionary(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEvent.cs index 556ad87e5..99ba1035e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEvent.cs @@ -96,8 +96,8 @@ namespace Barotrauma traitor.Character.IsTraitor = true; AddTarget(Tags.Traitor, traitor.Character); AddTarget(Tags.AnyTraitor, traitor.Character); - AddTargetPredicate(Tags.NonTraitor, e => e is Character c && (c.IsPlayer || c.IsBot) && !c.IsTraitor && c.TeamID == traitor.TeamID && !c.IsIncapacitated); - AddTargetPredicate(Tags.NonTraitorPlayer, e => e is Character c && c.IsPlayer && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated); + AddTargetPredicate(Tags.NonTraitor, TargetPredicate.EntityType.Character, e => e is Character c && (c.IsPlayer || c.IsBot) && !c.IsTraitor && c.TeamID == traitor.TeamID && !c.IsIncapacitated); + AddTargetPredicate(Tags.NonTraitorPlayer, TargetPredicate.EntityType.Character, e => e is Character c && c.IsPlayer && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated); } public void SetSecondaryTraitors(IEnumerable traitors) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEventPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEventPrefab.cs index e941208e9..1353c1bbf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEventPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEventPrefab.cs @@ -25,7 +25,8 @@ namespace Barotrauma MissionType = element.GetAttributeEnum(nameof(MissionType), MissionType.None); if (MissionIdentifier.IsEmpty && MissionTag.IsEmpty && MissionType == MissionType.None) { - DebugConsole.ThrowError($"Error in traitor event \"{prefab.Identifier}\". Mission requirement with no {nameof(MissionIdentifier)}, {nameof(MissionTag)} or {nameof(MissionType)}."); + DebugConsole.ThrowError($"Error in traitor event \"{prefab.Identifier}\". Mission requirement with no {nameof(MissionIdentifier)}, {nameof(MissionTag)} or {nameof(MissionType)}.", + contentPackage: prefab.ContentPackage); } } @@ -72,7 +73,7 @@ namespace Barotrauma //feels a little weird to have something this specific here, but couldn't think of a better way to implement this public ImmutableArray RequiredItemConditionals; - public LevelRequirement(XElement element, TraitorEventPrefab prefab) + public LevelRequirement(ContentXElement element, TraitorEventPrefab prefab) { levelType = element.GetAttributeEnum(nameof(LevelType), LevelType.Any); LocationTypes = element.GetAttributeIdentifierArray("locationtype", Array.Empty()).ToImmutableArray(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs index 88731d001..920dbb269 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs @@ -333,7 +333,8 @@ namespace Barotrauma { DebugConsole.AddWarning($"Failed to save upgrade \"{Prefab.Name}\" on {TargetEntity.Name} because property reference \"{propertyRef.Name}\" is missing original values. \n" + "Upgrades should always call Upgrade.ApplyUpgrade() or manually set the original value in a property reference after they have been added. \n" + - "If you are not a developer submit a bug report at https://github.com/Regalis11/Barotrauma/issues/."); + "If you are not a developer submit a bug report at https://github.com/Regalis11/Barotrauma/issues/.", + Prefab.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs index 9114c6f6a..8365930bb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs @@ -18,12 +18,8 @@ namespace Barotrauma public readonly int IncreaseHigh; - public readonly UpgradePrefab Prefab; - public UpgradePrice(UpgradePrefab prefab, ContentXElement element) { - Prefab = prefab; - IncreaseLow = UpgradePrefab.ParsePercentage(element.GetAttributeString("increaselow", string.Empty)!, "IncreaseLow".ToIdentifier(), element, suppressWarnings: prefab.SuppressWarnings); @@ -34,20 +30,21 @@ namespace Barotrauma if (BasePrice == -1) { - if (prefab.SuppressWarnings) + if (!prefab.SuppressWarnings) { DebugConsole.AddWarning($"Price attribute \"baseprice\" is not defined for {prefab?.Identifier}.\n " + - "The value has been assumed to be '1000'."); + "The value has been assumed to be '1000'.", + prefab!.ContentPackage); BasePrice = 1000; } } } - public int GetBuyPrice(int level, Location? location = null, ImmutableHashSet? characterList = null) + public int GetBuyPrice(UpgradePrefab prefab, int level, Location? location = null, ImmutableHashSet? characterList = null) { float price = BasePrice; - int maxLevel = Prefab.MaxLevel; + int maxLevel = prefab.MaxLevel; float lerpAmount = maxLevel is 0 ? level // avoid division by 0 @@ -342,7 +339,8 @@ namespace Barotrauma { DebugConsole.AddWarning($"Upgrade \"{prefab.Identifier}\" is affecting a property that is also being affected by \"{matchingPrefab.Identifier}\".\n" + "This is unsupported and might yield unexpected results if both upgrades are applied at the same time to the same item.\n" + - "Add the attribute suppresswarnings=\"true\" to your XML element to disable this warning if you know what you're doing."); + "Add the attribute suppresswarnings=\"true\" to your XML element to disable this warning if you know what you're doing.", + prefab.ContentPackage); } } } @@ -398,11 +396,11 @@ namespace Barotrauma public UpgradePrefab(ContentXElement element, UpgradeModulesFile file) : base(element, file) { - Name = element.GetAttributeString("name", string.Empty)!; - Description = element.GetAttributeString("description", string.Empty)!; - MaxLevel = element.GetAttributeInt("maxlevel", 1); - SuppressWarnings = element.GetAttributeBool("supresswarnings", false); - HideInMenus = element.GetAttributeBool("hideinmenus", false); + Name = element.GetAttributeString(nameof(Name), string.Empty)!; + Description = element.GetAttributeString(nameof(Description), string.Empty)!; + MaxLevel = element.GetAttributeInt(nameof(MaxLevel), 1); + SuppressWarnings = element.GetAttributeBool(nameof(SuppressWarnings), false); + HideInMenus = element.GetAttributeBool(nameof(HideInMenus), false); SourceElement = element; var targetProperties = new Dictionary(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs index 195cba7fc..1f116b00f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs @@ -139,17 +139,17 @@ namespace Barotrauma return new Rectangle(rect.X - amount, rect.Y + amount, rect.Width + amount * 2, rect.Height + amount * 2); } - public static int VectorOrientation(Vector2 p1, Vector2 p2, Vector2 p) + /// + /// Given three points A, B and C, + /// returns 1 if AC is oriented clockwise relative to AB, + /// -1 if AC is oriented counter-clockwise relative to AB, + /// or 0 if A, B and C are collinear. + /// + public static int VectorOrientation(Vector2 pointA, Vector2 pointB, Vector2 pointC) { - // Determinant - float Orin = (p2.X - p1.X) * (p.Y - p1.Y) - (p.X - p1.X) * (p2.Y - p1.Y); + float determinant = (pointB.X - pointA.X) * (pointC.Y - pointA.Y) - (pointC.X - pointA.X) * (pointB.Y - pointA.Y); - if (Orin > 0) - return -1; // (* Orientation is to the left-hand side *) - if (Orin < 0) - return 1; // (* Orientation is to the right-hand side *) - - return 0; // (* Orientation is neutral aka collinear *) + return -Math.Sign(determinant); } @@ -412,41 +412,29 @@ namespace Barotrauma return false; } - /*public static List GetLineRectangleIntersections(Vector2 a1, Vector2 a2, Rectangle rect) - { - List intersections = new List(); + public static Vector2 FlipX(this Vector2 vector) + => new Vector2(-vector.X, vector.Y); - Vector2? intersection = GetAxisAlignedLineIntersection(a1, a2, - new Vector2(rect.X, rect.Y), - new Vector2(rect.Right, rect.Y), - true); + public static Vector2 FlipY(this Vector2 vector) + => new Vector2(vector.X, -vector.Y); - if (intersection != null) intersections.Add((Vector2)intersection); + public static Vector2 YX(this Vector2 vector) + => new Vector2(x: vector.Y, y: vector.X); + + public static Point FlipY(this Point point) + => new Point(point.X, -point.Y); - intersection = GetAxisAlignedLineIntersection(a1, a2, - new Vector2(rect.X, rect.Y - rect.Height), - new Vector2(rect.Right, rect.Y - rect.Height), - true); + public static Point YX(this Point point) + => new Point(x: point.Y, y: point.X); + + public static Vector2 RotatedUnitXRadians(float radians) + => new Vector2(MathF.Cos(radians), MathF.Sin(radians)); - if (intersection != null) intersections.Add((Vector2)intersection); - - intersection = GetAxisAlignedLineIntersection(a1, a2, - new Vector2(rect.X, rect.Y), - new Vector2(rect.X, rect.Y - rect.Height), - false); - - if (intersection != null) intersections.Add((Vector2)intersection); - - intersection = GetAxisAlignedLineIntersection(a1, a2, - new Vector2(rect.Right, rect.Y), - new Vector2(rect.Right, rect.Y - rect.Height), - false); - - if (intersection != null) intersections.Add((Vector2)intersection); - - return intersections; - }*/ + public static Vector2 RotatedUnitYRadians(float radians) + => RotatedUnitXRadians(radians).YX().FlipX(); + public static Vector2 Round(this Vector2 vector) + => new Vector2((int)MathF.Round(vector.X), (int)MathF.Round(vector.Y)); /// /// Get the intersections between a line (either infinite or a line segment) and a circle @@ -1103,11 +1091,11 @@ namespace Barotrauma } } - class CompareCCW : IComparer + class CompareCW : IComparer { private Vector2 center; - public CompareCCW(Vector2 center) + public CompareCW(Vector2 center) { this.center = center; } @@ -1118,25 +1106,43 @@ namespace Barotrauma public static int Compare(Vector2 a, Vector2 b, Vector2 center) { - if (a == b) return 0; - if (a.X - center.X >= 0 && b.X - center.X < 0) return -1; - if (a.X - center.X < 0 && b.X - center.X >= 0) return 1; + if (a == b) { return 0; } + if (a.X - center.X >= 0 && b.X - center.X < 0) { return 1; } + if (a.X - center.X < 0 && b.X - center.X >= 0) { return -1; } if (a.X - center.X == 0 && b.X - center.X == 0) { - if (a.Y - center.Y >= 0 || b.Y - center.Y >= 0) return Math.Sign(b.Y - a.Y); + if (a.Y - center.Y >= 0 || b.Y - center.Y >= 0) { return Math.Sign(a.Y - b.Y); } return Math.Sign(a.Y - b.Y); } // compute the cross product of vectors (center -> a) x (center -> b) float det = (a.X - center.X) * (b.Y - center.Y) - (b.X - center.X) * (a.Y - center.Y); - if (det < 0) return -1; - if (det > 0) return 1; + if (det < 0) { return 1; } + if (det > 0) { return -1; } // points a and b are on the same line from the center // check which point is closer to the center float d1 = (a.X - center.X) * (a.X - center.X) + (a.Y - center.Y) * (a.Y - center.Y); float d2 = (b.X - center.X) * (b.X - center.X) + (b.Y - center.Y) * (b.Y - center.Y); - return Math.Sign(d2 - d1); + return Math.Sign(d1 - d2); + } + } + + class CompareCCW : IComparer + { + private Vector2 center; + + public CompareCCW(Vector2 center) + { + this.center = center; + } + public int Compare(Vector2 a, Vector2 b) + { + return -CompareCW.Compare(a, b, center); + } + public static int Compare(Vector2 a, Vector2 b, Vector2 center) + { + return -CompareCW.Compare(a, b, center); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs index 7d2a8d983..bd3f78492 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs @@ -160,7 +160,14 @@ namespace Barotrauma { string subName = subElement.GetAttributeString("name", ""); string ownedSubPath = Path.Combine(TempPath, subName + ".sub"); - ownedSubmarines.Add(new SubmarineInfo(ownedSubPath)); + if (!File.Exists(ownedSubPath)) + { + DebugConsole.ThrowError($"Could not find the submarine \"{subName}\" ({ownedSubPath})! The save file may be corrupted. Removing the submarine from owned submarines..."); + } + else + { + ownedSubmarines.Add(new SubmarineInfo(ownedSubPath)); + } } return ownedSubmarines; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Shapes/Quad2D.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Shapes/Quad2D.cs new file mode 100644 index 000000000..af6284bc9 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Shapes/Quad2D.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; + +namespace Barotrauma; + +readonly record struct Quad2D(Vector2 A, Vector2 B, Vector2 C, Vector2 D) +{ + public Vector2 Centroid => (A + B + C + D) / 4; + + public static Quad2D FromRectangle(RectangleF rectangle) + { + return new Quad2D( + A: (rectangle.Left, rectangle.Top), + B: (rectangle.Right, rectangle.Top), + C: (rectangle.Right, rectangle.Bottom), + D: (rectangle.Left, rectangle.Bottom)); + } + + public static Quad2D FromSubmarineRectangle(RectangleF rectangle) + { + return new Quad2D( + A: (rectangle.X, rectangle.Y), + B: (rectangle.X + rectangle.Width, rectangle.Y), + C: (rectangle.X + rectangle.Width, rectangle.Y - rectangle.Height), + D: (rectangle.X, rectangle.Y - rectangle.Height)); + } + + public Quad2D Rotated(float radians) + { + return new Quad2D( + A: MathUtils.RotatePointAroundTarget(point: A, target: Centroid, radians: radians), + B: MathUtils.RotatePointAroundTarget(point: B, target: Centroid, radians: radians), + C: MathUtils.RotatePointAroundTarget(point: C, target: Centroid, radians: radians), + D: MathUtils.RotatePointAroundTarget(point: D, target: Centroid, radians: radians)); + } + + public RectangleF BoundingAxisAlignedRectangle + { + get + { + Vector2 min = ( + X: Math.Min(A.X, Math.Min(B.X, Math.Min(C.X, D.X))), + Y: Math.Min(A.Y, Math.Min(B.Y, Math.Min(C.Y, D.Y)))); + Vector2 max = ( + X: Math.Max(A.X, Math.Max(B.X, Math.Max(C.X, D.X))), + Y: Math.Max(A.Y, Math.Max(B.Y, Math.Max(C.Y, D.Y)))); + return new RectangleF(location: min, size: max - min); + } + } + + public bool TryGetEdges(Span<(Vector2 A, Vector2 B)> outputSpan) + { + if (outputSpan.Length < 4) { return false; } + + outputSpan[0] = (A, B); + outputSpan[1] = (B, C); + outputSpan[2] = (C, D); + outputSpan[3] = (D, A); + return true; + } + + public bool Contains(Vector2 point) + { + // Break up the quad into two triangles and then see if the point is in either triangle. + // Since quads can be concave, care needs to be taken when splitting in two. + + (Triangle2D triangle1, Triangle2D triangle2) + = (new Triangle2D(A, B, C), new Triangle2D(A, D, C)); + + // If D is inside of the triangle ABC, or B is inside of the triangle ADC, + // then the quad is concave and we split at the wrong diagonal. + // Splitting at the other diagonal should be fine. + if (triangle1.Contains(D) || triangle2.Contains(B)) + { + (triangle1, triangle2) = (new Triangle2D(B, C, D), new Triangle2D(B, A, D)); + } + + return triangle1.Contains(point) || triangle2.Contains(point); + } + + public bool Intersects(Quad2D other) + { + if (!BoundingAxisAlignedRectangle.Intersects(other.BoundingAxisAlignedRectangle)) + { + return false; + } + + if (Contains(other.A)) { return true; } + if (Contains(other.B)) { return true; } + if (Contains(other.C)) { return true; } + if (Contains(other.D)) { return true; } + + if (other.Contains(A)) { return true; } + if (other.Contains(B)) { return true; } + if (other.Contains(C)) { return true; } + if (other.Contains(D)) { return true; } + + Span<(Vector2 A, Vector2 B)> myEdges = stackalloc (Vector2 A, Vector2 B)[4]; + TryGetEdges(myEdges); + Span<(Vector2 A, Vector2 B)> otherEdges = stackalloc (Vector2 A, Vector2 B)[4]; + other.TryGetEdges(otherEdges); + foreach (var edge in myEdges) + { + foreach (var otherEdge in otherEdges) + { + if (MathUtils.LineSegmentsIntersect(edge.A, edge.B, otherEdge.A, otherEdge.B)) { return true; } + } + } + return false; + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Shapes/Triangle2D.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Shapes/Triangle2D.cs new file mode 100644 index 000000000..ab5e4b33e --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Shapes/Triangle2D.cs @@ -0,0 +1,21 @@ +using Microsoft.Xna.Framework; + +namespace Barotrauma; + +readonly record struct Triangle2D(Vector2 A, Vector2 B, Vector2 C) +{ + public bool Contains(Vector2 point) + { + // Get the half-plane that the point lands in, for each side of the triangle + int halfPlaneAb = MathUtils.VectorOrientation(A, B, point); + int halfPlaneBc = MathUtils.VectorOrientation(B, C, point); + int halfPlaneCa = MathUtils.VectorOrientation(C, A, point); + + // The intersection of three half-planes derived from the three sides of the triangle + // is the triangle itself, so check for the point being in those three half-planes + bool allNonNegative = halfPlaneAb >= 0 && halfPlaneBc >= 0 && halfPlaneCa >= 0; + bool allNonPositive = halfPlaneAb <= 0 && halfPlaneBc <= 0 && halfPlaneCa <= 0; + + return allNonNegative || allNonPositive; + } +} diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index f325714d8..73a4ca7f7 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,118 @@ +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.2.6.0 +------------------------------------------------------------------------------------------------------------------------------------------------- + +- Added some more logging to diagnose the mysterious "pipe was broken" crashes. These seem to happen when the server crashes in some way that prevents it from generating a crash report or communicating the reason of the crash to the clients. Now the "pipe is broken" crash report should include the exit code of the server process, giving us more clues for diagnosing the issue, and the server should create simplified crash report if there's some kind of an issue with creating the normal crash report. + +Changes: +- Structures can now be rotated in the sub editor! +- Outpost security is now better at catching thieves: they can do random inspections to check for stolen items (more frequently if your reputation is low or if they've caught someone in your crew stealing), and notice if you're visibly carrying or wearing any stolen items. +- Option to get the items you're buying from an outpost store in your inventory immediately, as opposed to them being delivered to the sub at the start of the round. There's also now an anti-griefing setting for disabling this, in case you want to make sure a griefer can't easily gain access to dangerous items in the outposts. Players with campaign management permissions aren't affected by the setting. +- Added new variants of the Monsters Nearby and Submarine Flooded tracks. +- Minor adjustments to level layouts: all the levels now slope down a bit to give the impression you're actually heading deeper. +- Clicking on a door that's set to not toggle when clicked (= door that just send out an activate_out signal when clicked) doesn't activate the door's "toggle cooldown". +- Bots operating turrets can now lead targets (i.e. aim ahead of a moving target). Bots with a higher weapon skill are better at estimating the future position of the target. +- Minor balance change to sub vs sub combat. AI-controlled subs get at least 2 security instead of 1, so more turrets will be manned. +- Bots can see through windowed walls (also making it easier for them to spot stealing). +- Added bilge pumps and duct blocks throughout outposts. Otherwise the outposts can never drain if the player causes a flood. +- Removed karma restrictions from jobs. This feature wasn't communicated anywhere, and it often just seemed like a bug when someone didn't get assign the job they wanted due to their karma being too low. +- Added bullet casing particles to firearms. +- Adjusted default throwing pose a bit: characters don't extend their arm as high up, because it could cause the throwable item to hit the ceiling in roughly door-height rooms. +- Most message boxes can be closed by pressing enter (more specifically, all boxes that are created with just the "ok" button). +- ID cards left in a duffel bag are automatically removed at the end of the round. +- Added a light to handheld sonar to indicate when it's on + made the ping a bit higher in pitch to differentiate it from the sub's sonar. +- Reduced the forces applied on characters by fractal guardian's melee attacks. The previous values were so high they often lead to characters getting stunlocked when the attacks threw them against the walls of the ruin. + +Traitors: +- Three new Danger Level 1 traitor events: Exhibitionism, Firing Blanks, The Original Honkero. +- Five new multi-traitor events: Gnawing Cold, The Clobbery Robbery, Husk Roulette, Duck Side of the Moon, Powered by Faith. +- Fixed "Insurgency" achievement not unlocking when completing a traitor event. +- Fixed traitor-specific items sometimes being requested by stores. +- Fixed visibility checks in some traitor events having infinite range. +- Don't allow the victim's body to despawn in the "dead or alive" event because it'd interfere with completing the event (others finding the body). + +Optimization: +- Significant optimization to situations where a bot is idling and has trouble finding a path to another hull. Caused performance issues in colonies in particular. +- Fixed a memory leak that caused the memory usage to increase every round. +- Fixed firing turrets (or doing any other action that spawns lots of new entities) causing an enormous performance drop in some situations. Had to do with specific scripted events re-checking their targets whenever a new entity spawned, which was needlessly heavy and was done unnecessarily often. +- Miscellaneous small optimizations. +- Optimized labels (large labels with a scaled down texture in particular were unnecessarily heavy). +- Optimized Watcher's status effects. + +Multiplayer: +- Option to hide servers from the server list. +- Option to report inappropriate servers on the server list. +- Added "MinimumMidRoundSyncTimeout" server setting. Determines how long the server waits for a mid-round joining client to get in sync before disconnecting them. +- Fixed workshop item downloads sometimes getting stuck when opting to install server mods from the workshop. Happened if the mod had already been installed, was in the process of being installed, or if it couldn't be installed for some reason. +- Fixed dedicated servers with lots of mods enabled not showing up in the server browser. +- Fixed Text and NPCConversation files being required to be in sync between clients and the server, meaning if you were using a mod that changes the game to a different language, it'd get disabled when you joined a server that's not using the mod. +- Fixed lack of unban option in the "manage client" menu and the client context menu. + +Talents: +- Five new assistant talents: Starter Quest, Mule, Jenga Master, Indentured Servitude, Tasty Target. +- Fixed "junction junkie" not working on sonar monitors. +- Fixed a couple of inaccurate talent and mission descriptions in Russian. + +Skill gain balancing: +- Now takes longer to reach max skill level, the Helm skill especially was leveling way too fast. +- Helm skill levels up based on the submarine's speed. +- Added an exponent factor to the skill increases for diminishing returns at higher skill level. +- Increased skill gains at lower levels to compensate. +- Welding gives more skill. +- Fabricating items gives less skill. + +Fixes: +- Fixed "camera zoom effect" taking a very long time to appear when pressure is increasing inside the sub, giving you very little time to react when the pressure gets to a lethal level. +- Fixed turret lights always being forced on at the start of the round. +- Fixed lighting artifacts that looked like thin slivers of light and strange jagged lighting that sometimes occurred in corners of rooms or places where multiple walls meet. +- Fixed lights shining through obstacles in some very specific situations (one common spot was the windowed door at the left side of Dugong's command room). +- Fixed turret lights sometimes disappearing from view when you're far from the turret (but still close enough to see the light). +- Fixed "bad vibrations 2" event getting stuck if you choose the option to sleep. +- Fixed components disappearing from a circuit box if you delete one in the sub editor and undo the deletion. +- Fixed a rare crash when you'd selected text in a textbox and changed the language of the game to one where the word in the textbox is shorter than in the previous language. +- Fixed handcuffs appearing to continuously drop from characters in certain situations in multiplayer (more specifically, if the character has been disabled and re-enabled during the round). +- Workaround to a rare mystery issue that seems to sometimes cause save files to get corrupted. It seems in some situations the submarine doesn't get included in the save: now the game checks whether the submarine is included before attempting to save the file, and refuses to save if it isn't. This does not solve the underlying issue, but should make it easier for us to diagnose why the submarine gets left out, and prevent corrupting the save when it occurs. +- Fixed "operate turret" orders showing the order marker on the turret instead of the periscope. +- Fixed respawn shuttle lights shining through the top of the level. +- Fixed flares (or other "provocative" items) not attracting monsters when you're wearing a diving suit, because the diving suit was also considered a "provocative" item. +- Fixed ability to put items into circuit boxes by dragging and dropping and with the inventory hotkeys. This caused the components to end up in an invalid state, leading to errors when loading the next round. +- Fixed custom ID card tags not getting applied to ID cards when switching subs. +- Fixed subinventories constantly opening and closing when you're hovering the cursor over the inventory slot with 2 different containers equipped (e.g. 2 storage containers). +- Fixed bots being able to take ammo out from non-interactable items or items whose inventory is set to be inaccessible. +- Fixed progress bars being visible through walls when someone uses a repair tool, damages an item with a projectile or a melee weapon, or crowbars a door open. +- Fixed crashing if you're in an outpost level with no outpost. Should normally never happen, but can occur with e.g. outdated save files or mods. +- Fixed ballast flora spores (or any other level object with no actual sprite) not being visible. +- Fixed bots "cleaning up" items from inside artifact transport cases. +- Fixed ability to sell components from inside circuit boxes in outposts. +- Fixed outpost NPCs not caring about the players starting fires. +- Fixed purchased items sometimes spawning in the cargo crates when you've got a cargo mission active, making them potentially very hard to find. +- Fixed inability to select a component you've just placed in the circuit box until you move the component for the first time. +- Fixed health scanner HUD texts overlapping on high resolutions. +- Fixed "maintain position" marker being misplaced on the navigation terminal on some resolutions. +- Fixed chance of tainting the genetic material when refining sometimes being displayed incorrectly on research stations. +- Disable the health interface of the patient in the "good samaritan" event. Prevents being able to heal the opiate overdose "normally" before triggering the event. +- Fixed item hover texts (e.g. "[E] Repair") not refreshing when changing keybinds. +- Fixed fabricator listbox scroll always resetting to the top when someone selects the fabricator. +- Fixed bandoliers not affecting pulse laser fire rate. + +Modding: +- When running an outdated executable (i.e. a mod that overrides the game executable, but hasn't been updated yet after a new vanilla update has been released), there's a notification in the main menu and the "host server" menu warning you about the potential issues caused by running an outdated executable with the other up-to-date vanilla files. +- Console errors and warnings caused by modded content include the name of the mod to make it easier to diagnose which mod is causing some issue or whether it's an issue in the vanilla game. +- Support for overriding texts. Works using elements, the same way as overriding any other content. +- Fixed decorative sprites not being visible on items placed in a container. Did not affect any vanilla content (there were no decorative sprites on any item that can be visible in a container). +- Fixed characters with no AI defined causing crashes. +- Fixed inability to make StatusEffectAction modify the properties of an ItemComponent (only worked on the actual Item). +- Option to have multiple conditionals in CheckConditionalAction. +- Option to limit how many times a GoTo action can be executed (i.e. to create loops that only repeat a given number of times). +- TutorialHighlightAction can now be used in multiplayer too, renamed it as HighlightAction. +- Fixed first matching Containable definition of an ItemContainer determining the hiding, position and rotation of a contained item, disregarding which subcontainer the item is actually in. E.g. if a weapon had 2 subcontainers for magazines, both with a different ItemPos, the ItemPos defined in the first subcontainer would be used for both magazines. +- Fixed stun batons not accepting modded batteries because the RequiredItem s were configured using identifiers instead of tags. +- Fixed Character.CameraShake setting the camera shake regardless if the character the effect is being applied on is the controlled one or not. +- Location names can now be translated. In the vanilla game the names are only translated if you're playing in Chinese. +- Fixed some ConversationAction options going outside the bounds of the dialog if there's a very large number of them. +- Fixed inability to use %ModDir% in LevelGenerationParams.ForceBeaconStation. +- Fixed status effects configured to spawn an item in ContainedInventory not spawning the item if it can't go inside the first item in the contained inventory. + ------------------------------------------------------------------------------------------------------------------------------------------------- v1.1.19.3 ------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Barotrauma/BarotraumaTest/FabricatorQualityRollTests.cs b/Barotrauma/BarotraumaTest/FabricatorQualityRollTests.cs new file mode 100644 index 000000000..128ead59e --- /dev/null +++ b/Barotrauma/BarotraumaTest/FabricatorQualityRollTests.cs @@ -0,0 +1,66 @@ + +using System; +using System.Collections.Generic; +using System.Linq; +using Barotrauma; +using Barotrauma.Items.Components; +using FluentAssertions; +using FsCheck; +using Microsoft.Xna.Framework; +using Xunit; + +namespace TestProject; + +public sealed class FabricatorQualityRollTests +{ + [Fact] + public void TestPercentageChance() + { + Prop.ForAll( + Arb.Generate().Where(static i => i is <= 3 and >= 0).ToArbitrary(), + Arb.Generate().Where(static i => i is <= 100 and >= 50).ToArbitrary(), + Arb.Generate().Where(static i => i is <= 100 and >= 0).ToArbitrary(), (startingQuality, skillLevel, targetLevel) => + { + float plusOneProbability = 0f, + plusTwoProbability = 0f; + + if (skillLevel >= Fabricator.PlusOneQualityBonusThreshold) + { + var bonusChance1 = MathHelper.Lerp(targetLevel, Fabricator.PlusOneTarget, Fabricator.PlusOneLerp); + plusOneProbability = Fabricator.CalculateBonusRollPercentage(skillLevel, bonusChance1); + + if (skillLevel >= Fabricator.PlusTwoQualityBonusThreshold) + { + var bonusChance2 = MathHelper.Lerp(targetLevel, Fabricator.PlusTwoTarget, Fabricator.PlusTwoLerp); + plusTwoProbability = Fabricator.CalculateBonusRollPercentage(skillLevel, bonusChance2); + } + } + + var result = new Fabricator.QualityResult(startingQuality, plusOneProbability, plusTwoProbability); + + // iterate to confirm that the percentage chance is correct + const int iterations = 100000; + var plusOneCount = 0; + var plusTwoCount = 0; + for (int i = 0; i < iterations; i++) + { + int quality = result.RollQuality(); + if (quality == startingQuality + 1) + { + plusOneCount++; + } + else if (quality == startingQuality + 2) + { + plusTwoCount++; + } + } + + var iteratedPlusOneChance = plusOneCount / (float)iterations * 100f; + var iteratedPlusTwoChance = plusTwoCount / (float)iterations * 100f; + + // check that the percentage chance is within 3% of the expected value + result.TotalPlusOnePercentage.Should().BeApproximately(iteratedPlusOneChance, 3f); + result.TotalPlusTwoPercentage.Should().BeApproximately(iteratedPlusTwoChance, 3f); + }).QuickCheckThrowOnFailure(); + } +} \ No newline at end of file From 7f295c291e8c73c8b808acb1f2628d04a4a4f75d Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Thu, 14 Dec 2023 16:12:10 +0200 Subject: [PATCH 19/53] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 872515cac..116e9e01d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.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.1.19.3 (Treacherous Tides Hotfix 2) - - v1.2.5.0 (Unstable) + - v1.2.6.0 (Winter Update) - Other validations: required: true From ada8d6f260892bbf0e43e2f3312968ef925006f6 Mon Sep 17 00:00:00 2001 From: Regalis11 Date: Wed, 20 Dec 2023 15:00:42 +0200 Subject: [PATCH 20/53] v1.2.7.0 (Winter Update hotfix) --- .../ClientSource/DebugConsole.cs | 2 +- .../ClientSource/Items/Item.cs | 2 +- .../BarotraumaClient/ClientSource/Map/Hull.cs | 1 + .../ClientSource/Map/Submarine.cs | 7 +++ .../ClientSource/Networking/GameClient.cs | 2 +- .../ClientSource/Particles/ParticleManager.cs | 13 ++++- .../CharacterEditor/CharacterEditorScreen.cs | 22 ++++----- .../ClientSource/Screens/SubEditorScreen.cs | 13 ++--- .../BarotraumaClient/LinuxClient.csproj | 2 +- Barotrauma/BarotraumaClient/MacClient.csproj | 2 +- .../BarotraumaClient/WindowsClient.csproj | 2 +- .../BarotraumaServer/LinuxServer.csproj | 2 +- Barotrauma/BarotraumaServer/MacServer.csproj | 2 +- .../BarotraumaServer/ServerSource/GameMain.cs | 2 +- .../BarotraumaServer/WindowsServer.csproj | 2 +- .../AI/Objectives/AIObjectiveGetItem.cs | 1 + .../AI/Objectives/AIObjectiveManager.cs | 4 ++ .../Characters/Animation/Ragdoll.cs | 28 ++++++----- .../Health/Afflictions/AfflictionPrefab.cs | 2 +- .../SharedSource/Characters/Jobs/Job.cs | 6 +-- .../SharedSource/Characters/Jobs/JobPrefab.cs | 12 ++++- .../AbilityConditionAttackData.cs | 15 ++++-- .../CharacterAbilityReplaceAffliction.cs | 47 +++++++++++++++++++ .../CharacterAbilityGroupInterval.cs | 5 +- .../SharedSource/DebugConsole.cs | 4 +- .../Events/EventActions/AfflictionAction.cs | 6 +-- .../Events/Missions/MissionPrefab.cs | 6 +-- .../GameAnalytics/GameAnalyticsConsent.cs | 8 ++-- .../GameSession/GameModes/CampaignMode.cs | 4 +- .../Items/Components/Machines/Fabricator.cs | 2 +- .../SharedSource/Map/ItemAssemblyPrefab.cs | 2 +- .../SharedSource/Map/Map/Location.cs | 8 ++-- .../SharedSource/Map/StructurePrefab.cs | 2 +- .../Networking/ClientPermissions.cs | 4 +- Barotrauma/BarotraumaShared/changelog.txt | 19 ++++++++ 35 files changed, 181 insertions(+), 80 deletions(-) create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityReplaceAffliction.cs diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index d08948b86..8df615ee8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -1392,7 +1392,7 @@ namespace Barotrauma { if (!(MapEntityPrefab.Find(null, deconstructItem.ItemIdentifier, showErrorMessages: false) is ItemPrefab targetItem)) { - ThrowError("Error in item \"" + itemPrefab.Name + "\" - could not find deconstruct item \"" + deconstructItem.ItemIdentifier + "\"!"); + ThrowErrorLocalized("Error in item \"" + itemPrefab.Name + "\" - could not find deconstruct item \"" + deconstructItem.ItemIdentifier + "\"!"); continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 1633cf7bc..6552ba373 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -536,7 +536,7 @@ namespace Barotrauma rotation: RotationRad, clr: Color.White, depth: 0, - thickness: 2f / Screen.Selected.Cam.Zoom); + thickness: Math.Max(2f / Screen.Selected.Cam.Zoom, 1)); foreach (Rectangle t in Prefab.Triggers) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index b4786982f..a3ab80804 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -218,6 +218,7 @@ namespace Barotrauma { if (primaryMouseButtonHeld) { + ShowHulls = true; hull.WaterVolume += 100000.0f * deltaTime; hull.networkUpdatePending = true; hull.serverUpdateDelay = 0.5f; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index 3d9e53dc6..9cf47521f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -499,6 +499,13 @@ namespace Barotrauma } } + if (Hull.HullList.Any(h => h.WaterVolume > 0.0f)) + { + errorMsgs.Add(TextManager.Get("WaterInHullsWarning").Value); + warnings.Add(SubEditorScreen.WarningType.WaterInHulls); + Hull.ShowHulls = true; + } + if (Info.Type == SubmarineType.Player) { foreach (Item item in Item.ItemList) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index c6d338147..c65b1da61 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -1597,7 +1597,7 @@ namespace Barotrauma.Networking } catch (Exception e) { - DebugConsole.ThrowError("There was an error initializing the round.", e, true); + DebugConsole.ThrowError("There was an error initializing the round.", e, createMessageBox: true); roundInitStatus = RoundInitStatus.Error; break; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs index 37c906798..457755dfa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs @@ -27,10 +27,9 @@ namespace Barotrauma.Particles get { return maxParticles; } set { - if (maxParticles == value || value < 4) return; + if (maxParticles == value || value < 4) { return; } Particle[] newParticles = new Particle[value]; - for (int i = 0; i < Math.Min(maxParticles, value); i++) { newParticles[i] = particles[i]; @@ -39,6 +38,16 @@ namespace Barotrauma.Particles particleCount = Math.Min(particleCount, value); particles = newParticles; maxParticles = value; + + var oldParticlesInCreationOrder = particlesInCreationOrder.ToList(); + particlesInCreationOrder.Clear(); + foreach (var particle in oldParticlesInCreationOrder) + { + if (particles.Contains(particle)) + { + particlesInCreationOrder.AddLast(particle); + } + } } } private Particle[] particles; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index f93163433..7dc80f765 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -1219,7 +1219,7 @@ namespace Barotrauma.CharacterEditor { if (RagdollParams.Joints.Any(j => j.Limb1 == fromLimb && j.Limb2 == toLimb)) { - DebugConsole.ThrowError(GetCharacterEditorTranslation("ExistingJointFound").Replace("[limbid1]", fromLimb.ToString()).Replace("[limbid2]", toLimb.ToString())); + DebugConsole.ThrowErrorLocalized(GetCharacterEditorTranslation("ExistingJointFound").Replace("[limbid1]", fromLimb.ToString()).Replace("[limbid2]", toLimb.ToString())); return; } if (RagdollParams.MainElement == null) @@ -1239,7 +1239,7 @@ namespace Barotrauma.CharacterEditor var lastJointElement = RagdollParams.MainElement.GetChildElements("joint").LastOrDefault() ?? RagdollParams.MainElement.GetChildElements("limb").LastOrDefault(); if (lastJointElement == null) { - DebugConsole.ThrowError(GetCharacterEditorTranslation("CantAddJointsNoLimbElements")); + DebugConsole.ThrowErrorLocalized(GetCharacterEditorTranslation("CantAddJointsNoLimbElements")); return; } lastJointElement.AddAfterSelf(newJointElement); @@ -1271,7 +1271,7 @@ namespace Barotrauma.CharacterEditor { if (character.IsHumanoid) { - DebugConsole.ThrowError(GetCharacterEditorTranslation("HumanoidLimbDeletionDisabled")); + DebugConsole.ThrowErrorLocalized(GetCharacterEditorTranslation("HumanoidLimbDeletionDisabled")); break; } var limb = selectedLimbs[i]; @@ -1675,7 +1675,7 @@ namespace Barotrauma.CharacterEditor if (contentPackage == null) { // This should not be possible. - DebugConsole.ThrowError(GetCharacterEditorTranslation("NoContentPackageSelected")); + DebugConsole.ThrowErrorLocalized(GetCharacterEditorTranslation("NoContentPackageSelected")); return false; } if (vanilla != null && contentPackage == vanilla) @@ -2898,7 +2898,7 @@ namespace Barotrauma.CharacterEditor } catch (Exception e) { - DebugConsole.ThrowError(GetCharacterEditorTranslation("CouldntOpenDirectory").Replace("[folder]", RagdollParams.Folder), e); + DebugConsole.ThrowErrorLocalized(GetCharacterEditorTranslation("CouldntOpenDirectory").Replace("[folder]", RagdollParams.Folder), e); } } PopulateListBox(); @@ -2932,7 +2932,7 @@ namespace Barotrauma.CharacterEditor } catch (Exception e) { - DebugConsole.ThrowError(TextManager.Get("DeleteFileError").Replace("[file]", selectedFile), e); + DebugConsole.ThrowErrorLocalized(TextManager.Get("DeleteFileError").Replace("[file]", selectedFile), e); } msgBox.Close(); listBox.ClearChildren(); @@ -3058,7 +3058,7 @@ namespace Barotrauma.CharacterEditor } catch (Exception e) { - DebugConsole.ThrowError(GetCharacterEditorTranslation("CouldntOpenDirectory").Replace("[folder]", CurrentAnimation.Folder), e); + DebugConsole.ThrowErrorLocalized(GetCharacterEditorTranslation("CouldntOpenDirectory").Replace("[folder]", CurrentAnimation.Folder), e); } } PopulateListBox(); @@ -3092,7 +3092,7 @@ namespace Barotrauma.CharacterEditor } catch (Exception e) { - DebugConsole.ThrowError(TextManager.GetWithVariable("DeleteFileError", "[file]", selectedFile), e); + DebugConsole.ThrowErrorLocalized(TextManager.GetWithVariable("DeleteFileError", "[file]", selectedFile), e); } msgBox.Close(); PopulateListBox(); @@ -3129,7 +3129,7 @@ namespace Barotrauma.CharacterEditor humanAnimController.SwimFastParams = HumanSwimFastParams.GetAnimParams(character, fileName); break; default: - DebugConsole.ThrowError(GetCharacterEditorTranslation("AnimationTypeNotImplemented").Replace("[type]", selectedType.ToString())); + DebugConsole.ThrowErrorLocalized(GetCharacterEditorTranslation("AnimationTypeNotImplemented").Replace("[type]", selectedType.ToString())); break; } } @@ -3150,7 +3150,7 @@ namespace Barotrauma.CharacterEditor character.AnimController.SwimFastParams = FishSwimFastParams.GetAnimParams(character, fileName); break; default: - DebugConsole.ThrowError(GetCharacterEditorTranslation("AnimationTypeNotImplemented").Replace("[type]", selectedType.ToString())); + DebugConsole.ThrowErrorLocalized(GetCharacterEditorTranslation("AnimationTypeNotImplemented").Replace("[type]", selectedType.ToString())); break; } } @@ -3548,7 +3548,7 @@ namespace Barotrauma.CharacterEditor } else { - DebugConsole.ThrowError(GetCharacterEditorTranslation("NoFieldForParameterFound").Replace("[parameter]", name.Value)); + DebugConsole.ThrowErrorLocalized(GetCharacterEditorTranslation("NoFieldForParameterFound").Replace("[parameter]", name.Value)); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index a9a34256c..768e1f4cc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -80,7 +80,8 @@ namespace Barotrauma WallCount, ItemCount, LightCount, - ShadowCastingLightCount + ShadowCastingLightCount, + WaterInHulls } public static Vector2 MouseDragStart = Vector2.Zero; @@ -1319,7 +1320,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError(TextManager.GetWithVariable("DeleteFileError", "[file]", assemblyPrefab.Name), e); + DebugConsole.ThrowErrorLocalized(TextManager.GetWithVariable("DeleteFileError", "[file]", assemblyPrefab.Name), e); } return true; }; @@ -1557,7 +1558,9 @@ namespace Barotrauma if (editorSelectedTime.TryUnwrap(out DateTime selectedTime)) { TimeSpan timeInEditor = DateTime.Now - selectedTime; - if (timeInEditor.TotalSeconds > Timing.TotalTime) + //this is intended for diagnosing why the "x hours in editor" achievement seems to sometimes trigger too soon + //require the time in editor to be x1.5 higher to disregard any rounding errors or discrepancies in Datetime.Now and the game's own timekeeping + if (timeInEditor.TotalSeconds > Timing.TotalTime * 1.5) { DebugConsole.ThrowErrorAndLogToGA( "SubEditorScreen.DeselectEditorSpecific:InvalidTimeInEditor", @@ -3711,7 +3714,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError(TextManager.GetWithVariable("DeleteFileError", "[file]", sub.FilePath), e); + DebugConsole.ThrowErrorLocalized(TextManager.GetWithVariable("DeleteFileError", "[file]", sub.FilePath), e); } return true; }; @@ -5199,9 +5202,7 @@ namespace Barotrauma SkipInventorySlotUpdate = false; ImageManager.Update((float)deltaTime); -#if DEBUG Hull.UpdateCheats((float)deltaTime, cam); -#endif if (GameMain.GraphicsWidth != screenResolution.X || GameMain.GraphicsHeight != screenResolution.Y) { diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index cba9e2ea5..d017a5cc0 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.2.6.0 + 1.2.7.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 8694f9367..33b14c6a6 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.2.6.0 + 1.2.7.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 0f7aacf16..9f8a6bb7f 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.2.6.0 + 1.2.7.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index e983630ac..5496b5a27 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.2.6.0 + 1.2.7.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 56fca5e74..667339bf3 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.2.6.0 + 1.2.7.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index 7b2abfa96..1be65ac9c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -120,7 +120,7 @@ namespace Barotrauma { if (Version < VanillaContent.GameVersion) { - DebugConsole.ThrowError( + DebugConsole.ThrowErrorLocalized( TextManager.GetWithVariables("versionmismatchwarning", ("[gameversion]", Version.ToString()), ("[contentversion]", VanillaContent.GameVersion.ToString()))); diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 0ece0e532..192792db2 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.2.6.0 + 1.2.7.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index 1ed1fe400..963be9a2b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -454,6 +454,7 @@ namespace Barotrauma if (!itemInventory.Container.HasRequiredItems(character, addMessage: false)) { continue; } } float itemPriority = item.Prefab.BotPriority; + if (itemPriority <= 0) { continue; } if (GetItemPriority != null) { itemPriority *= GetItemPriority(item); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index 42c31a65c..4b4433332 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -163,6 +163,10 @@ namespace Barotrauma continue; } } + if (autonomousObjective.IgnoreAtNonOutpost && !Level.IsLoadedFriendlyOutpost) + { + continue; + } var objective = CreateObjective(order, autonomousObjective.PriorityModifier); if (objective != null && objective.CanBeCompleted) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index ffebedae2..f9d751411 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -740,28 +740,32 @@ namespace Barotrauma { Stairs = character.SelectedBy.AnimController.Stairs; } + + var collisionResponse = getStairCollisionResponse(); + if (collisionResponse == LimbStairCollisionResponse.ClimbWithLimbCollision) + { + Stairs = structure; + } else { - var collisionResponse = handleLimbStairCollision(); - if (collisionResponse == LimbStairCollisionResponse.ClimbWithLimbCollision) - { - Stairs = structure; - } - else - { - if (collisionResponse == LimbStairCollisionResponse.DontClimbStairs) { Stairs = null; } + if (collisionResponse == LimbStairCollisionResponse.DontClimbStairs) { Stairs = null; } - return false; - } - } + return false; + } - LimbStairCollisionResponse handleLimbStairCollision() + LimbStairCollisionResponse getStairCollisionResponse() { //don't collide with stairs if //1. bottom of the collider is at the bottom of the stairs and the character isn't trying to move upwards float stairBottomPos = ConvertUnits.ToSimUnits(structure.Rect.Y - structure.Rect.Height + 10); if (colliderBottom.Y < stairBottomPos && targetMovement.Y < 0.5f) { return LimbStairCollisionResponse.DontClimbStairs; } + if (character.SelectedBy != null && + character.SelectedBy.AnimController.GetColliderBottom().Y < stairBottomPos && + character.SelectedBy.AnimController.targetMovement.Y < 0.5f) + { + return LimbStairCollisionResponse.DontClimbStairs; + } //2. bottom of the collider is at the top of the stairs and the character isn't trying to move downwards if (targetMovement.Y >= 0.0f && colliderBottom.Y >= ConvertUnits.ToSimUnits(structure.Rect.Y - Submarine.GridSize.Y * 5)) { return LimbStairCollisionResponse.DontClimbStairs; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 035a8c9ae..2e760b73d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -904,7 +904,7 @@ namespace Barotrauma string indicatorLimbName = element.GetAttributeString("indicatorlimb", "Torso"); if (!Enum.TryParse(indicatorLimbName, out IndicatorLimb)) { - DebugConsole.ThrowError("Error in affliction prefab " + Name + " - limb type \"" + indicatorLimbName + "\" not found."); + DebugConsole.ThrowErrorLocalized("Error in affliction prefab " + Name + " - limb type \"" + indicatorLimbName + "\" not found."); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs index 411ff78b0..2dff4a2f4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs @@ -150,11 +150,11 @@ namespace Barotrauma if (itemElement.Attribute("name") != null) { string itemName = itemElement.Attribute("name").Value; - DebugConsole.ThrowError("Error in Job config (" + Name + ") - use item identifiers instead of names to configure the items."); + DebugConsole.ThrowErrorLocalized("Error in Job config (" + Name + ") - use item identifiers instead of names to configure the items."); itemPrefab = MapEntityPrefab.FindByName(itemName) as ItemPrefab; if (itemPrefab == null) { - DebugConsole.ThrowError("Tried to spawn \"" + Name + "\" with the item \"" + itemName + "\". Matching item prefab not found."); + DebugConsole.ThrowErrorLocalized("Tried to spawn \"" + Name + "\" with the item \"" + itemName + "\". Matching item prefab not found."); return; } } @@ -164,7 +164,7 @@ namespace Barotrauma itemPrefab = MapEntityPrefab.FindByIdentifier(itemIdentifier.ToIdentifier()) as ItemPrefab; if (itemPrefab == null) { - DebugConsole.ThrowError("Tried to spawn \"" + Name + "\" with the item \"" + itemIdentifier + "\". Matching item prefab not found."); + DebugConsole.ThrowErrorLocalized("Tried to spawn \"" + Name + "\" with the item \"" + itemIdentifier + "\". Matching item prefab not found."); return; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs index e93e38566..a01454ba2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs @@ -13,7 +13,14 @@ namespace Barotrauma public readonly Identifier Identifier; public readonly Identifier Option; public readonly float PriorityModifier; + /// + /// The order is ignored in outpost levels. Doesn't apply to outpost NPCs. + /// public readonly bool IgnoreAtOutpost; + /// + /// The order is ignored in "normal" non-outpost levels + /// + public readonly bool IgnoreAtNonOutpost; public AutonomousObjective(XElement element) { @@ -29,6 +36,7 @@ namespace Barotrauma PriorityModifier = element.GetAttributeFloat("prioritymodifier", 1); PriorityModifier = MathHelper.Max(PriorityModifier, 0); IgnoreAtOutpost = element.GetAttributeBool("ignoreatoutpost", false); + IgnoreAtNonOutpost = element.GetAttributeBool("ignoreatnonoutpost", false); } } @@ -244,14 +252,14 @@ namespace Barotrauma { if (itemElement.Element("name") != null) { - DebugConsole.ThrowError("Error in job config \"" + Name + "\" - use identifiers instead of names to configure the items."); + DebugConsole.ThrowErrorLocalized("Error in job config \"" + Name + "\" - use identifiers instead of names to configure the items."); continue; } Identifier itemIdentifier = itemElement.GetAttributeIdentifier("identifier", Identifier.Empty); if (itemIdentifier.IsEmpty) { - DebugConsole.ThrowError("Error in job config \"" + Name + "\" - item with no identifier."); + DebugConsole.ThrowErrorLocalized("Error in job config \"" + Name + "\" - item with no identifier."); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs index 905adea68..37faf6a24 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs @@ -1,8 +1,7 @@ -using System; using Barotrauma.Items.Components; +using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Abilities { @@ -25,11 +24,15 @@ namespace Barotrauma.Abilities private readonly Identifier[] tags; private readonly WeaponType weapontype; private readonly bool ignoreNonHarmfulAttacks; + + private readonly bool ignoreOwnAttacks; + public AbilityConditionAttackData(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { - itemIdentifier = conditionElement.GetAttributeString("itemidentifier", string.Empty); - tags = conditionElement.GetAttributeIdentifierArray("tags", Array.Empty()); - ignoreNonHarmfulAttacks = conditionElement.GetAttributeBool("ignorenonharmfulattacks", false); + itemIdentifier = conditionElement.GetAttributeString(nameof(itemIdentifier), string.Empty); + tags = conditionElement.GetAttributeIdentifierArray(nameof(tags), Array.Empty()); + ignoreNonHarmfulAttacks = conditionElement.GetAttributeBool(nameof(ignoreNonHarmfulAttacks), false); + ignoreOwnAttacks = conditionElement.GetAttributeBool(nameof(ignoreOwnAttacks), false); string weaponTypeStr = conditionElement.GetAttributeString("weapontype", "Any"); if (!Enum.TryParse(weaponTypeStr, ignoreCase: true, out weapontype)) @@ -43,6 +46,8 @@ namespace Barotrauma.Abilities { if (abilityObject is AbilityAttackData attackData) { + if (ignoreOwnAttacks && attackData.Attacker == character) { return false; } + if (ignoreNonHarmfulAttacks && attackData.SourceAttack != null) { if (attackData.SourceAttack.Stun <= 0.0f && (attackData.SourceAttack.Afflictions?.All(a => a.Key.Prefab.IsBuff) ?? true)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityReplaceAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityReplaceAffliction.cs new file mode 100644 index 000000000..180319723 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityReplaceAffliction.cs @@ -0,0 +1,47 @@ +#nullable enable + +namespace Barotrauma.Abilities +{ + internal sealed class CharacterAbilityReplaceAffliction : CharacterAbility + { + private readonly Identifier afflictionId; + private readonly Identifier newAfflictionId; + private readonly float strengthMultiplier; + + public CharacterAbilityReplaceAffliction(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + afflictionId = abilityElement.GetAttributeIdentifier("afflictionid", abilityElement.GetAttributeIdentifier("affliction", Identifier.Empty)); + newAfflictionId = abilityElement.GetAttributeIdentifier("newafflictionid", abilityElement.GetAttributeIdentifier("newaffliction", Identifier.Empty)); + + strengthMultiplier = abilityElement.GetAttributeFloat("strengthmultiplier", 1.0f); + + if (afflictionId.IsEmpty) + { + DebugConsole.ThrowError($"Error in {nameof(CharacterAbilityReplaceAffliction)} - affliction identifier not set."); + } + } + + protected override void ApplyEffect() + { + var affliction = Character.CharacterHealth.GetAffliction(afflictionId); + if (affliction != null) + { + float afflictionStrength = affliction.Strength; + Limb limb = Character.CharacterHealth.GetAfflictionLimb(affliction); + Character.CharacterHealth.ReduceAfflictionOnAllLimbs(affliction.Identifier, afflictionStrength); + if (!newAfflictionId.IsEmpty && AfflictionPrefab.Prefabs.TryGet(newAfflictionId, out var newAfflictionPrefab)) + { + Character.CharacterHealth.ApplyAffliction(targetLimb: limb, newAfflictionPrefab.Instantiate(afflictionStrength * strengthMultiplier)); + } + } + } + + protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) + { + if (conditionsMatched) + { + ApplyEffect(); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs index cf1100db6..18855af2e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs @@ -1,14 +1,13 @@ using System.Collections.Generic; -using System.Linq; namespace Barotrauma.Abilities { class CharacterAbilityGroupInterval : CharacterAbilityGroup { - private float interval { get; set; } + private readonly float interval; public float TimeSinceLastUpdate { get; private set; } - private float effectDelay; + private readonly float effectDelay; private float effectDelayTimer; diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index ae30a5412..0805e53d6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -2561,9 +2561,9 @@ namespace Barotrauma } } - public static void ThrowError(LocalizedString error, Exception e = null, bool createMessageBox = false, bool appendStackTrace = false) + public static void ThrowErrorLocalized(LocalizedString error, Exception e = null, ContentPackage contentPackage = null, bool createMessageBox = false, bool appendStackTrace = false) { - ThrowError(error.Value, e, createMessageBox, appendStackTrace); + ThrowError(error.Value, e, contentPackage, createMessageBox, appendStackTrace); } public static void ThrowError(string error, Exception e = null, ContentPackage contentPackage = null, bool createMessageBox = false, bool appendStackTrace = false) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/AfflictionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/AfflictionAction.cs index c6792de8d..a5b62d869 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/AfflictionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/AfflictionAction.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { @@ -36,8 +33,7 @@ namespace Barotrauma public override void Update(float deltaTime) { if (isFinished) { return; } - var afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(p => p.Identifier == Affliction); - if (afflictionPrefab != null) + if (AfflictionPrefab.Prefabs.TryGet(Affliction, out var afflictionPrefab)) { var targets = ParentEvent.GetTargets(TargetTag); foreach (var target in targets) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs index 4df37a741..064527a38 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -364,12 +364,12 @@ namespace Barotrauma if (!Enum.TryParse(missionTypeName.Value, true, out Type)) { - DebugConsole.ThrowError("Error in mission prefab \"" + Name + "\" - \"" + missionTypeName + "\" is not a valid mission type."); + DebugConsole.ThrowErrorLocalized("Error in mission prefab \"" + Name + "\" - \"" + missionTypeName + "\" is not a valid mission type."); return; } if (Type == MissionType.None) { - DebugConsole.ThrowError("Error in mission prefab \"" + Name + "\" - mission type cannot be none."); + DebugConsole.ThrowErrorLocalized("Error in mission prefab \"" + Name + "\" - mission type cannot be none."); return; } @@ -383,7 +383,7 @@ namespace Barotrauma } else { - DebugConsole.ThrowError("Error in mission prefab \"" + Name + "\" - unsupported mission type \"" + Type.ToString() + "\""); + DebugConsole.ThrowErrorLocalized("Error in mission prefab \"" + Name + "\" - unsupported mission type \"" + Type.ToString() + "\""); } if (constructor == null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs index b87dd7027..a6ba4d40d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs @@ -269,7 +269,7 @@ namespace Barotrauma { if (response.ErrorException != null) { - DebugConsole.ThrowError(TextManager.GetWithVariable("MasterServerErrorException", "[error]", response.ErrorException.ToString())); + DebugConsole.ThrowErrorLocalized(TextManager.GetWithVariable("MasterServerErrorException", "[error]", response.ErrorException.ToString())); return false; } @@ -278,13 +278,13 @@ namespace Barotrauma switch (response.StatusCode) { case HttpStatusCode.NotFound: - DebugConsole.ThrowError(TextManager.GetWithVariable("MasterServerError404", "[masterserverurl]", consentServerUrl)); + DebugConsole.ThrowErrorLocalized(TextManager.GetWithVariable("MasterServerError404", "[masterserverurl]", consentServerUrl)); break; case HttpStatusCode.ServiceUnavailable: - DebugConsole.ThrowError(TextManager.Get("MasterServerErrorUnavailable")); + DebugConsole.ThrowErrorLocalized(TextManager.Get("MasterServerErrorUnavailable")); break; default: - DebugConsole.ThrowError(TextManager.GetWithVariables("MasterServerErrorDefault", + DebugConsole.ThrowErrorLocalized(TextManager.GetWithVariables("MasterServerErrorDefault", ("[statuscode]", response.StatusCode.ToString()), ("[statusdescription]", response.StatusDescription))); break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index e12456b62..374768a3b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -557,7 +557,7 @@ namespace Barotrauma if (availableTransition == TransitionType.None) { - DebugConsole.ThrowError("Failed to load a new campaign level. No available level transitions " + + DebugConsole.ThrowErrorLocalized("Failed to load a new campaign level. No available level transitions " + "(current location: " + (map.CurrentLocation?.DisplayName ?? "null") + ", " + "selected location: " + (map.SelectedLocation?.DisplayName ?? "null") + ", " + "leaving sub: " + (leavingSub?.Info?.Name ?? "null") + ", " + @@ -568,7 +568,7 @@ namespace Barotrauma } if (nextLevel == null) { - DebugConsole.ThrowError("Failed to load a new campaign level. No available level transitions " + + DebugConsole.ThrowErrorLocalized("Failed to load a new campaign level. No available level transitions " + "(transition type: " + availableTransition + ", " + "current location: " + (map.CurrentLocation?.DisplayName ?? "null") + ", " + "selected location: " + (map.SelectedLocation?.DisplayName ?? "null") + ", " + diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index 63de9cce6..4331f4199 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -170,7 +170,7 @@ namespace Barotrauma.Items.Components { if (recipe.RequiredItems.Length > inputContainer.Capacity) { - DebugConsole.ThrowError("Error in item \"" + item.Name + "\": There's not enough room in the input inventory for the ingredients of \"" + recipe.TargetItem.Name + "\"!"); + DebugConsole.ThrowErrorLocalized("Error in item \"" + item.Name + "\": There's not enough room in the input inventory for the ingredients of \"" + recipe.TargetItem.Name + "\"!"); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs index 706996a5d..9746cb9ae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs @@ -176,7 +176,7 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError("Deleting item assembly \"" + Name + "\" failed.", e); + DebugConsole.ThrowErrorLocalized("Deleting item assembly \"" + Name + "\" failed.", e); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index 552b93ade..7a0ef7607 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -570,9 +570,9 @@ namespace Barotrauma if (nameIdentifier.IsEmpty) { //backwards compatibility - rawName = element.GetAttributeString("basename", ""); - nameIdentifier = rawName.ToIdentifier(); DisplayName = element.GetAttributeString("name", ""); + rawName = element.GetAttributeString("rawname", element.GetAttributeString("basename", DisplayName.Value)); + nameIdentifier = rawName.ToIdentifier(); } else { @@ -1088,12 +1088,12 @@ namespace Barotrauma { if (!Type.HasHireableCharacters) { - DebugConsole.ThrowError("Cannot hire a character from location \"" + DisplayName + "\" - the location has no hireable characters.\n" + Environment.StackTrace.CleanupStackTrace()); + DebugConsole.ThrowErrorLocalized("Cannot hire a character from location \"" + DisplayName + "\" - the location has no hireable characters.\n" + Environment.StackTrace.CleanupStackTrace()); return; } if (HireManager == null) { - DebugConsole.ThrowError("Cannot hire a character from location \"" + DisplayName + "\" - hire manager has not been instantiated.\n" + Environment.StackTrace.CleanupStackTrace()); + DebugConsole.ThrowErrorLocalized("Cannot hire a character from location \"" + DisplayName + "\" - hire manager has not been instantiated.\n" + Environment.StackTrace.CleanupStackTrace()); return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs index a089421ab..8bdbea685 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs @@ -185,7 +185,7 @@ namespace Barotrauma if (subElement.GetAttribute("sourcerect") == null && subElement.GetAttribute("sheetindex") == null) { - DebugConsole.ThrowError("Warning - sprite sourcerect not configured for structure \"" + Name + "\"!"); + DebugConsole.ThrowErrorLocalized("Warning - sprite sourcerect not configured for structure \"" + Name + "\"!"); } #if CLIENT if (subElement.GetAttributeBool("fliphorizontal", false)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs index b17701a49..d8f76a414 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs @@ -51,7 +51,7 @@ namespace Barotrauma.Networking string permissionsStr = element.GetAttributeString("permissions", ""); if (!Enum.TryParse(permissionsStr, out Permissions)) { - DebugConsole.ThrowError("Error in permission preset \"" + DisplayName + "\" - " + permissionsStr + " is not a valid permission!"); + DebugConsole.ThrowErrorLocalized("Error in permission preset \"" + DisplayName + "\" - " + permissionsStr + " is not a valid permission!"); } PermittedCommands = new HashSet(); @@ -66,7 +66,7 @@ namespace Barotrauma.Networking if (command == null) { #if SERVER - DebugConsole.ThrowError("Error in permission preset \"" + DisplayName + "\" - " + commandName + "\" is not a valid console command."); + DebugConsole.ThrowErrorLocalized("Error in permission preset \"" + DisplayName + "\" - " + commandName + "\" is not a valid console command."); #endif continue; } diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 73a4ca7f7..a64e7dc57 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,22 @@ +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.2.7.0 +------------------------------------------------------------------------------------------------------------------------------------------------- + +- Fixed an issue that sometimes caused the game to crash without generating a crash report (which would lead to a "pipe was broken" error if the issue happened server-side). The issue had to do with a stack overflow when throwing specific kinds of console errors: one common case was when the game failed to connect to our server to check the GameAnalytics consent, which lead to a crash on startup. +- Fixed piezo crystals sometimes spawning right at the start of the level. +- Fixed assistants not being able to pick all tier 1 and 2 talents. +- Fixed location names disappearing when locations change their type in saves started in pre-1.2 versions. +- Fixed selection rectangle disappearing from items when zoomed in by more than x2 in the sub editor. +- Fixed "killcrawlerswarmlarge2" mission not having a description. +- Fixed bots sometimes choosing to wear broken diving suits. +- Fixed currently visible particles freezing when lowering the particle limit. +- Fixed dragged characters always colliding with stairs, making it impossible to drag them past the stairs. +- Fixed "drunken sailor" talent not nullifying the negative effects of drunkenness. +- Fixed stun from the "lightning wizard" talent activating if you cause damage to yourself (e.g. by breaking a wall and taking damage from the shrapnel). +- Fixed escorted security officers sometimes inspecting the crew for stolen items during escort missions. +- Fixed basics tutorial sometimes getting stuck in the "weld leak" objective. +- Fixed ancient weapon not flipping horizontally when aiming it to the left. + ------------------------------------------------------------------------------------------------------------------------------------------------- v1.2.6.0 ------------------------------------------------------------------------------------------------------------------------------------------------- From 8ea2b47889ede027c2214ce95f4f0d26b67ee450 Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Wed, 20 Dec 2023 15:03:02 +0200 Subject: [PATCH 21/53] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 116e9e01d..6c0e60891 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -73,7 +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.2.6.0 (Winter Update) + - v1.2.7.0 (Winter Update hotfix) - Other validations: required: true From 0c433eb187b20f4bd24a1919b478529b387ee0bf Mon Sep 17 00:00:00 2001 From: Regalis11 Date: Thu, 11 Jan 2024 16:19:43 +0200 Subject: [PATCH 22/53] v1.2.8.0 (Winter Update hotfix 2) --- .../Items/Components/Machines/MiniMap.cs | 21 ++- .../ClientSource/Map/ItemAssemblyPrefab.cs | 6 +- .../ClientSource/Map/Structure.cs | 94 +++++++++++-- .../ClientSource/Map/SubmarinePreview.cs | 15 +- .../Serialization/SerializableEntityEditor.cs | 34 +++-- .../ClientSource/Sprite/Sprite.cs | 130 ++++++------------ .../BarotraumaClient/LinuxClient.csproj | 2 +- Barotrauma/BarotraumaClient/MacClient.csproj | 2 +- .../BarotraumaClient/WindowsClient.csproj | 2 +- .../BarotraumaServer/LinuxServer.csproj | 2 +- Barotrauma/BarotraumaServer/MacServer.csproj | 2 +- .../BarotraumaServer/WindowsServer.csproj | 2 +- .../AI/Objectives/AIObjectiveFindSafety.cs | 6 +- .../Health/Afflictions/AfflictionPrefab.cs | 5 +- .../SharedSource/Items/Item.cs | 1 + .../SharedSource/Items/ItemPrefab.cs | 2 +- .../SharedSource/Map/Structure.cs | 30 +++- .../Editable/ConditionallyEditable.cs | 24 +++- Barotrauma/BarotraumaShared/changelog.txt | 18 +++ 19 files changed, 262 insertions(+), 136 deletions(-) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index b81560ca1..909becc9a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -1421,10 +1421,27 @@ namespace Barotrauma.Items.Components { Sprite sprite = structure.Sprite; if (sprite is null) { return; } + + Vector2 textureOffset = structure.TextureOffset; + textureOffset = new Vector2( + MathUtils.PositiveModulo(-textureOffset.X, sprite.SourceRect.Width * structure.TextureScale.X * structure.Scale), + MathUtils.PositiveModulo(-textureOffset.Y, sprite.SourceRect.Height * structure.TextureScale.Y * structure.Scale)); RectangleF entityRect = ScaleRectToUI(structure, parent, border); - Vector2 spriteScale = new Vector2(entityRect.Size.X / sprite.size.X, entityRect.Size.Y / sprite.size.Y); - sprite.Draw(spriteBatch, new Vector2(entityRect.Location.X + inflate, entityRect.Location.Y + inflate), structure.SpriteColor, Vector2.Zero, 0f, spriteScale, sprite.effects ^ structure.SpriteEffects); + Vector2 spriteScale = new Vector2(entityRect.Size.X / structure.Rect.Width, entityRect.Size.Y / structure.Rect.Height); + float rotation = MathHelper.ToRadians(structure.Rotation); + + sprite.DrawTiled( + spriteBatch: spriteBatch, + position: entityRect.Location + entityRect.Size * 0.5f + (inflate, inflate), + targetSize: entityRect.Size, + rotation: rotation, + origin: entityRect.Size * 0.5f, + color: structure.SpriteColor, + startOffset: textureOffset * spriteScale, + textureScale: structure.TextureScale * structure.Scale * spriteScale, + depth: structure.SpriteDepth, + spriteEffects: sprite.effects ^ structure.SpriteEffects); } private static RectangleF ScaleRectToUI(MapEntity entity, RectangleF parentRect, RectangleF worldBorders) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs index 52ba077ff..9f9c06073 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs @@ -86,7 +86,7 @@ namespace Barotrauma MathUtils.RoundTowardsClosest(center.Y, Submarine.GridSize.Y) - center.Y - Submarine.GridSize.Y / 2); MapEntity.SelectedList.Clear(); - entities.ForEach(e => MapEntity.AddSelection(e)); + assemblyEntities.ForEach(e => MapEntity.AddSelection(e)); foreach (MapEntity mapEntity in assemblyEntities) { @@ -99,6 +99,10 @@ namespace Barotrauma } } + //restore the previous selection + MapEntity.SelectedList.Clear(); + entities.ForEach(e => MapEntity.AddSelection(e)); + return element; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index 4fe5ce8e0..b14536cc1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -51,6 +51,72 @@ namespace Barotrauma UpdateSpriteStates(0.0f); } + public static Vector2 UpgradeTextureOffset( + Vector2 targetSize, + Vector2 originalTextureOffset, + SubmarineInfo submarineInfo, + Rectangle sourceRect, + Vector2 scale, + bool flippedX, + bool flippedY) + { + if (submarineInfo.GameVersion <= Sprite.LastBrokenTiledSpriteGameVersion) + { + // Tiled sprite rendering was significantly changed after v1.2.3.0: + // Rendering flipped, scaled and offset textures was completely broken, + // but some existing community submarines depend on that old behavior, + // so let's redo some of the broken logic here if the sub is old enough + + Vector2 flipper = (flippedX ? -1f : 1f, flippedY ? -1f : 1f); + + var textureOffset = originalTextureOffset * flipper; + + textureOffset = new Vector2( + MathUtils.PositiveModulo((int)-textureOffset.X, sourceRect.Width), + MathUtils.PositiveModulo((int)-textureOffset.Y, sourceRect.Height)); + + textureOffset.X = (textureOffset.X / scale.X) % sourceRect.Width; + textureOffset.Y = (textureOffset.Y / scale.Y) % sourceRect.Height; + + Vector2 flippedDrawOffset = Vector2.Zero; + if (flippedX) + { + float diff = targetSize.X % (sourceRect.Width * scale.X); + flippedDrawOffset.X = (sourceRect.Width * scale.X - diff) / scale.X; + flippedDrawOffset.X = + MathUtils.NearlyEqual(flippedDrawOffset.X, MathF.Round(flippedDrawOffset.X)) ? + MathF.Round(flippedDrawOffset.X) : flippedDrawOffset.X; + } + if (flippedY) + { + float diff = targetSize.Y % (sourceRect.Height * scale.Y); + flippedDrawOffset.Y = (sourceRect.Height * scale.Y - diff) / scale.Y; + flippedDrawOffset.Y = + MathUtils.NearlyEqual(flippedDrawOffset.Y, MathF.Round(flippedDrawOffset.Y)) ? + MathF.Round(flippedDrawOffset.Y) : flippedDrawOffset.Y; + } + + var textureOffsetPlusFlipBs = textureOffset + flippedDrawOffset; + + if (textureOffsetPlusFlipBs.X > sourceRect.Width) + { + var diff = textureOffsetPlusFlipBs.X - sourceRect.Width; + textureOffset.X = (textureOffset.X + diff * (scale.X - 1f)) % sourceRect.Width; + } + if (textureOffsetPlusFlipBs.Y > sourceRect.Height) + { + var diff = textureOffsetPlusFlipBs.Y - sourceRect.Height; + textureOffset.Y = (textureOffset.Y + diff * (scale.Y - 1f)) % sourceRect.Height; + } + + textureOffset *= scale * flipper; + + return -textureOffset; + } + + return originalTextureOffset; + } + partial void CreateConvexHull(Vector2 position, Vector2 size, float rotation) { if (!CastShadow) { return; } @@ -112,8 +178,8 @@ namespace Barotrauma foreach (LightSource light in Lights) { Vector2 bgOffset = new Vector2( - MathUtils.PositiveModulo((int)-textOffset.X, light.texture.Width), - MathUtils.PositiveModulo((int)-textOffset.Y, light.texture.Height)); + MathUtils.PositiveModulo(-textOffset.X, light.texture.Width), + MathUtils.PositiveModulo(-textOffset.Y, light.texture.Height)); light.LightTextureOffset = bgOffset; } @@ -128,6 +194,16 @@ namespace Barotrauma CanTakeKeyBoardFocus = false }; var editor = new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true, titleFont: GUIStyle.LargeFont) { UserData = this }; + + if (editor.Fields.TryGetValue(nameof(Scale).ToIdentifier(), out GUIComponent[] scaleFields) && + scaleFields.FirstOrDefault() is GUINumberInput scaleInput) + { + //texture offset needs to be adjusted when scaling the entity to keep the look of the entity unchanged + scaleInput.OnValueChanged += (GUINumberInput numberInput) => + { + TextureOffset *= (Scale / ScaleWhenTextureOffsetSet); + }; + } if (Submarine.MainSub?.Info?.Type == SubmarineType.OutpostModule) { @@ -334,8 +410,6 @@ namespace Barotrauma float depth = GetDrawDepth(); Vector2 textureOffset = this.textureOffset; - if (FlippedX) { textureOffset.X = -textureOffset.X; } - if (FlippedY) { textureOffset.Y = -textureOffset.Y; } if (back && damageEffect == null && !isWiringMode) { @@ -365,8 +439,8 @@ namespace Barotrauma Prefab.BackgroundSprite.effects ^= SpriteEffects; Vector2 backGroundOffset = new Vector2( - MathUtils.PositiveModulo((int)-textureOffset.X, Prefab.BackgroundSprite.SourceRect.Width), - MathUtils.PositiveModulo((int)-textureOffset.Y, Prefab.BackgroundSprite.SourceRect.Height)); + MathUtils.PositiveModulo(-textureOffset.X, Prefab.BackgroundSprite.SourceRect.Width * TextureScale.X * Scale), + MathUtils.PositiveModulo(-textureOffset.Y, Prefab.BackgroundSprite.SourceRect.Height * TextureScale.Y * Scale)); Prefab.BackgroundSprite.DrawTiled( spriteBatch, @@ -442,11 +516,11 @@ namespace Barotrauma Math.Abs(rect.Location.X - drawSection.Location.X), Math.Abs(rect.Location.Y - drawSection.Location.Y)); - if (FlippedX && IsHorizontal) { sectionOffset.X = drawSection.Right - rect.Right; } - if (FlippedY && !IsHorizontal) { sectionOffset.Y = (rect.Y - rect.Height) - (drawSection.Y - drawSection.Height); } + if (FlippedX && IsHorizontal) { sectionOffset.X = rect.Right - drawSection.Right; } + if (FlippedY && !IsHorizontal) { sectionOffset.Y = (drawSection.Y - drawSection.Height) - (rect.Y - rect.Height); } - sectionOffset.X += MathUtils.PositiveModulo((int)-textureOffset.X, Prefab.Sprite.SourceRect.Width); - sectionOffset.Y += MathUtils.PositiveModulo((int)-textureOffset.Y, Prefab.Sprite.SourceRect.Height); + sectionOffset.X += MathUtils.PositiveModulo(-textureOffset.X, Prefab.Sprite.SourceRect.Width * TextureScale.X * Scale); + sectionOffset.Y += MathUtils.PositiveModulo(-textureOffset.Y, Prefab.Sprite.SourceRect.Height * TextureScale.Y * Scale); Vector2 pos = new Vector2(drawSection.X, drawSection.Y); pos -= rect.Location.ToVector2(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs index 24c77837e..f544b6dc3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs @@ -477,12 +477,19 @@ namespace Barotrauma Vector2 backGroundOffset = Vector2.Zero; Vector2 textureOffset = element.GetAttributeVector2("textureoffset", Vector2.Zero); - if (flippedX) { textureOffset.X = -textureOffset.X; } - if (flippedY) { textureOffset.Y = -textureOffset.Y; } + + textureOffset = Structure.UpgradeTextureOffset( + targetSize: rect.Size.ToVector2(), + originalTextureOffset: textureOffset, + submarineInfo: submarineInfo, + sourceRect: prefab.Sprite.SourceRect, + scale: textureScale * scale, + flippedX: flippedX, + flippedY: flippedY); backGroundOffset = new Vector2( - MathUtils.PositiveModulo((int)-textureOffset.X, prefab.Sprite.SourceRect.Width), - MathUtils.PositiveModulo((int)-textureOffset.Y, prefab.Sprite.SourceRect.Height)); + MathUtils.PositiveModulo(-textureOffset.X, prefab.Sprite.SourceRect.Width * textureScale.X * scale), + MathUtils.PositiveModulo(-textureOffset.Y, prefab.Sprite.SourceRect.Height * textureScale.Y * scale)); prefab.Sprite.DrawTiled( spriteBatch: spriteRecorder, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index 38f110c95..4d9def9a6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -583,12 +583,23 @@ namespace Barotrauma } }; + HandleSetterValueTampering(numberInput, () => property.GetFloatValue(entity)); + refresh += () => + { + if (!numberInput.TextBox.Selected) { numberInput.FloatValue = (float)property.GetValue(entity); } + }; + if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name.ToIdentifier(), new GUIComponent[] { numberInput }); } + return frame; + } + + private static void HandleSetterValueTampering(GUINumberInput numberInput, Func getter) + { // Lots of UI boilerplate to handle all(?) cases where the property's setter may be called // and modify the input value (e.g. rotation value wrapping) void HandleSetterModifyingInput(GUINumberInput numInput) { var inputFloatValue = numInput.FloatValue; - var resultingFloatValue = property.GetFloatValue(entity); + var resultingFloatValue = getter(); if (!MathUtils.NearlyEqual(resultingFloatValue, inputFloatValue)) { numInput.FloatValue = resultingFloatValue; @@ -602,12 +613,6 @@ namespace Barotrauma numberInput.PlusButton.OnClicked += HandleSetterModifyingInputOnButtonClicked; numberInput.MinusButton.OnPressed += HandleSetterModifyingInputOnButtonPressed; numberInput.MinusButton.OnClicked += HandleSetterModifyingInputOnButtonClicked; - refresh += () => - { - if (!numberInput.TextBox.Selected) { numberInput.FloatValue = (float)property.GetValue(entity); } - }; - if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name.ToIdentifier(), new GUIComponent[] { numberInput }); } - return frame; } public GUIComponent CreateEnumField(ISerializableEntity entity, SerializableProperty property, object value, LocalizedString displayName, LocalizedString toolTip) @@ -881,26 +886,33 @@ namespace Barotrauma numberInput.MaxValueFloat = editableAttribute.MaxValueFloat; numberInput.DecimalsToDisplay = editableAttribute.DecimalCount; numberInput.ValueStep = editableAttribute.ValueStep; + numberInput.ForceShowPlusMinusButtons = editableAttribute.ForceShowPlusMinusButtons; - if (i == 0) - numberInput.FloatValue = value.X; - else - numberInput.FloatValue = value.Y; + numberInput.FloatValue = i == 0 ? value.X : value.Y; int comp = i; numberInput.OnValueChanged += (numInput) => { Vector2 newVal = (Vector2)property.GetValue(entity); if (comp == 0) + { newVal.X = numInput.FloatValue; + } else + { newVal.Y = numInput.FloatValue; + } if (SetPropertyValue(property, entity, newVal)) { TrySendNetworkUpdate(entity, property); } }; + HandleSetterValueTampering(numberInput, () => + { + Vector2 currVal = (Vector2)property.GetValue(entity); + return comp == 0 ? currVal.X : currVal.Y; + }); fields[i] = numberInput; } refresh += () => diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs index 5314247ec..94f4d5547 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs @@ -284,6 +284,11 @@ namespace Barotrauma } } + /// + /// Last version of the game that had broken handling of sprites that were scaled, flipped and offset + /// + public static readonly Version LastBrokenTiledSpriteGameVersion = new Version(major: 1, minor: 2, build: 7, revision: 0); + public void DrawTiled(ISpriteBatch spriteBatch, Vector2 position, Vector2 targetSize, @@ -298,8 +303,9 @@ namespace Barotrauma if (Texture == null) { return; } spriteEffects ??= effects; - bool flipHorizontal = (spriteEffects.Value & SpriteEffects.FlipHorizontally) != 0; - bool flipVertical = (spriteEffects.Value & SpriteEffects.FlipVertically) != 0; + + bool flipHorizontal = spriteEffects.Value.HasFlag(SpriteEffects.FlipHorizontally); + bool flipVertical = spriteEffects.Value.HasFlag(SpriteEffects.FlipVertically); float addedRotation = rotation + this.rotation; if (flipHorizontal != flipVertical) { addedRotation = -addedRotation; } @@ -317,42 +323,42 @@ namespace Barotrauma void drawSection(Vector2 slicePos, Rectangle sliceRect) { - Vector2 transformedPos = slicePos - position; + Vector2 transformedPos = slicePos; + + if (flipHorizontal) + { + transformedPos.X = targetSize.X - transformedPos.X - sliceRect.Width * scale.X; + } + if (flipVertical) + { + transformedPos.Y = targetSize.Y - transformedPos.Y - sliceRect.Height * scale.Y; + } + transformedPos = advanceX * transformedPos.X + advanceY * transformedPos.Y; transformedPos += position - transformedOrigin; - spriteBatch.Draw(texture, transformedPos, sliceRect, drawColor, addedRotation, Vector2.Zero, scale, spriteEffects.Value, depth ?? this.depth); + spriteBatch.Draw( + texture: texture, + position: transformedPos, + sourceRectangle: sliceRect, + color: drawColor, + rotation: addedRotation, + origin: Vector2.Zero, + scale: scale, + effects: spriteEffects.Value, + layerDepth: depth ?? this.depth); } //wrap the drawOffset inside the sourceRect drawOffset.X = (drawOffset.X / scale.X) % sourceRect.Width; drawOffset.Y = (drawOffset.Y / scale.Y) % sourceRect.Height; - Vector2 flippedDrawOffset = Vector2.Zero; - if (flipHorizontal) - { - float diff = targetSize.X % (sourceRect.Width * scale.X); - flippedDrawOffset.X = (sourceRect.Width * scale.X - diff) / scale.X; - flippedDrawOffset.X = - MathUtils.NearlyEqual(flippedDrawOffset.X, MathF.Round(flippedDrawOffset.X)) ? - MathF.Round(flippedDrawOffset.X) : flippedDrawOffset.X; - } - if (flipVertical) - { - float diff = targetSize.Y % (sourceRect.Height * scale.Y); - flippedDrawOffset.Y = (sourceRect.Height * scale.Y - diff) / scale.Y; - flippedDrawOffset.Y = - MathUtils.NearlyEqual(flippedDrawOffset.Y, MathF.Round(flippedDrawOffset.Y)) ? - MathF.Round(flippedDrawOffset.Y) : flippedDrawOffset.Y; - } - drawOffset += flippedDrawOffset; - //how many times the texture needs to be drawn on the x-axis int xTiles = (int)Math.Ceiling((targetSize.X + drawOffset.X * scale.X) / (sourceRect.Width * scale.X)); //how many times the texture needs to be drawn on the y-axis int yTiles = (int)Math.Ceiling((targetSize.Y + drawOffset.Y * scale.Y) / (sourceRect.Height * scale.Y)); //where the current tile is being drawn; - Vector2 currDrawPosition = position - drawOffset; + Vector2 currDrawPosition = -drawOffset; //which part of the texture we are currently drawing Rectangle texPerspective = sourceRect; @@ -364,54 +370,22 @@ namespace Barotrauma texPerspective.Height = sourceRect.Height; //offset to the left, draw a partial slice - if (currDrawPosition.X < position.X) + if (currDrawPosition.X < 0) { - float diff = (position.X - currDrawPosition.X); + float diff = -currDrawPosition.X; currDrawPosition.X += diff; texPerspective.Width -= (int)diff; - if (!flipHorizontal) - { - texPerspective.X += (int)diff; - } - if (!flipVertical) - { - texPerspective.Y += (int)diff; - } - } - //drawing an offset flipped sprite, need to draw an extra slice to the left side - if (currDrawPosition.X > position.X && x == 0) - { - if (flipHorizontal) - { - int sliceWidth = (int)((currDrawPosition.X - position.X) * scale.X); - Vector2 slicePos = currDrawPosition; - slicePos.X = position.X; - Rectangle sliceRect = texPerspective; - sliceRect.X = SourceRect.X; - sliceRect.Width = (int)(sliceWidth / scale.X); - - if (flipVertical) - { - slicePos.Y += flippedDrawOffset.Y; - } - - drawSection(slicePos, sliceRect); - currDrawPosition.X = slicePos.X + sliceWidth; - } + texPerspective.X += (int)diff; } //make sure the rightmost tiles don't go over the right side if (x == xTiles - 1) { - int diff = (int)(((currDrawPosition.X + texPerspective.Width * scale.X) - (position.X + targetSize.X)) / scale.X); + int diff = (int)(((currDrawPosition.X + texPerspective.Width * scale.X) - targetSize.X) / scale.X); texPerspective.Width -= diff; - if (flipHorizontal) - { - texPerspective.X += diff; - } } - currDrawPosition.Y = position.Y - drawOffset.Y; + currDrawPosition.Y = -drawOffset.Y; for (int y = 0; y < yTiles; y++) { @@ -419,45 +393,19 @@ namespace Barotrauma texPerspective.Height = sourceRect.Height; //offset above the top, draw a partial slice - if (currDrawPosition.Y < position.Y) + if (currDrawPosition.Y < 0f) { - float diff = (position.Y - currDrawPosition.Y); + float diff = -currDrawPosition.Y; currDrawPosition.Y += diff; texPerspective.Height -= (int)diff; - if (!flipVertical) - { - texPerspective.Y += (int)diff; - } - } - - //drawing an offset flipped sprite, need to draw an extra slice to the top - if (currDrawPosition.Y > position.Y && y == 0) - { - if (flipVertical) - { - int sliceHeight = (int)((currDrawPosition.Y - position.Y) * scale.Y); - - Vector2 slicePos = currDrawPosition; - slicePos.Y = position.Y; - Rectangle sliceRect = texPerspective; - sliceRect.Y = SourceRect.Y; - sliceRect.Height = (int)(sliceHeight / scale.Y); - - drawSection(slicePos, sliceRect); - - currDrawPosition.Y = slicePos.Y + sliceHeight; - } + texPerspective.Y += (int)diff; } //make sure the bottommost tiles don't go over the bottom if (y == yTiles - 1) { - int diff = (int)(((currDrawPosition.Y + texPerspective.Height * scale.Y) - (position.Y + targetSize.Y)) / scale.Y); + int diff = (int)(((currDrawPosition.Y + texPerspective.Height * scale.Y) - targetSize.Y) / scale.Y); texPerspective.Height -= diff; - if (flipVertical) - { - texPerspective.Y += diff; - } } drawSection(currDrawPosition, texPerspective); diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index d017a5cc0..3f7a3f89c 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.2.7.0 + 1.2.8.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 33b14c6a6..df3355c96 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.2.7.0 + 1.2.8.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 9f8a6bb7f..4cd3790e1 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.2.7.0 + 1.2.8.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 5496b5a27..4f1bb7e53 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.2.7.0 + 1.2.8.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 667339bf3..43e565bd8 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.2.7.0 + 1.2.8.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 192792db2..9573174e1 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.2.7.0 + 1.2.8.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs index a9abe7d30..253dc0e5c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -255,7 +255,11 @@ namespace Barotrauma } if (subObjectives.Any(so => so.CanBeCompleted)) { return; } UpdateSimpleEscape(deltaTime); - if (cannotFindSafeHull && !character.IsInFriendlySub && objectiveManager.Objectives.None(o => o is AIObjectiveReturn)) + + bool inFriendlySub = + character.IsInFriendlySub || + (character.IsEscorted && character.IsInPlayerSub); + if (cannotFindSafeHull && !inFriendlySub && objectiveManager.Objectives.None(o => o is AIObjectiveReturn)) { if (OrderPrefab.Prefabs.TryGet("return".ToIdentifier(), out OrderPrefab orderPrefab)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 2e760b73d..e74665360 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -436,7 +436,10 @@ namespace Barotrauma break; case "statvalue": var newStatValue = new AppliedStatValue(subElement); - afflictionStatValues.Add(newStatValue.StatType, newStatValue); + if (newStatValue.StatType == StatTypes.None || !afflictionStatValues.TryAdd(newStatValue.StatType, newStatValue)) + { + DebugConsole.ThrowError($"Invalid stat value in the affliction \"{parentDebugName}\".", contentPackage: element.ContentPackage); + } break; case "abilityflag": AbilityFlags flagType = subElement.GetAttributeEnum("flagtype", AbilityFlags.None); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index c44369a96..5d732d53c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -3129,6 +3129,7 @@ namespace Barotrauma if (character.IsDead) { return; } if (!UseInHealthInterface) { return; } + GameAnalyticsManager.AddDesignEvent("ApplyTreatment:" + Prefab.Identifier); #if CLIENT if (user == Character.Controlled) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index 1b6d9aff5..447f3b41d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -834,7 +834,7 @@ namespace Barotrauma [Serialize(true, IsPropertySaveable.No)] public bool CanFlipY { get; private set; } - [Serialize(0.1f, IsPropertySaveable.No)] + [Serialize(0.01f, IsPropertySaveable.No)] public float MinScale { get; private set; } [Serialize(10.0f, IsPropertySaveable.No)] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index afbdb5271..cce8509ab 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -11,6 +11,7 @@ using System.Xml.Linq; using System.Collections.Immutable; using Barotrauma.Abilities; #if CLIENT +using System.Diagnostics; using Microsoft.Xna.Framework.Graphics; using Barotrauma.Lights; #endif @@ -281,14 +282,21 @@ namespace Barotrauma } } + public float ScaleWhenTextureOffsetSet { get; private set; } = 1.0f; + protected Vector2 textureOffset = Vector2.Zero; - [Editable(MinValueFloat = -1000f, MaxValueFloat = 1000f, ValueStep = 10f), Serialize("0.0, 0.0", IsPropertySaveable.Yes)] + [Editable(ForceShowPlusMinusButtons = true, ValueStep = 1f), Serialize("0.0, 0.0", IsPropertySaveable.Yes)] public Vector2 TextureOffset { get { return textureOffset; } set { textureOffset = value; + textureOffset.X = + MathUtils.PositiveModulo(textureOffset.X, Sprite.SourceRect.Width * TextureScale.X * Scale); + textureOffset.Y = + MathUtils.PositiveModulo(textureOffset.Y, Sprite.SourceRect.Height * TextureScale.Y * Scale); + ScaleWhenTextureOffsetSet = Scale; #if CLIENT SetLightTextureOffset(); #endif @@ -1584,6 +1592,9 @@ namespace Barotrauma Submarine = submarine, }; + bool flippedX = element.GetAttributeBool(nameof(FlippedX), false); + bool flippedY = element.GetAttributeBool(nameof(FlippedY), false); + if (submarine?.Info.GameVersion != null) { SerializableProperty.UpgradeGameVersion(s, s.Prefab.ConfigElement, submarine.Info.GameVersion); @@ -1594,6 +1605,19 @@ namespace Barotrauma { s.CrushDepth = Math.Max(s.CrushDepth, GameMain.GameSession.LevelData.InitialDepth * Physics.DisplayToRealWorldRatio + 500); } + +#if CLIENT + s.TextureOffset = UpgradeTextureOffset( + targetSize: rect.Size.ToVector2(), + originalTextureOffset: + // Note: cannot use s.TextureOffset because wrapping is very weird in the old logic + element.GetAttributeVector2("TextureOffset", Vector2.Zero), + submarineInfo: submarine.Info, + sourceRect: s.Sprite.SourceRect, + scale: s.Scale * s.TextureScale, + flippedX: flippedX, + flippedY: flippedY); +#endif } bool hasDamage = false; @@ -1637,8 +1661,8 @@ namespace Barotrauma } } - if (element.GetAttributeBool(nameof(FlippedX), false)) { s.FlipX(false); } - if (element.GetAttributeBool(nameof(FlippedY), false)) { s.FlipY(false); } + if (flippedX) { s.FlipX(false); } + if (flippedY) { s.FlipY(false); } //structures with a body drop a shadow by default if (element.GetAttribute(nameof(UseDropShadow)) == null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/Editable/ConditionallyEditable.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/Editable/ConditionallyEditable.cs index 5229bf5b5..4ac5dbc79 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/Editable/ConditionallyEditable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/Editable/ConditionallyEditable.cs @@ -45,7 +45,7 @@ sealed class ConditionallyEditable : Editable => (entity is Item { body: null } item && item.Prefab.AllowRotatingInEditor) || (entity is Structure structure && structure.Prefab.AllowRotatingInEditor), ConditionType.Attachable - => entity is Holdable { Attachable: true }, + => GetComponent(entity) is Holdable { Attachable: true }, ConditionType.HasBody => entity is Structure { HasBody: true } or Item { body: not null }, ConditionType.Pickable @@ -53,14 +53,28 @@ sealed class ConditionallyEditable : Editable ConditionType.OnlyByStatusEffectsAndNetwork => GameMain.NetworkMember is { IsServer: true }, ConditionType.HasIntegratedButtons - => entity is Door { HasIntegratedButtons: true }, + => GetComponent(entity) is { HasIntegratedButtons: true }, ConditionType.IsToggleableController - => entity is Controller { IsToggle: true } controller && controller.Item.GetComponent() != null, + => GetComponent(entity) is Controller { IsToggle: true } controller && + controller.Item.GetComponent() != null, ConditionType.HasConnectionPanel - => (entity is Item item && item.GetComponent() != null) - || (entity is ItemComponent ic && ic.Item.GetComponent() != null), + => GetComponent(entity) != null, _ => false }; + + static T GetComponent(ISerializableEntity e) where T : ItemComponent + { + if (e is T t) { return t; } + if (e is Item item) + { + return item.GetComponent(); + } + if (e is ItemComponent ic) + { + return ic.Item.GetComponent(); + } + return null; + } } } diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index a64e7dc57..e0b94102d 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,21 @@ +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.2.8.0 +------------------------------------------------------------------------------------------------------------------------------------------------- + +Changes: +- Allow scaling most items below 0.1 in the submarine editor again. We set 0.1 as a hard limit because smaller scales caused issues with tiling items (such as labels), without realizing some sub builders have found some creative uses for heavily downscaled items (such as tiny turrets used as "dials"). + +Fixes: +- Fixed escorted characters trying to leave the sub if they can't find a safe hull to get to (e.g. when the sub is flooded). +- Fixed rotated structures not appearing rotated on the status monitor. +- Fixed wires getting misaligned when saving them in an item assembly. +- Fixed security NPCs being unable to swap batteries in stun batons. +- Fixed inability to edit the false output of a switch in multiplayer. +- (Finally) fixed texture offsets on tiled sprites such as background walls behaving erratically on non-default texture scales and mirrored entities. + +Modding: +- Fixed crashing if an affliction prefab contains stat values with a duplicate type, or multiple stat values whose type can't be parsed. + ------------------------------------------------------------------------------------------------------------------------------------------------- v1.2.7.0 ------------------------------------------------------------------------------------------------------------------------------------------------- From 27bceae85392ff746f0c9b7d7b93e54ecba0d9c7 Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Thu, 11 Jan 2024 16:20:17 +0200 Subject: [PATCH 23/53] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 6c0e60891..97a42ff12 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -73,7 +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.2.7.0 (Winter Update hotfix) + - v1.2.8.0 (Winter Update hotfix 2) - Other validations: required: true From 860aff6a9e8acbeeb568ab30bbd1cbc84bb2cf08 Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Thu, 18 Jan 2024 17:07:06 +0200 Subject: [PATCH 24/53] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 97a42ff12..bb9a18f67 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -74,6 +74,7 @@ body: 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.2.8.0 (Winter Update hotfix 2) + - v1.2.9.0 (unstable) - Other validations: required: true From 6ab23ddf891d4d2ead7a2ff6050751e317f98b3f Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Mon, 29 Jan 2024 09:47:04 +0200 Subject: [PATCH 25/53] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index bb9a18f67..424938507 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -74,7 +74,7 @@ body: 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.2.8.0 (Winter Update hotfix 2) - - v1.2.9.0 (unstable) + - v1.2..0 (unstable) - Other validations: required: true From 05398cc56cca028801789d9563616f2a582e563f Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Mon, 29 Jan 2024 09:47:17 +0200 Subject: [PATCH 26/53] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 424938507..9d8ebde47 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -74,7 +74,7 @@ body: 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.2.8.0 (Winter Update hotfix 2) - - v1.2..0 (unstable) + - v1.2.10.0 (unstable) - Other validations: required: true From bff194d1abfd8f1662eecf3aab1d4e37e5cc79e4 Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Fri, 16 Feb 2024 16:45:43 +0200 Subject: [PATCH 27/53] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 9d8ebde47..2c0cc4a14 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -74,7 +74,7 @@ body: 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.2.8.0 (Winter Update hotfix 2) - - v1.2.10.0 (unstable) + - v1.2.12.0 (unstable) - Other validations: required: true From 3b722fae594d298f7a3b9812adac7b9f991be2dd Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Wed, 6 Mar 2024 09:46:38 +0200 Subject: [PATCH 28/53] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 2c0cc4a14..9fb331b8c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -74,7 +74,8 @@ body: 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.2.8.0 (Winter Update hotfix 2) - - v1.2.12.0 (unstable) + - v1.2.13.0 (unstable) + - v1.2.8.0 (EOS test build) - Other validations: required: true From d453115a92e7bd8ec89f458f325599ec13de8e34 Mon Sep 17 00:00:00 2001 From: Regalis11 Date: Wed, 20 Mar 2024 13:01:48 +0200 Subject: [PATCH 29/53] Bug report template --- .github/DISCUSSION_TEMPLATE/Bug reports.yml | 102 ++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 .github/DISCUSSION_TEMPLATE/Bug reports.yml diff --git a/.github/DISCUSSION_TEMPLATE/Bug reports.yml b/.github/DISCUSSION_TEMPLATE/Bug reports.yml new file mode 100644 index 000000000..9fb331b8c --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/Bug reports.yml @@ -0,0 +1,102 @@ +name: Bug Report +description: Found a bug? Help us squash it by making a bug report! +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to report a bug! Actionable reports are very important in identifying and fixing bugs, so please fill out all the fields carefully and provide as much information as you can while being concise. Please also note that we get lots of reports and may not always write back to each individually – that does not mean we have not read it. + - type: checkboxes + id: checks + attributes: + label: "Disclaimers" + options: + - label: "I have searched the issue tracker to check if the issue has already been reported." + required: true + - label: "My issue happened while using mods." + required: false + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Please tell us also what you expected should have happened if the game was behaving correctly. + placeholder: "Using the bike horn crashes the game." + validations: + required: true + - type: textarea + id: repro + attributes: + label: Reproduction steps + description: | + If possible, describe how the developers can get the bug to happen (or, in other words, what actions lead to you encountering the bug). **This is by far the most important part of the report** - it is often extremely difficult, or even impossible, to diagnose an issue if we don't know the conditions it occurs in. + If you have a save, a submarine file, screenshots or any other files that might help us diagnose the issue, you can attach them here. Note that GitHub doesn't support the .save or .sub file extensions, so you should .zip those types of files to allow them to be attached. + placeholder: | + 1. Start a multiplayer campaign + 2. Spawn a bike horn with console commands + 3. Use the bike horn + 4. Observe how the game crashes + validations: + required: true + - type: dropdown + id: prevalence + attributes: + label: Bug prevalence + description: "How often do you or others encounter this bug?" + options: + - Just once + - Happens every now and then + - Happens regularly + - Happens every time I play + validations: + required: true + - type: dropdown + id: mporsp + attributes: + label: Single player or multiplayer? + description: Did the issue happen in single player, multiplayer, or both? How was the server being hosted? + options: + - Single player + - Multiplayer hosted from the in-game menu (= using a listen server) + - Multiplayer hosted using a dedicated server + - Happens in both single player and multiplayer + - Happens outside single player or multiplayer game modes (e.g. game launches on startup, something broken in the main menu) + - Other + validations: + required: true + - type: input + id: othermporsp + attributes: + label: "-" + description: If you selected "Other" in the above dropdown, please clarify here. + - type: dropdown + id: version + attributes: + 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.2.8.0 (Winter Update hotfix 2) + - v1.2.13.0 (unstable) + - v1.2.8.0 (EOS test build) + - Other + validations: + required: true + - type: input + id: otherversion + attributes: + label: "-" + description: If you selected "Other" in the above dropdown because you are e.g. using a custom build, please tell us more about it here. + - type: dropdown + id: operating-systems + attributes: + label: Which operating system did you encounter this bug on? + options: + - Windows + - MacOS + - Linux + validations: + required: true + - type: textarea + id: errors + attributes: + label: Relevant error messages and crash reports + description: If the game produces any text relevant to your issue, please include those in full. You can copy error messages from the in-game console by right clicking the error and selecting "copy". Crash reports will be named crashreport.log or servercrashreport.log and they're automatically generated into the root of the game's installation folder on your computer. You can usually find the game files location through Steam (Right click Barotrauma in your Steam Library -> Properties -> Local files -> Browse local files). + render: shell From 71f7933f393263030ef00de58077dfd9fb93f38f Mon Sep 17 00:00:00 2001 From: Regalis11 Date: Wed, 20 Mar 2024 13:05:00 +0200 Subject: [PATCH 30/53] Renamed bug report template (should now show in the correct place?) --- .github/DISCUSSION_TEMPLATE/{Bug reports.yml => bug-reports.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/DISCUSSION_TEMPLATE/{Bug reports.yml => bug-reports.yml} (100%) diff --git a/.github/DISCUSSION_TEMPLATE/Bug reports.yml b/.github/DISCUSSION_TEMPLATE/bug-reports.yml similarity index 100% rename from .github/DISCUSSION_TEMPLATE/Bug reports.yml rename to .github/DISCUSSION_TEMPLATE/bug-reports.yml From b77981165a122cdeae008584d3f5dfb5cb00336f Mon Sep 17 00:00:00 2001 From: Regalis11 Date: Wed, 20 Mar 2024 13:17:07 +0200 Subject: [PATCH 31/53] Changed bug report template fields to match those in github's example file --- .github/DISCUSSION_TEMPLATE/bug-reports.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/DISCUSSION_TEMPLATE/bug-reports.yml b/.github/DISCUSSION_TEMPLATE/bug-reports.yml index 9fb331b8c..1e05751fa 100644 --- a/.github/DISCUSSION_TEMPLATE/bug-reports.yml +++ b/.github/DISCUSSION_TEMPLATE/bug-reports.yml @@ -1,5 +1,5 @@ -name: Bug Report -description: Found a bug? Help us squash it by making a bug report! +title: Bug Report +labels: Found a bug? Help us squash it by making a bug report! body: - type: markdown attributes: From ad49aa254787619b931699fe4e271ea8f30d80d1 Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Thu, 28 Mar 2024 13:55:44 +0200 Subject: [PATCH 32/53] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 111 +++----------------------- 1 file changed, 10 insertions(+), 101 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 9fb331b8c..f910332f9 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,102 +1,11 @@ name: Bug Report -description: Found a bug? Help us squash it by making a bug report! -body: - - type: markdown - attributes: - value: | - Thanks for taking the time to report a bug! Actionable reports are very important in identifying and fixing bugs, so please fill out all the fields carefully and provide as much information as you can while being concise. Please also note that we get lots of reports and may not always write back to each individually – that does not mean we have not read it. - - type: checkboxes - id: checks - attributes: - label: "Disclaimers" - options: - - label: "I have searched the issue tracker to check if the issue has already been reported." - required: true - - label: "My issue happened while using mods." - required: false - - type: textarea - id: what-happened - attributes: - label: What happened? - description: Please tell us also what you expected should have happened if the game was behaving correctly. - placeholder: "Using the bike horn crashes the game." - validations: - required: true - - type: textarea - id: repro - attributes: - label: Reproduction steps - description: | - If possible, describe how the developers can get the bug to happen (or, in other words, what actions lead to you encountering the bug). **This is by far the most important part of the report** - it is often extremely difficult, or even impossible, to diagnose an issue if we don't know the conditions it occurs in. - If you have a save, a submarine file, screenshots or any other files that might help us diagnose the issue, you can attach them here. Note that GitHub doesn't support the .save or .sub file extensions, so you should .zip those types of files to allow them to be attached. - placeholder: | - 1. Start a multiplayer campaign - 2. Spawn a bike horn with console commands - 3. Use the bike horn - 4. Observe how the game crashes - validations: - required: true - - type: dropdown - id: prevalence - attributes: - label: Bug prevalence - description: "How often do you or others encounter this bug?" - options: - - Just once - - Happens every now and then - - Happens regularly - - Happens every time I play - validations: - required: true - - type: dropdown - id: mporsp - attributes: - label: Single player or multiplayer? - description: Did the issue happen in single player, multiplayer, or both? How was the server being hosted? - options: - - Single player - - Multiplayer hosted from the in-game menu (= using a listen server) - - Multiplayer hosted using a dedicated server - - Happens in both single player and multiplayer - - Happens outside single player or multiplayer game modes (e.g. game launches on startup, something broken in the main menu) - - Other - validations: - required: true - - type: input - id: othermporsp - attributes: - label: "-" - description: If you selected "Other" in the above dropdown, please clarify here. - - type: dropdown - id: version - attributes: - 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.2.8.0 (Winter Update hotfix 2) - - v1.2.13.0 (unstable) - - v1.2.8.0 (EOS test build) - - Other - validations: - required: true - - type: input - id: otherversion - attributes: - label: "-" - description: If you selected "Other" in the above dropdown because you are e.g. using a custom build, please tell us more about it here. - - type: dropdown - id: operating-systems - attributes: - label: Which operating system did you encounter this bug on? - options: - - Windows - - MacOS - - Linux - validations: - required: true - - type: textarea - id: errors - attributes: - label: Relevant error messages and crash reports - description: If the game produces any text relevant to your issue, please include those in full. You can copy error messages from the in-game console by right clicking the error and selecting "copy". Crash reports will be named crashreport.log or servercrashreport.log and they're automatically generated into the root of the game's installation folder on your computer. You can usually find the game files location through Steam (Right click Barotrauma in your Steam Library -> Properties -> Local files -> Browse local files). - render: shell +description: **Tl;Dr: Please write your reports in the [Discussions section](https://github.com/FakeFishGames/Barotrauma/discussions/categories/bug-reports) from now on, and please look for and upvote reports about issues similar to yours.** + +For years, we've directed all bug reports from players to our issue tracker. That was handy because the reports went straight to our programmers. It was also really cumbersome, because we get a lot of reports, and using the issue tracker, we had to investigate them all more closely than we had time for. + +So now we've moved all player reports to the [discussions section](https://github.com/FakeFishGames/Barotrauma/discussions/categories/bug-reports), where we previously directed only feature requests. Why? + +- **Discussions can be upvoted** to show that you have the same issue. This is the number 1 new thing that we'd like everyone to do, as this will help us focus our attention on those issues that are bothering a lot of players. You're also encouraged to chime in on an existing discussion to give more information about it. +- We can turn discussions into tickets with a single click once we pick one to be worked on internally, so this doesn't make our work any more difficult. + +Nothing else changes! Please continue reporting, upvoting, and discussing issues. From 20d187cebfd53bbfb0bc2c162319ace4577c066d Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Thu, 28 Mar 2024 13:56:02 +0200 Subject: [PATCH 33/53] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index f910332f9..54cb38e9d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,5 +1,5 @@ name: Bug Report -description: **Tl;Dr: Please write your reports in the [Discussions section](https://github.com/FakeFishGames/Barotrauma/discussions/categories/bug-reports) from now on, and please look for and upvote reports about issues similar to yours.** +description: "**Tl;Dr: Please write your reports in the [Discussions section](https://github.com/FakeFishGames/Barotrauma/discussions/categories/bug-reports) from now on, and please look for and upvote reports about issues similar to yours.** For years, we've directed all bug reports from players to our issue tracker. That was handy because the reports went straight to our programmers. It was also really cumbersome, because we get a lot of reports, and using the issue tracker, we had to investigate them all more closely than we had time for. @@ -8,4 +8,4 @@ So now we've moved all player reports to the [discussions section](https://githu - **Discussions can be upvoted** to show that you have the same issue. This is the number 1 new thing that we'd like everyone to do, as this will help us focus our attention on those issues that are bothering a lot of players. You're also encouraged to chime in on an existing discussion to give more information about it. - We can turn discussions into tickets with a single click once we pick one to be worked on internally, so this doesn't make our work any more difficult. -Nothing else changes! Please continue reporting, upvoting, and discussing issues. +Nothing else changes! Please continue reporting, upvoting, and discussing issues." From b7e96b78239a1848f666a2b43a6daffe48e16ad3 Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Thu, 28 Mar 2024 13:57:39 +0200 Subject: [PATCH 34/53] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 54cb38e9d..589044400 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,5 +1,10 @@ name: Bug Report -description: "**Tl;Dr: Please write your reports in the [Discussions section](https://github.com/FakeFishGames/Barotrauma/discussions/categories/bug-reports) from now on, and please look for and upvote reports about issues similar to yours.** +description: "Please write your reports in the [Discussions section](https://github.com/FakeFishGames/Barotrauma/discussions/categories/bug-reports), and please look for and upvote reports about issues similar to yours." +body: + - type: markdown + attributes: + value: | + "**Tl;Dr: Please write your reports in the [Discussions section](https://github.com/FakeFishGames/Barotrauma/discussions/categories/bug-reports) from now on, and please look for and upvote reports about issues similar to yours.** For years, we've directed all bug reports from players to our issue tracker. That was handy because the reports went straight to our programmers. It was also really cumbersome, because we get a lot of reports, and using the issue tracker, we had to investigate them all more closely than we had time for. From c73945c1f408fd7ef5718284682073539e3be1d9 Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Thu, 28 Mar 2024 13:58:55 +0200 Subject: [PATCH 35/53] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 589044400..963032bfb 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -4,7 +4,7 @@ body: - type: markdown attributes: value: | - "**Tl;Dr: Please write your reports in the [Discussions section](https://github.com/FakeFishGames/Barotrauma/discussions/categories/bug-reports) from now on, and please look for and upvote reports about issues similar to yours.** +**Tl;Dr: Please write your reports in the [Discussions section](https://github.com/FakeFishGames/Barotrauma/discussions/categories/bug-reports) from now on, and please look for and upvote reports about issues similar to yours.** For years, we've directed all bug reports from players to our issue tracker. That was handy because the reports went straight to our programmers. It was also really cumbersome, because we get a lot of reports, and using the issue tracker, we had to investigate them all more closely than we had time for. @@ -13,4 +13,4 @@ So now we've moved all player reports to the [discussions section](https://githu - **Discussions can be upvoted** to show that you have the same issue. This is the number 1 new thing that we'd like everyone to do, as this will help us focus our attention on those issues that are bothering a lot of players. You're also encouraged to chime in on an existing discussion to give more information about it. - We can turn discussions into tickets with a single click once we pick one to be worked on internally, so this doesn't make our work any more difficult. -Nothing else changes! Please continue reporting, upvoting, and discussing issues." +Nothing else changes! Please continue reporting, upvoting, and discussing issues. From 92893babd48e3ff1484d8bcbc0e54e926252b8f9 Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Thu, 28 Mar 2024 13:59:35 +0200 Subject: [PATCH 36/53] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 963032bfb..f2d1bb24e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -4,13 +4,13 @@ body: - type: markdown attributes: value: | -**Tl;Dr: Please write your reports in the [Discussions section](https://github.com/FakeFishGames/Barotrauma/discussions/categories/bug-reports) from now on, and please look for and upvote reports about issues similar to yours.** +Tl;Dr: Please write your reports in the [Discussions section](https://github.com/FakeFishGames/Barotrauma/discussions/categories/bug-reports) from now on, and please look for and upvote reports about issues similar to yours. For years, we've directed all bug reports from players to our issue tracker. That was handy because the reports went straight to our programmers. It was also really cumbersome, because we get a lot of reports, and using the issue tracker, we had to investigate them all more closely than we had time for. So now we've moved all player reports to the [discussions section](https://github.com/FakeFishGames/Barotrauma/discussions/categories/bug-reports), where we previously directed only feature requests. Why? -- **Discussions can be upvoted** to show that you have the same issue. This is the number 1 new thing that we'd like everyone to do, as this will help us focus our attention on those issues that are bothering a lot of players. You're also encouraged to chime in on an existing discussion to give more information about it. +- Discussions can be upvoted to show that you have the same issue. This is the number 1 new thing that we'd like everyone to do, as this will help us focus our attention on those issues that are bothering a lot of players. You're also encouraged to chime in on an existing discussion to give more information about it. - We can turn discussions into tickets with a single click once we pick one to be worked on internally, so this doesn't make our work any more difficult. Nothing else changes! Please continue reporting, upvoting, and discussing issues. From ac7017c5a07c15f7d78d3409e42a89d873ce99cf Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Thu, 28 Mar 2024 14:01:28 +0200 Subject: [PATCH 37/53] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index f2d1bb24e..53a643923 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -4,13 +4,4 @@ body: - type: markdown attributes: value: | -Tl;Dr: Please write your reports in the [Discussions section](https://github.com/FakeFishGames/Barotrauma/discussions/categories/bug-reports) from now on, and please look for and upvote reports about issues similar to yours. - -For years, we've directed all bug reports from players to our issue tracker. That was handy because the reports went straight to our programmers. It was also really cumbersome, because we get a lot of reports, and using the issue tracker, we had to investigate them all more closely than we had time for. - -So now we've moved all player reports to the [discussions section](https://github.com/FakeFishGames/Barotrauma/discussions/categories/bug-reports), where we previously directed only feature requests. Why? - -- Discussions can be upvoted to show that you have the same issue. This is the number 1 new thing that we'd like everyone to do, as this will help us focus our attention on those issues that are bothering a lot of players. You're also encouraged to chime in on an existing discussion to give more information about it. -- We can turn discussions into tickets with a single click once we pick one to be worked on internally, so this doesn't make our work any more difficult. - -Nothing else changes! Please continue reporting, upvoting, and discussing issues. +Please write your reports in the [Discussions section](https://github.com/FakeFishGames/Barotrauma/discussions/categories/bug-reports) from now on, and please look for and upvote reports about issues similar to yours. From ccf874f9540034c2a69a8f2ac1528c92f9764a7d Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Thu, 28 Mar 2024 14:02:53 +0200 Subject: [PATCH 38/53] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 53a643923..774d9db32 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -4,4 +4,13 @@ body: - type: markdown attributes: value: | -Please write your reports in the [Discussions section](https://github.com/FakeFishGames/Barotrauma/discussions/categories/bug-reports) from now on, and please look for and upvote reports about issues similar to yours. + Tl;Dr: Please write your reports in the [Discussions section](https://github.com/FakeFishGames/Barotrauma/discussions/categories/bug-reports) from now on, and please look for and upvote reports about issues similar to yours.** + + For years, we've directed all bug reports from players to our issue tracker. That was handy because the reports went straight to our programmers. It was also really cumbersome, because we get a lot of reports, and using the issue tracker, we had to investigate them all more closely than we had time for. + + So now we've moved all player reports to the [discussions section](https://github.com/FakeFishGames/Barotrauma/discussions/categories/bug-reports), where we previously directed only feature requests. Why? + + - **Discussions can be upvoted** to show that you have the same issue. This is the number 1 new thing that we'd like everyone to do, as this will help us focus our attention on those issues that are bothering a lot of players. You're also encouraged to chime in on an existing discussion to give more information about it. + - We can turn discussions into tickets with a single click once we pick one to be worked on internally, so this doesn't make our work any more difficult. + + Nothing else changes! Please continue reporting, upvoting, and discussing issues. From b6b28568a29a7b1a849967b5af3cd5550a7b459b Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Thu, 28 Mar 2024 14:08:31 +0200 Subject: [PATCH 39/53] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 774d9db32..e55312c17 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -14,3 +14,9 @@ body: - We can turn discussions into tickets with a single click once we pick one to be worked on internally, so this doesn't make our work any more difficult. Nothing else changes! Please continue reporting, upvoting, and discussing issues. + - type: checkboxes + id: checks + attributes: + options: + - label: "I have read the above." + required: true From bf5e4aeb54bd17d5134bc97c6cabb36987eb3142 Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Thu, 28 Mar 2024 14:10:00 +0200 Subject: [PATCH 40/53] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index e55312c17..9a8420c08 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -14,9 +14,6 @@ body: - We can turn discussions into tickets with a single click once we pick one to be worked on internally, so this doesn't make our work any more difficult. Nothing else changes! Please continue reporting, upvoting, and discussing issues. - - type: checkboxes - id: checks - attributes: - options: - - label: "I have read the above." - required: true +- type: textarea + attributes: + label: "GitHub doesn't allow creating issue templates that don't allow user input, so here's a textbox you can type in! :)" From e34a9654e863d0cd5ddd64ebef9920d31831b20b Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Thu, 28 Mar 2024 14:11:52 +0200 Subject: [PATCH 41/53] Update bug_report.yml (ffs) --- .github/ISSUE_TEMPLATE/bug_report.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 9a8420c08..b966c44b6 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -14,6 +14,6 @@ body: - We can turn discussions into tickets with a single click once we pick one to be worked on internally, so this doesn't make our work any more difficult. Nothing else changes! Please continue reporting, upvoting, and discussing issues. -- type: textarea - attributes: - label: "GitHub doesn't allow creating issue templates that don't allow user input, so here's a textbox you can type in! :)" + - type: textarea + attributes: + label: "GitHub doesn't allow creating issue templates that don't allow user input, so here's a textbox you can type in! :)" From 9472034c2209c69273b9678995e4d5b1a2feb1fc Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Thu, 28 Mar 2024 14:13:10 +0200 Subject: [PATCH 42/53] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index b966c44b6..afdfa51f6 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,5 +1,5 @@ name: Bug Report -description: "Please write your reports in the [Discussions section](https://github.com/FakeFishGames/Barotrauma/discussions/categories/bug-reports), and please look for and upvote reports about issues similar to yours." +description: "Please write your reports in the [Discussions section](https://github.com/FakeFishGames/Barotrauma/discussions/categories/bug-reports) from now on" body: - type: markdown attributes: From cc592ce1dc0e426ecec220df07c28016b8b31334 Mon Sep 17 00:00:00 2001 From: Regalis11 Date: Thu, 28 Mar 2024 14:17:23 +0200 Subject: [PATCH 43/53] Disabled blank issues (must use the issue template, which now redirects to the Discussions section) --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- .github/ISSUE_TEMPLATE/config.yml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index afdfa51f6..afd1a0c99 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,5 +1,5 @@ name: Bug Report -description: "Please write your reports in the [Discussions section](https://github.com/FakeFishGames/Barotrauma/discussions/categories/bug-reports) from now on" +description: "Please write your reports in the Discussions section from now on." body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..275528fd7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Bug reports + url: https://github.com/FakeFishGames/Barotrauma/discussions/categories/bug-reports + about: Please post your bug reports in the Discussions section. \ No newline at end of file From 81ca8637be702c5adac2f0a42ccd0651e285057e Mon Sep 17 00:00:00 2001 From: Regalis11 Date: Thu, 28 Mar 2024 14:21:15 +0200 Subject: [PATCH 44/53] Deleted bug report template (let's see what this does) --- .github/ISSUE_TEMPLATE/bug_report.yml | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml deleted file mode 100644 index afd1a0c99..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Bug Report -description: "Please write your reports in the Discussions section from now on." -body: - - type: markdown - attributes: - value: | - Tl;Dr: Please write your reports in the [Discussions section](https://github.com/FakeFishGames/Barotrauma/discussions/categories/bug-reports) from now on, and please look for and upvote reports about issues similar to yours.** - - For years, we've directed all bug reports from players to our issue tracker. That was handy because the reports went straight to our programmers. It was also really cumbersome, because we get a lot of reports, and using the issue tracker, we had to investigate them all more closely than we had time for. - - So now we've moved all player reports to the [discussions section](https://github.com/FakeFishGames/Barotrauma/discussions/categories/bug-reports), where we previously directed only feature requests. Why? - - - **Discussions can be upvoted** to show that you have the same issue. This is the number 1 new thing that we'd like everyone to do, as this will help us focus our attention on those issues that are bothering a lot of players. You're also encouraged to chime in on an existing discussion to give more information about it. - - We can turn discussions into tickets with a single click once we pick one to be worked on internally, so this doesn't make our work any more difficult. - - Nothing else changes! Please continue reporting, upvoting, and discussing issues. - - type: textarea - attributes: - label: "GitHub doesn't allow creating issue templates that don't allow user input, so here's a textbox you can type in! :)" From 3791670c42bdd80ba3a2c7c0f6d6131efb20c841 Mon Sep 17 00:00:00 2001 From: Regalis11 Date: Thu, 28 Mar 2024 18:34:33 +0200 Subject: [PATCH 45/53] v1.3.0.1 (Epic Store release) --- .../ClientSource/Characters/CharacterInfo.cs | 2 +- .../CircuitBox/CircuitBoxConnection.cs | 13 +- .../ClientSource/CircuitBox/CircuitBoxUI.cs | 2 +- .../Transition/LegacySteamUgcTransition.cs | 2 +- .../ClientSource/DebugConsole.cs | 106 +- .../ClientSource/Eos/EosAccount.cs | 203 ++++ .../PrimaryLogin/Epic/EosEpicPrimaryLogin.cs | 70 ++ .../Steam/EosEpicSecondaryLogin.cs | 201 ++++ .../Steam/EosSteamPrimaryLogin.cs | 321 +++++ .../ClientSource/Fonts/ScalableFont.cs | 72 +- .../BarotraumaClient/ClientSource/GUI/GUI.cs | 60 +- .../ClientSource/GUI/GUIButton.cs | 2 +- .../ClientSource/GUI/GUIColorPicker.cs | 6 +- .../ClientSource/GUI/GUIComponent.cs | 16 +- .../ClientSource/GUI/GUIContextMenu.cs | 6 +- .../ClientSource/GUI/GUIDropDown.cs | 2 +- .../ClientSource/GUI/GUILayoutGroup.cs | 6 + .../ClientSource/GUI/GUIMessageBox.cs | 12 +- .../ClientSource/GUI/GUIPrefab.cs | 101 +- .../ClientSource/GUI/GUITextBlock.cs | 9 +- .../ClientSource/GUI/VideoPlayer.cs | 2 +- .../GameAnalytics/GameAnalyticsManager.cs | 13 +- .../BarotraumaClient/ClientSource/GameMain.cs | 155 ++- .../GameModes/SinglePlayerCampaign.cs | 6 +- .../Items/Components/ItemLabel.cs | 8 +- .../Items/Components/Machines/MiniMap.cs | 7 +- .../ClientSource/Map/Lights/LightManager.cs | 2 +- .../ClientSource/Map/Map/Map.cs | 3 +- .../ClientSource/Map/Map/Radiation.cs | 23 +- .../ClientSource/Map/MapEntity.cs | 6 +- .../Networking/ChildServerRelay.cs | 4 +- .../ClientSource/Networking/ConnectCommand.cs | 135 +++ .../ClientSource/Networking/GameClient.cs | 105 +- .../P2PSocket/DualStackP2PSocket.cs | 82 ++ .../Primitives/P2PSocket/EosP2PSocket.cs | 99 ++ .../Primitives/P2PSocket/P2PSocket.cs | 56 + .../P2PSocket/SteamConnectSocket.cs | 114 ++ .../Primitives/P2PSocket/SteamListenSocket.cs | 148 +++ .../Networking/Primitives/Peers/ClientPeer.cs | 89 +- .../Primitives/Peers/LidgrenClientPeer.cs | 80 +- ...SteamP2PClientPeer.cs => P2PClientPeer.cs} | 274 +++-- .../Primitives/Peers/P2POwnerPeer.cs | 557 +++++++++ .../Primitives/Peers/SteamP2POwnerPeer.cs | 492 -------- .../FriendProviders/FriendProvider.cs | 11 - .../FriendProviders/SteamFriendProvider.cs | 67 -- .../Networking/ServerList/PingUtils.cs | 69 +- .../Networking/ServerList/ServerInfo.cs | 118 +- .../CompositeServerProvider.cs | 2 +- .../ServerProviders/EosServerProvider.cs | 139 +++ .../ServerProviders/ServerProvider.cs | 4 +- .../SteamDedicatedServerProvider.cs | 19 +- .../ServerProviders/SteamP2PServerProvider.cs | 18 +- .../BarotraumaClient/ClientSource/Program.cs | 15 +- .../ClientSource/Screens/CampaignEndScreen.cs | 2 +- .../SinglePlayerCampaignSetupUI.cs | 2 +- .../Screens/EventEditor/EditorNode.cs | 5 +- .../Screens/EventEditor/EventEditorScreen.cs | 2 +- .../{ => MainMenuScreen}/MainMenuScreen.cs | 253 ++-- .../ClientSource/Screens/NetLobbyScreen.cs | 16 +- .../ClientSource/Screens/Screen.cs | 29 +- .../ServerListScreen/ServerListScreen.cs | 464 ++----- .../ClientSource/Screens/SubEditorScreen.cs | 10 +- .../ClientSource/Settings/SettingsMenu.cs | 102 +- .../ClientSource/Social/FriendInfo.cs | 73 ++ .../CompositeFriendProvider.cs | 46 + .../FriendProviders/EpicFriendProvider.cs | 173 +++ .../Social/FriendProviders/FriendProvider.cs | 25 + .../FriendProviders/SteamFriendProvider.cs | 69 ++ .../ClientSource/Social/SocialExtensions.cs | 34 + .../ClientSource/Social/SocialOverlay.cs | 1063 +++++++++++++++++ .../ClientSource/SpamServerFilter.cs | 4 +- .../ClientSource/Steam/AuthTicket.cs | 27 - .../ClientSource/Steam/BulkDownloader.cs | 8 +- .../ClientSource/Steam/Lobby.cs | 53 +- .../ClientSource/Steam/SteamManager.cs | 8 +- .../ClientSource/Steam/Workshop.cs | 4 +- .../ClientSource/Steam/WorkshopMenu/BBCode.cs | 4 +- .../WorkshopMenu/Mutable/InstalledTab.cs | 94 +- .../Steam/WorkshopMenu/Mutable/ItemList.cs | 4 +- .../Mutable/MutableWorkshopMenu.cs | 57 +- .../Steam/WorkshopMenu/Mutable/PublishTab.cs | 31 +- .../StoreIntegration/StoreIntegration.cs | 70 ++ .../Text/LocalizedString/LimitLString.cs | 6 +- .../Text/LocalizedString/WrappedLString.cs | 5 +- .../ClientSource/Utils/ConnectCommand.cs | 32 - .../ClientSource/Utils/ToolBox.cs | 25 +- .../BarotraumaClient/LinuxClient.csproj | 24 +- Barotrauma/BarotraumaClient/MacClient.csproj | 23 +- .../BarotraumaClient/WindowsClient.csproj | 24 +- .../BarotraumaServer/LinuxServer.csproj | 26 +- Barotrauma/BarotraumaServer/MacServer.csproj | 27 +- .../ServerSource/Characters/CharacterInfo.cs | 2 +- .../ServerSource/DebugConsole.cs | 10 +- .../Events/EventActions/EventLogAction.cs | 4 +- .../BarotraumaServer/ServerSource/GameMain.cs | 28 +- .../ServerSource/Networking/BanList.cs | 18 +- .../ServerSource/Networking/GameServer.cs | 62 +- .../Peers/Server/LidgrenServerPeer.cs | 181 +-- ...SteamP2PServerPeer.cs => P2PServerPeer.cs} | 215 ++-- .../Primitives/Peers/Server/ServerPeer.cs | 117 +- .../ServerSource/Networking/ServerSettings.cs | 9 +- .../BarotraumaServer/ServerSource/Program.cs | 31 +- .../ServerSource/Steam/SteamManager.cs | 88 +- .../ServerSource/Traitors/TraitorManager.cs | 4 +- .../BarotraumaServer/WindowsServer.csproj | 26 +- .../BarotraumaShared/DeployEosPrivate.props | 19 + .../DeployGameAnalytics.props | 4 +- ...vementManager.cs => AchievementManager.cs} | 184 ++- .../Animation/HumanoidAnimController.cs | 2 +- .../SharedSource/Characters/Character.cs | 13 +- .../SharedSource/Characters/CharacterInfo.cs | 2 +- .../Characters/Health/CharacterHealth.cs | 6 +- .../AbilityConditionHasItem.cs | 2 +- .../AbilityConditionHoldingItem.cs | 4 +- .../CircuitBox/CircuitBoxCursor.cs | 2 +- .../ContentFile/SubmarineFile.cs | 2 +- .../ContentPackage/ContentPackage.cs | 6 +- .../ContentPackageManager.cs | 4 +- .../ContentManagement/ContentXElement.cs | 1 + .../SharedSource/DebugConsole.cs | 10 +- .../SharedSource/Eos/Session.cs | 117 ++ .../SharedSource/Events/EventSet.cs | 6 +- .../Extensions/IEnumerableExtensions.cs | 103 -- .../Extensions/StringExtensions.cs | 16 +- .../GameAnalytics/GameAnalyticsConsent.cs | 134 ++- .../GameAnalytics/GameAnalyticsManager.cs | 45 +- .../GameSession/Data/CampaignMetadata.cs | 6 +- .../GameModes/MultiPlayerCampaign.cs | 8 +- .../SharedSource/GameSession/GameSession.cs | 6 +- .../Items/Components/Repairable.cs | 2 +- .../Items/Components/Signal/ColorComponent.cs | 2 +- .../SharedSource/Items/ItemPrefab.cs | 8 +- .../SharedSource/Map/Levels/Level.cs | 6 +- .../Networking/ChildServerRelay.cs | 36 +- .../SharedSource/Networking/NetworkMember.cs | 3 +- .../Networking/Primitives/AccountInfo.cs | 7 +- .../Primitives/Auth/AuthenticationTicket.cs | 65 + .../Primitives/Auth/Authenticator.cs | 38 + .../Auth/EgsOwnershipTokenAuthenticator.cs | 21 + .../SteamAuthTicketForEosHostAuthenticator.cs | 58 + ...teamAuthTicketForSteamHostAuthenticator.cs | 77 ++ .../Primitives/Endpoint/EosP2PEndpoint.cs | 32 + .../Primitives/Endpoint/LidgrenEndpoint.cs | 2 +- .../Primitives/Endpoint/P2PEndpoint.cs | 12 + .../Primitives/Endpoint/SteamP2PEndpoint.cs | 33 +- .../Fragmentation/MessageDefragmenter.cs | 40 + .../Message/Fragmentation/MessageFragment.cs | 17 + .../Fragmentation/MessageFragmenter.cs | 38 + .../Networking/Primitives/Message/Message.cs | 6 +- .../NetworkConnection/EosP2PConnection.cs | 8 + .../NetworkConnection/LidgrenConnection.cs | 2 +- .../NetworkConnection/NetworkConnection.cs | 13 +- .../NetworkConnection/P2PConnection.cs | 30 + .../NetworkConnection/PipeConnection.cs | 6 +- .../NetworkConnection/SteamP2PConnection.cs | 19 +- .../Primitives/NetworkExtensions.cs | 4 +- .../Primitives/NetworkPeerStructs.cs | 70 +- .../ServerListContentPackageInfo.cs | 23 + .../SharedSource/Networking/ServerSettings.cs | 47 +- .../SharedSource/Prefabs/PrefabCollection.cs | 2 +- .../Serialization/XMLExtensions.cs | 14 + .../SharedSource/Settings/GameSettings.cs | 9 +- .../SharedSource/Steam/AuthTicket.cs | 43 +- .../SharedSource/Steam/SteamManager.cs | 93 +- .../SharedSource/Steam/Workshop.cs | 22 +- .../Text/LocalizedString/LocalizedString.cs | 17 +- .../SharedSource/Utils/AssemblyInfo.cs | 53 +- .../SharedSource/Utils/Md5Hash.cs | 20 +- .../SharedSource/Utils/Rand.cs | 14 +- .../SharedSource/Utils/Result.cs | 85 -- .../SharedSource/Utils/TaskExtensions.cs | 18 +- .../SharedSource/Utils/ToolBox.cs | 65 - Barotrauma/BarotraumaShared/changelog.txt | 14 + Barotrauma/BarotraumaTest/SplitEscapedTest.cs | 63 + Deploy/DeployAll/Deployables.cs | 18 +- Deploy/DeployAll/DotnetCmd.cs | 4 +- Deploy/DeployAll/EgsAssistant.cs | 143 +++ Deploy/DeployAll/GitCmd.cs | 4 +- Deploy/DeployAll/Program.cs | 9 + Deploy/DeployAll/SteamPipeAssistant.cs | 30 +- Deploy/DeployAll/Util.cs | 55 +- .../Achievements/AchievementStats.cs | 59 + .../BarotraumaCore/BarotraumaCore.csproj | 28 + .../Extensions/ColorExtensions.cs | 0 .../Extensions/EnumerableExtensionsCore.cs | 34 + .../Extensions/PointExtensions.cs | 0 .../Extensions/RectangleExtensions.cs | 0 .../Extensions/RngExtensions.cs | 11 + .../Extensions/StringExtensions.cs | 66 + .../Extensions/StringFormatter.cs | 4 +- .../Extensions/StructExtensions.cs | 0 .../Extensions/VectorExtensions.cs | 0 .../Primitives/AccountId/AccountId.cs | 11 +- .../Primitives/AccountId/EpicAccountId.cs | 32 + .../Primitives/AccountId/SteamId.cs | 5 +- .../Networking/Primitives/Address/Address.cs | 2 +- .../Primitives/Address/EosP2PAddress.cs | 38 + .../Primitives/Address/LidgrenAddress.cs | 2 +- .../Primitives/Address/P2PAddress.cs | 7 + .../Primitives/Address/PipeAddress.cs | 2 +- .../Primitives/Address/SteamP2PAddress.cs | 8 +- .../Primitives/Address/UnknownAddress.cs | 2 +- .../Networking/Primitives/NetworkEnums.cs | 24 +- .../BarotraumaCore/Social/FriendStatus.cs | 9 + .../BarotraumaCore}/Utils/CollectionConcat.cs | 0 .../BarotraumaCore}/Utils/Either.cs | 8 + .../BarotraumaCore/Utils/GameVersion.cs | 9 + .../Utils/IEnumerableExtensionsCore.cs | 87 ++ .../BarotraumaCore/Utils}/Identifier.cs | 30 +- .../BarotraumaCore/Utils/Janitor.cs | 49 + .../BarotraumaCore/Utils/JsonWebToken.cs | 74 ++ .../BarotraumaCore}/Utils/MathUtils.cs | 21 +- .../BarotraumaCore}/Utils/NamedEvent.cs | 22 +- .../BarotraumaCore/Utils/OneOf.cs | 49 + .../BarotraumaCore}/Utils/Option/Option.cs | 4 + .../BarotraumaCore}/Utils/Range.cs | 0 .../BarotraumaCore}/Utils/ReflectionUtils.cs | 32 +- .../BarotraumaCore/Utils/Result.cs | 120 ++ .../BarotraumaCore/Utils/TaskExtensions.cs | 40 + .../BarotraumaCore}/Utils/TaskPool.cs | 51 +- .../BarotraumaCore}/Utils/Threading.cs | 4 +- .../BarotraumaCore/Utils/ToolBoxCore.cs | 96 ++ .../BarotraumaCore/Utils/Unit.cs | 8 + .../Utils/UnreachableCodeException.cs | 8 + .../Achievements/AchievementErrors.cs | 45 + .../EosInterface/Achievements/Achievements.cs | 55 + .../Core/ApplicationCredentials.cs | 10 + .../EosInterface/Core/AssemblyInfo.cs | 5 + .../BarotraumaLibs/EosInterface/Core/Core.cs | 258 ++++ .../EosInterface/Core/StatusExtensions.cs | 13 + .../EosInterface/EosInterface.csproj | 26 + .../EosInterface/Friends/EgsFriend.cs | 13 + .../EosInterface/Friends/Friends.cs | 62 + .../EosInterface/Friends/Presence.cs | 105 ++ .../IdAndAuth/EgsAuthContinuanceToken.cs | 34 + .../EosInterface/IdAndAuth/EgsIdToken.cs | 52 + .../IdAndAuth/EosConnectContinuanceToken.cs | 37 + .../EosInterface/IdAndAuth/EosIdToken.cs | 114 ++ .../EosInterface/IdAndAuth/IdQueries.cs | 64 + .../EosInterface/IdAndAuth/Login.cs | 206 ++++ .../EosInterface/IdAndAuth/Ownership.cs | 32 + .../EosInterface/IdAndAuth/ProductUserId.cs | 13 + .../EosInterface/P2P/P2PSocket.cs | 95 ++ .../EosInterface/P2P/SocketId.cs | 6 + .../EosInterface/Sessions/Sessions.cs | 167 +++ .../EosInterfacePrivate/.gitignore | 2 + .../EosInterface.Implementation.Linux.csproj | 40 + .../EosInterface.Implementation.MacOS.csproj | 40 + .../EosInterface.Implementation.Win64.csproj | 39 + .../Achievements/AchievementsPrivate.cs | 266 +++++ .../InterfaceImpl/Core/CorePrivate.cs | 176 +++ .../InterfaceImpl/Core/CustomTaskScheduler.cs | 67 ++ .../InterfaceImpl/Friends/FriendsPrivate.cs | 249 ++++ .../InterfaceImpl/Friends/PresencePrivate.cs | 465 +++++++ .../IdAndAuth/EgsIdTokenPrivate.cs | 119 ++ .../IdAndAuth/EosIdTokenPrivate.cs | 73 ++ .../IdAndAuth/IdQueriesPrivate.cs | 222 ++++ .../InterfaceImpl/IdAndAuth/LoginPrivate.cs | 708 +++++++++++ .../IdAndAuth/OwnershipPrivate.cs | 158 +++ .../InterfaceImpl/P2P/P2PSocketPrivate.cs | 252 ++++ .../Sessions/OwnedSessionsPrivate.cs | 319 +++++ .../Sessions/RemoteSessionsPrivate.cs | 159 +++ .../InterfaceImpl/Util/CallbackWaiter.cs | 50 + .../InterfaceImpl/Util/ResultExtension.cs | 14 + Libraries/Facepunch.Steamworks/SteamServer.cs | 2 +- .../Facepunch.Steamworks/SteamUserStats.cs | 21 +- LinuxSolution.sln | 50 +- MacSolution.sln | 48 + WindowsSolution.sln | 44 +- 269 files changed, 13160 insertions(+), 2966 deletions(-) create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Eos/EosAccount.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Eos/PrimaryLogin/Epic/EosEpicPrimaryLogin.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Eos/PrimaryLogin/Steam/EosEpicSecondaryLogin.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Eos/PrimaryLogin/Steam/EosSteamPrimaryLogin.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Networking/ConnectCommand.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/P2PSocket/DualStackP2PSocket.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/P2PSocket/EosP2PSocket.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/P2PSocket/P2PSocket.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/P2PSocket/SteamConnectSocket.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/P2PSocket/SteamListenSocket.cs rename Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/{SteamP2PClientPeer.cs => P2PClientPeer.cs} (59%) create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/P2POwnerPeer.cs delete mode 100644 Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs delete mode 100644 Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/FriendProviders/FriendProvider.cs delete mode 100644 Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/FriendProviders/SteamFriendProvider.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/EosServerProvider.cs rename Barotrauma/BarotraumaClient/ClientSource/Screens/{ => MainMenuScreen}/MainMenuScreen.cs (90%) create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Social/FriendInfo.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Social/FriendProviders/CompositeFriendProvider.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Social/FriendProviders/EpicFriendProvider.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Social/FriendProviders/FriendProvider.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Social/FriendProviders/SteamFriendProvider.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Social/SocialExtensions.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Social/SocialOverlay.cs delete mode 100644 Barotrauma/BarotraumaClient/ClientSource/Steam/AuthTicket.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/StoreIntegration/StoreIntegration.cs delete mode 100644 Barotrauma/BarotraumaClient/ClientSource/Utils/ConnectCommand.cs rename Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/{SteamP2PServerPeer.cs => P2PServerPeer.cs} (59%) create mode 100644 Barotrauma/BarotraumaShared/DeployEosPrivate.props rename Barotrauma/BarotraumaShared/SharedSource/{SteamAchievementManager.cs => AchievementManager.cs} (74%) create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Eos/Session.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Auth/AuthenticationTicket.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Auth/Authenticator.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Auth/EgsOwnershipTokenAuthenticator.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Auth/SteamAuthTicketForEosHostAuthenticator.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Auth/SteamAuthTicketForSteamHostAuthenticator.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/EosP2PEndpoint.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/P2PEndpoint.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Fragmentation/MessageDefragmenter.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Fragmentation/MessageFragment.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Fragmentation/MessageFragmenter.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/EosP2PConnection.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/P2PConnection.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Networking/ServerListContentPackageInfo.cs delete mode 100644 Barotrauma/BarotraumaShared/SharedSource/Utils/Result.cs create mode 100644 Barotrauma/BarotraumaTest/SplitEscapedTest.cs create mode 100644 Deploy/DeployAll/EgsAssistant.cs create mode 100644 Libraries/BarotraumaLibs/BarotraumaCore/Achievements/AchievementStats.cs create mode 100644 Libraries/BarotraumaLibs/BarotraumaCore/BarotraumaCore.csproj rename {Barotrauma/BarotraumaShared/SharedSource => Libraries/BarotraumaLibs/BarotraumaCore}/Extensions/ColorExtensions.cs (100%) create mode 100644 Libraries/BarotraumaLibs/BarotraumaCore/Extensions/EnumerableExtensionsCore.cs rename {Barotrauma/BarotraumaShared/SharedSource => Libraries/BarotraumaLibs/BarotraumaCore}/Extensions/PointExtensions.cs (100%) rename {Barotrauma/BarotraumaShared/SharedSource => Libraries/BarotraumaLibs/BarotraumaCore}/Extensions/RectangleExtensions.cs (100%) create mode 100644 Libraries/BarotraumaLibs/BarotraumaCore/Extensions/RngExtensions.cs create mode 100644 Libraries/BarotraumaLibs/BarotraumaCore/Extensions/StringExtensions.cs rename {Barotrauma/BarotraumaShared/SharedSource => Libraries/BarotraumaLibs/BarotraumaCore}/Extensions/StringFormatter.cs (97%) rename {Barotrauma/BarotraumaShared/SharedSource => Libraries/BarotraumaLibs/BarotraumaCore}/Extensions/StructExtensions.cs (100%) rename {Barotrauma/BarotraumaShared/SharedSource => Libraries/BarotraumaLibs/BarotraumaCore}/Extensions/VectorExtensions.cs (100%) rename {Barotrauma/BarotraumaShared/SharedSource => Libraries/BarotraumaLibs/BarotraumaCore}/Networking/Primitives/AccountId/AccountId.cs (60%) create mode 100644 Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/AccountId/EpicAccountId.cs rename {Barotrauma/BarotraumaShared/SharedSource => Libraries/BarotraumaLibs/BarotraumaCore}/Networking/Primitives/AccountId/SteamId.cs (96%) rename {Barotrauma/BarotraumaShared/SharedSource => Libraries/BarotraumaLibs/BarotraumaCore}/Networking/Primitives/Address/Address.cs (95%) create mode 100644 Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/Address/EosP2PAddress.cs rename {Barotrauma/BarotraumaShared/SharedSource => Libraries/BarotraumaLibs/BarotraumaCore}/Networking/Primitives/Address/LidgrenAddress.cs (98%) create mode 100644 Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/Address/P2PAddress.cs rename {Barotrauma/BarotraumaShared/SharedSource => Libraries/BarotraumaLibs/BarotraumaCore}/Networking/Primitives/Address/PipeAddress.cs (91%) rename {Barotrauma/BarotraumaShared/SharedSource => Libraries/BarotraumaLibs/BarotraumaCore}/Networking/Primitives/Address/SteamP2PAddress.cs (81%) rename {Barotrauma/BarotraumaShared/SharedSource => Libraries/BarotraumaLibs/BarotraumaCore}/Networking/Primitives/Address/UnknownAddress.cs (87%) rename {Barotrauma/BarotraumaShared/SharedSource => Libraries/BarotraumaLibs/BarotraumaCore}/Networking/Primitives/NetworkEnums.cs (65%) create mode 100644 Libraries/BarotraumaLibs/BarotraumaCore/Social/FriendStatus.cs rename {Barotrauma/BarotraumaShared/SharedSource => Libraries/BarotraumaLibs/BarotraumaCore}/Utils/CollectionConcat.cs (100%) rename {Barotrauma/BarotraumaShared/SharedSource => Libraries/BarotraumaLibs/BarotraumaCore}/Utils/Either.cs (92%) create mode 100644 Libraries/BarotraumaLibs/BarotraumaCore/Utils/GameVersion.cs create mode 100644 Libraries/BarotraumaLibs/BarotraumaCore/Utils/IEnumerableExtensionsCore.cs rename {Barotrauma/BarotraumaShared/SharedSource/ContentManagement => Libraries/BarotraumaLibs/BarotraumaCore/Utils}/Identifier.cs (82%) create mode 100644 Libraries/BarotraumaLibs/BarotraumaCore/Utils/Janitor.cs create mode 100644 Libraries/BarotraumaLibs/BarotraumaCore/Utils/JsonWebToken.cs rename {Barotrauma/BarotraumaShared/SharedSource => Libraries/BarotraumaLibs/BarotraumaCore}/Utils/MathUtils.cs (98%) rename {Barotrauma/BarotraumaShared/SharedSource => Libraries/BarotraumaLibs/BarotraumaCore}/Utils/NamedEvent.cs (62%) create mode 100644 Libraries/BarotraumaLibs/BarotraumaCore/Utils/OneOf.cs rename {Barotrauma/BarotraumaShared/SharedSource => Libraries/BarotraumaLibs/BarotraumaCore}/Utils/Option/Option.cs (94%) rename {Barotrauma/BarotraumaShared/SharedSource => Libraries/BarotraumaLibs/BarotraumaCore}/Utils/Range.cs (100%) rename {Barotrauma/BarotraumaShared/SharedSource => Libraries/BarotraumaLibs/BarotraumaCore}/Utils/ReflectionUtils.cs (79%) create mode 100644 Libraries/BarotraumaLibs/BarotraumaCore/Utils/Result.cs create mode 100644 Libraries/BarotraumaLibs/BarotraumaCore/Utils/TaskExtensions.cs rename {Barotrauma/BarotraumaShared/SharedSource => Libraries/BarotraumaLibs/BarotraumaCore}/Utils/TaskPool.cs (58%) rename {Barotrauma/BarotraumaShared/SharedSource => Libraries/BarotraumaLibs/BarotraumaCore}/Utils/Threading.cs (88%) create mode 100644 Libraries/BarotraumaLibs/BarotraumaCore/Utils/ToolBoxCore.cs create mode 100644 Libraries/BarotraumaLibs/BarotraumaCore/Utils/Unit.cs create mode 100644 Libraries/BarotraumaLibs/BarotraumaCore/Utils/UnreachableCodeException.cs create mode 100644 Libraries/BarotraumaLibs/EosInterface/Achievements/AchievementErrors.cs create mode 100644 Libraries/BarotraumaLibs/EosInterface/Achievements/Achievements.cs create mode 100644 Libraries/BarotraumaLibs/EosInterface/Core/ApplicationCredentials.cs create mode 100644 Libraries/BarotraumaLibs/EosInterface/Core/AssemblyInfo.cs create mode 100644 Libraries/BarotraumaLibs/EosInterface/Core/Core.cs create mode 100644 Libraries/BarotraumaLibs/EosInterface/Core/StatusExtensions.cs create mode 100644 Libraries/BarotraumaLibs/EosInterface/EosInterface.csproj create mode 100644 Libraries/BarotraumaLibs/EosInterface/Friends/EgsFriend.cs create mode 100644 Libraries/BarotraumaLibs/EosInterface/Friends/Friends.cs create mode 100644 Libraries/BarotraumaLibs/EosInterface/Friends/Presence.cs create mode 100644 Libraries/BarotraumaLibs/EosInterface/IdAndAuth/EgsAuthContinuanceToken.cs create mode 100644 Libraries/BarotraumaLibs/EosInterface/IdAndAuth/EgsIdToken.cs create mode 100644 Libraries/BarotraumaLibs/EosInterface/IdAndAuth/EosConnectContinuanceToken.cs create mode 100644 Libraries/BarotraumaLibs/EosInterface/IdAndAuth/EosIdToken.cs create mode 100644 Libraries/BarotraumaLibs/EosInterface/IdAndAuth/IdQueries.cs create mode 100644 Libraries/BarotraumaLibs/EosInterface/IdAndAuth/Login.cs create mode 100644 Libraries/BarotraumaLibs/EosInterface/IdAndAuth/Ownership.cs create mode 100644 Libraries/BarotraumaLibs/EosInterface/IdAndAuth/ProductUserId.cs create mode 100644 Libraries/BarotraumaLibs/EosInterface/P2P/P2PSocket.cs create mode 100644 Libraries/BarotraumaLibs/EosInterface/P2P/SocketId.cs create mode 100644 Libraries/BarotraumaLibs/EosInterface/Sessions/Sessions.cs create mode 100644 Libraries/BarotraumaLibs/EosInterfacePrivate/.gitignore create mode 100644 Libraries/BarotraumaLibs/EosInterfacePrivate/EosInterface.Implementation.Linux.csproj create mode 100644 Libraries/BarotraumaLibs/EosInterfacePrivate/EosInterface.Implementation.MacOS.csproj create mode 100644 Libraries/BarotraumaLibs/EosInterfacePrivate/EosInterface.Implementation.Win64.csproj create mode 100644 Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/Achievements/AchievementsPrivate.cs create mode 100644 Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/Core/CorePrivate.cs create mode 100644 Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/Core/CustomTaskScheduler.cs create mode 100644 Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/Friends/FriendsPrivate.cs create mode 100644 Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/Friends/PresencePrivate.cs create mode 100644 Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/IdAndAuth/EgsIdTokenPrivate.cs create mode 100644 Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/IdAndAuth/EosIdTokenPrivate.cs create mode 100644 Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/IdAndAuth/IdQueriesPrivate.cs create mode 100644 Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/IdAndAuth/LoginPrivate.cs create mode 100644 Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/IdAndAuth/OwnershipPrivate.cs create mode 100644 Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/P2P/P2PSocketPrivate.cs create mode 100644 Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/Sessions/OwnedSessionsPrivate.cs create mode 100644 Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/Sessions/RemoteSessionsPrivate.cs create mode 100644 Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/Util/CallbackWaiter.cs create mode 100644 Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/Util/ResultExtension.cs diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index e18d1e4e2..1ce98e9b5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -543,7 +543,7 @@ namespace Barotrauma Identifier factionId = inc.ReadIdentifier(); float minReputationToHire = 0.0f; - if (factionId != default) + if (!factionId.IsEmpty) { minReputationToHire = inc.ReadSingle(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxConnection.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxConnection.cs index ea89c9f57..dfe8278e5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxConnection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxConnection.cs @@ -73,13 +73,16 @@ namespace Barotrauma if (isMouseOver) { - var glowSprite = GUIStyle.UIGlowCircular.Value.Sprite; - float glowScale = 40f / glowSprite.size.X; - if (isScrewed) + var glowSprite = GUIStyle.UIGlowCircular.Value?.Sprite; + if (glowSprite is not null) { - glowScale *= 1.2f; + float glowScale = 40f / glowSprite.size.X; + if (isScrewed) + { + glowScale *= 1.2f; + } + glowSprite.Draw(spriteBatch, position, GUIStyle.Yellow, glowSprite.size / 2, scale: glowScale); } - glowSprite.Draw(spriteBatch, position, GUIStyle.Yellow, glowSprite.size / 2, scale: glowScale); } tooltip = Option.None; diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxUI.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxUI.cs index a195b3276..ee7a45761 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxUI.cs @@ -24,7 +24,7 @@ namespace Barotrauma private GUIFrame? selectedWireFrame; private GUIListBox? componentList; private GUITextBlock? inventoryIndicatorText; - private readonly Sprite cursorSprite = GUIStyle.CursorSprite[CursorState.Default]; + private readonly Sprite? cursorSprite = GUIStyle.CursorSprite[CursorState.Default]; private Option selection = Option.None; private string searchTerm = string.Empty; diff --git a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/LegacySteamUgcTransition.cs b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/LegacySteamUgcTransition.cs index 2b766254c..5f45b7d56 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/LegacySteamUgcTransition.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/LegacySteamUgcTransition.cs @@ -198,7 +198,7 @@ namespace Barotrauma.Transition DebugConsole.ThrowError("There was an error transferring mods", t2.Exception.GetInnermost()); } ContentPackageManager.LocalPackages.Refresh(); - if (t2.TryGetResult(out string[] modsToEnable)) + if (t2.TryGetResult(out string[]? modsToEnable)) { var newRegular = ContentPackageManager.EnabledPackages.Regular.ToList(); newRegular.AddRange(ContentPackageManager.LocalPackages.Regular diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index 8df615ee8..fa1990bcb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -9,10 +9,12 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Text; +using System.Threading.Tasks; using System.Xml.Linq; using static Barotrauma.FabricationRecipe; @@ -35,9 +37,7 @@ namespace Barotrauma if (!allowCheats && !CheatsEnabled && IsCheat) { NewMessage("You need to enable cheats using the command \"enablecheats\" before you can use the command \"" + Names[0] + "\".", Color.Red); -#if USE_STEAM - NewMessage("Enabling cheats will disable Steam achievements during this play session.", Color.Red); -#endif + NewMessage("Enabling cheats will disable achievements during this play session.", Color.Red); return; } @@ -66,8 +66,12 @@ namespace Barotrauma private static GUIFrame frame; private static GUIListBox listBox; private static GUITextBox textBox; +#if DEBUG + private const int maxLength = 100000; +#else private const int maxLength = 1000; - +#endif + public static GUITextBox TextBox => textBox; private static readonly ChatManager chatManager = new ChatManager(true, 64); @@ -157,7 +161,7 @@ namespace Barotrauma activeQuestionText?.SetAsLastChild(); - if (PlayerInput.KeyHit(Keys.F3)) + if (PlayerInput.KeyHit(Keys.F3) && !PlayerInput.KeyDown(Keys.LeftControl) && !PlayerInput.KeyDown(Keys.RightControl)) { Toggle(); } @@ -247,6 +251,9 @@ namespace Barotrauma case "unbindkey": case "wikiimage_character": case "wikiimage_sub": + case "eosStat": + case "eosUnlink": + case "eosLoginEpicViaSteam": return true; default: return client.HasConsoleCommandPermission(command); @@ -389,6 +396,87 @@ namespace Barotrauma private static void InitProjectSpecific() { + commands.Add(new Command("eosStat", "Query and display all logged in EOS users. Normally this is at most two users, but in a developer environment it could be more.", args => + { + if (!EosInterface.Core.IsInitialized) + { + NewMessage("EOS not initialized"); + return; + } + + var loggedInUsers = EosInterface.IdQueries.GetLoggedInPuids(); + if (!loggedInUsers.Any()) + { + NewMessage("EOS user not logged in"); + return; + } + + NewMessage("Logged in EOS users:"); + foreach (var puid in loggedInUsers) + { + TaskPool.Add( + $"eosStat -> {puid}", + EosInterface.IdQueries.GetSelfExternalAccountIds(puid), + t => + { + if (!t.TryGetResult(out Result, EosInterface.IdQueries.GetSelfExternalIdError> result)) { return; } + NewMessage($" - {puid}"); + + if (result.TryUnwrapSuccess(out var ids)) + { + foreach (var id in ids) + { + NewMessage($" - {id}"); + if (id is EpicAccountId eaid) + { + async Task gameOwnershipTokenTest() + { + var tokenOption = await EosInterface.Ownership.GetGameOwnershipToken(eaid); + var verified = await tokenOption.Bind(t => t.Verify()); + NewMessage($"Ownership token verify result: {verified}"); + } + _ = gameOwnershipTokenTest(); // Fire and forget! + EosInterface.Login.TestEosSessionTimeoutRecovery(puid); + } + } + } + else + { + NewMessage($" - Failed to get external IDs linked to {puid}: {result}"); + } + }); + } + })); + AssignRelayToServer("eosStat", false); + + commands.Add(new Command("eosUnlink", "Unlink the primary logged in external account ID from its corresponding EOS Product User ID and close the game. This is meant to be used to test the EOS consent flow.", args => + { + var userId = EosInterface.IdQueries.GetLoggedInPuids().FirstOrDefault(); + NewMessage($"Unlinking external account from PUID {userId}"); + GameSettings.SetCurrentConfig(GameSettings.CurrentConfig with { CrossplayChoice = Eos.EosSteamPrimaryLogin.CrossplayChoice.Unknown }); + GameSettings.SaveCurrentConfig(); + TaskPool.Add("unlinkTask", EosInterface.Login.UnlinkExternalAccount(userId), _ => + { + GameMain.Instance.Exit(); + }); + })); + AssignRelayToServer("eosUnlink", false); + + commands.Add(new Command("eosLoginEpicViaSteam", "Log into an Epic account via a link to the currently logged in Steam account", + args => + { + TaskPool.Add("eosLoginEpicViaSteam", + Eos.EosEpicSecondaryLogin.LoginToLinkedEpicAccount(), + TaskPool.IgnoredCallback); + })); + AssignRelayToServer("eosLoginEpicViaSteam", false); + + commands.Add(new Command("resetgameanalyticsconsent", "Reset whether you've given your consent for the game to send statistics to GameAnalytics. After executing the command, the game should ask for your consent again on relaunch.", args => + { + GameAnalyticsManager.ResetConsent(); + })); + AssignRelayToServer("resetgameanalyticsconsent", false); + commands.Add(new Command("copyitemnames", "", (string[] args) => { StringBuilder sb = new StringBuilder(); @@ -422,18 +510,16 @@ namespace Barotrauma } })); - commands.Add(new Command("enablecheats", "enablecheats: Enables cheat commands and disables Steam achievements during this play session.", (string[] args) => + commands.Add(new Command("enablecheats", "enablecheats: Enables cheat commands and disables achievements during this play session.", (string[] args) => { CheatsEnabled = true; - SteamAchievementManager.CheatsEnabled = true; + AchievementManager.CheatsEnabled = true; if (GameMain.GameSession?.Campaign is CampaignMode campaign) { campaign.CheatsEnabled = true; } NewMessage("Enabled cheat commands.", Color.Red); -#if USE_STEAM - NewMessage("Steam achievements have been disabled during this play session.", Color.Red); -#endif + NewMessage("Achievements have been disabled during this play session.", Color.Red); })); AssignRelayToServer("enablecheats", true); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Eos/EosAccount.cs b/Barotrauma/BarotraumaClient/ClientSource/Eos/EosAccount.cs new file mode 100644 index 000000000..6197ad2fd --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Eos/EosAccount.cs @@ -0,0 +1,203 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Barotrauma.Extensions; +using Barotrauma.Networking; +using Barotrauma.Steam; +using Microsoft.Xna.Framework; + +namespace Barotrauma.Eos; + +internal static class EosAccount +{ + /// + /// The user can have several account IDs if they've linked their Steam account to an Epic Games account + /// + public static ImmutableHashSet SelfAccountIds { get; private set; } = ImmutableHashSet.Empty; + + private static readonly Queue postLoginActions = new(); + private static bool isLoggedIn; + + public static void RefreshSelfAccountIds(Action? onRefreshComplete = null) + { + SelfAccountIds = ImmutableHashSet.Empty; + var selfPuids = EosInterface.IdQueries.GetLoggedInPuids(); + + if (selfPuids.Length == 0) + { + onRefreshComplete?.Invoke(); + return; + } + + var collectedIds = new Option>[selfPuids.Length]; + + Action taskDoneHandler(int index) + { + void countDoneTask(Task t) + { + try + { + if (!t.TryGetResult(out Result, EosInterface.IdQueries.GetSelfExternalIdError>? result)) { return; } + if (!result.TryUnwrapSuccess(out var ids)) { return; } + collectedIds[index] = Option.Some(ids); + } + finally + { + // If we failed to get IDs from this query, fill in the relevant slot in the collectedIds array + // to indicate that the task is done anyway + collectedIds[index] = Option.Some(collectedIds[index].Fallback(ImmutableArray.Empty)); + + // If all of the tasks are done, merge all of the collected IDs into the hashset + if (collectedIds.All(o => o.IsSome())) + { + SelfAccountIds = collectedIds.NotNone().SelectMany(a => a).ToImmutableHashSet(); + onRefreshComplete?.Invoke(); + } + } + } + + return countDoneTask; + } + + for (int i = 0; i < selfPuids.Length; i++) + { + TaskPool.Add($"SelfPlayerRowWithExternalAccountIds{i}", + EosInterface.IdQueries.GetSelfExternalAccountIds(selfPuids[i]), + taskDoneHandler(i)); + } + } + + #region Message box stuff + private static GUIMessageBox? messageBox; + + public static void ReplaceMessageBox(GUIMessageBox? newMessageBox) + { + messageBox?.Close(); + messageBox = newMessageBox; + } + + public static void CloseMessageBox() + => ReplaceMessageBox(null); + + public static GUIMessageBox CreateLoadingMessageBox((Func CanCancel, Action Cancel)? actions = null) + { + var relativeSize = messageBox?.InnerFrame.RectTransform.RelativeSize ?? (0.35f, 0.3f); + var newMessageBox = new GUIMessageBox( + headerText: LocalizedString.EmptyString, + text: LocalizedString.EmptyString, + relativeSize: relativeSize, + buttons: actions != null ? new[] { TextManager.Get("Cancel") } : Array.Empty()); + + if (actions != null) + { + newMessageBox.Buttons[0].Visible = false; + newMessageBox.Buttons[0].OnClicked = (_, _) => + { + actions.Value.Cancel.Invoke(); + return false; + }; + new GUICustomComponent(new RectTransform(Vector2.Zero, newMessageBox.InnerFrame.RectTransform), onUpdate: + (_, _) => + { + bool canCancel = actions.Value.CanCancel.Invoke(); + newMessageBox.Buttons[0].Visible |= canCancel; + newMessageBox.Buttons[0].Enabled = canCancel; + }); + } + + new GUICustomComponent( + new RectTransform(Vector2.One * 0.25f, newMessageBox.InnerFrame.RectTransform, Anchor.Center, scaleBasis: ScaleBasis.Smallest), + onDraw: static (sb, component) => + { + GUIStyle.GenericThrobber.Draw( + sb, + spriteIndex: (int)(Timing.TotalTime * 20f) % GUIStyle.GenericThrobber.FrameCount, + pos: component.Rect.Center.ToVector2(), + color: Color.White, + origin: GUIStyle.GenericThrobber.FrameSize.ToVector2() * 0.5f, + rotate: 0f, + scale: component.Rect.Size.ToVector2() / GUIStyle.GenericThrobber.FrameSize.ToVector2()); + }); + ReplaceMessageBox(newMessageBox); + return newMessageBox; + } + + public static Action RetryAction(LocalizedString intro, LocalizedString reason, Action retryAction, Action cancelAction) + { + return () => GameMain.ExecuteAfterContentFinishedLoading(() => AskRetry(intro, reason, retryAction, cancelAction)); + } + + private static void AskRetry(LocalizedString intro, LocalizedString failureReason, Action retryAction, Action cancelAction) + { + var options = new[] + { + TextManager.Get("Retry"), + TextManager.Get("Cancel") + }; + var askHowToProceed = TextManager.Get("AskHowToProceed"); + + GUIMessageBox msgBox = new GUIMessageBox( + headerText: TextManager.Get("EosIntroHeader"), + text: intro + "\n\n" + failureReason + "\n\n" + askHowToProceed, + options, + relativeSize: (0.4f, 0.4f)); + + msgBox.Buttons[0].OnClicked = delegate + { + retryAction(); + CloseMessageBox(); + return false; + }; + + msgBox.Buttons[1].OnClicked = delegate + { + cancelAction(); + CloseMessageBox(); + return false; + }; + + ReplaceMessageBox(msgBox); + } + #endregion + + public static void LoginPlatformSpecific() + { + if (GameMain.Instance.EgsExchangeCode.TryUnwrap(out var exchangeCode)) + { + LoginEpic(exchangeCode); + } + else if (SteamManager.IsInitialized) + { + LoginSteam(); + } + } + + private static void LoginSteam() + => EosSteamPrimaryLogin.Start(); + + private static void LoginEpic(string exchangeCode) + => EosEpicPrimaryLogin.Start(exchangeCode); + + public static void OnLoginSuccess() + { + isLoggedIn = true; + while (postLoginActions.TryDequeue(out var action)) + { + action(); + } + } + + public static void ExecuteAfterLogin(Action action) + { + if (isLoggedIn) + { + action(); + return; + } + postLoginActions.Enqueue(action); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Eos/PrimaryLogin/Epic/EosEpicPrimaryLogin.cs b/Barotrauma/BarotraumaClient/ClientSource/Eos/PrimaryLogin/Epic/EosEpicPrimaryLogin.cs new file mode 100644 index 000000000..501fc0c47 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Eos/PrimaryLogin/Epic/EosEpicPrimaryLogin.cs @@ -0,0 +1,70 @@ +#nullable enable +using System; +using System.Threading.Tasks; + +namespace Barotrauma.Eos; + +/// +/// Handles a player that owns a copy of Barotrauma on Epic Games Store (therefore they +/// will use their Epic Account ID as their primary identity) logging into EOS. +/// +static class EosEpicPrimaryLogin +{ + public static void Start(string exchangeCode) + { + TaskPool.Add("Eos.Core.LoginEpic", Initialize(exchangeCode), t => + { + if (!t.TryGetResult(out Action? action)) { return; } + action(); + }); + } + + private static void Success() + { + Eos.EosAccount.CloseMessageBox(); + Eos.EosAccount.RefreshSelfAccountIds(); + EosAccount.OnLoginSuccess(); + } + + private static async Task Initialize(string exchangeCode) + { + void retry() => Start(exchangeCode); + static void cancel() => EosInterface.Core.CleanupAndQuit(); + + var failedToInitializeIntro = TextManager.Get("EosFailedToInitialize"); + + var loginResult = await EosInterface.Login.LoginEpicExchangeCode(exchangeCode); + if (!loginResult.TryUnwrapSuccess(out var either)) + { + LocalizedString localizedError = $"Login failed with unknown error code."; + if (loginResult.TryUnwrapFailure(out EosInterface.Login.LoginError errorCode)) + { + localizedError = TextManager + .Get($"EosInterface.Core.InitError.{errorCode}") + .Fallback($"Failed to initialize Epic Online Services (error code {errorCode})"); + } + return EosAccount.RetryAction(failedToInitializeIntro, localizedError, retry, cancel); + } + + if (either.TryGet(out EosInterface.EosConnectContinuanceToken eosContinuanceToken)) + { + var createProductAccountResult = await EosInterface.Login.CreateProductAccount(eosContinuanceToken); + if (!createProductAccountResult.TryUnwrapSuccess(out var puid)) + { + return EosAccount.RetryAction( + failedToInitializeIntro, + $"Failed to create product user account: {(createProductAccountResult.TryUnwrapFailure(out var failure) ? failure : "unknown")}", + retry, cancel); + } + DebugConsole.NewMessage($"Logged into EOS for the first time with Epic as primary external account ID: {puid}"); + return Success; + } + else if (either.TryGet(out EosInterface.ProductUserId puid)) + { + DebugConsole.NewMessage($"Logged into EOS with Epic as primary external account ID: {puid}"); + return Success; + } + + throw new UnreachableCodeException(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Eos/PrimaryLogin/Steam/EosEpicSecondaryLogin.cs b/Barotrauma/BarotraumaClient/ClientSource/Eos/PrimaryLogin/Steam/EosEpicSecondaryLogin.cs new file mode 100644 index 000000000..15803f492 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Eos/PrimaryLogin/Steam/EosEpicSecondaryLogin.cs @@ -0,0 +1,201 @@ +#nullable enable +using System.Linq; +using System.Threading.Tasks; +using Barotrauma.Extensions; + +namespace Barotrauma.Eos; + +/// +/// Handles a player that owns a copy of Barotrauma on Steam, +/// and wishes to link their Steam account to an Epic account +/// to interact with friends on Epic Games' account system. +/// +static class EosEpicSecondaryLogin +{ + public enum ProbeResult + { + NoAccount, + LinkedExternalAccountsButNoPuid, + LoggedIn + } + + public static async Task ProbeLinkedEpicAccount() + { + var loginResult = await EosInterface.Login.LoginEpicWithLinkedSteamAccount(EosInterface.Login.LoginEpicFlags.FailWithoutOpeningBrowser); + if (!loginResult.TryUnwrapSuccess(out var success)) { return ProbeResult.NoAccount; } + + if (success.TryGet(out EosInterface.ProductUserId _)) + { + // Make Steam account the primary external account just in case + await EosInterface.Login.LoginSteam(); + + return ProbeResult.LoggedIn; + } + if (success.TryGet(out EosInterface.EosConnectContinuanceToken? _)) + { + return ProbeResult.LinkedExternalAccountsButNoPuid; + } + + return ProbeResult.NoAccount; + } + + public enum LoginErrorDesc + { + NoPrimaryPuid, + + FailedToLogInViaLinkedEpicAccount, + + FailedToForceSteamAsPrimaryExternalAccountId, + SteamIsNoLongerLinkedToPuid, + SteamPuidMismatchedPreviousPrimaryPuid, + + FailedToLinkSteamAccountToEpicAccount, + FailedToCreatePuidForEpicAccount, + + UnhandledErrorCondition + } + + public readonly record struct LoginError( + LoginErrorDesc ErrorDesc, + Option LoginEosConnectError = default, + Option LinkExternalToEpicError = default, + Option CreatePuidError = default) + { + public override string ToString() + { + string error = $"LoginError ({ErrorDesc}"; + if (LoginEosConnectError.TryUnwrap(out var connectError)) + { + error += $", {connectError}"; + } + if (LinkExternalToEpicError.TryUnwrap(out var externalToEpicError)) + { + error += $", {externalToEpicError}"; + } + if (CreatePuidError.TryUnwrap(out var createPuidError)) + { + error += $", {createPuidError}"; + } + return error + ")"; + } + } + + public static async Task> LoginToLinkedEpicAccount() + { + var primaryPuidOption = EosInterface.IdQueries.GetLoggedInPuids().FirstOrNone(); + if (!primaryPuidOption.TryUnwrap(out var primaryPuid)) { return Result.Failure(new LoginError(LoginErrorDesc.NoPrimaryPuid)); } + + // No matter what happens, refresh account IDs when returning + using var janitor = Janitor.Start(); + janitor.AddAction(static () => EosAccount.RefreshSelfAccountIds()); + + async Task> makeSteamPrimaryExternalAccount() + { + // By logging into EOS connect via Steam, we make sure that it's + // treated as the primary external account, which means that it's + // prioritized over the Epic account ID in other EOS functions + var loginSteamResult = await EosInterface.Login.LoginSteam(); + if (!loginSteamResult.TryUnwrapSuccess(out var loginSteamSuccess)) + { + return Result.Failure(new LoginError(LoginErrorDesc.FailedToForceSteamAsPrimaryExternalAccountId)); + } + + if (!loginSteamSuccess.TryGet(out EosInterface.ProductUserId primaryPuidAgain)) + { + return Result.Failure(new LoginError(LoginErrorDesc.SteamIsNoLongerLinkedToPuid)); + } + + if (primaryPuid != primaryPuidAgain) + { + return Result.Failure(new LoginError(LoginErrorDesc.SteamPuidMismatchedPreviousPrimaryPuid)); + } + + return Result.Success(Unit.Value); + } + + const int MaxLoginPasses = 5; + for (int loginPass = 0; loginPass < MaxLoginPasses; loginPass++) + { + // Try to log into EOS via Epic via Steam several times, + // only stop once we get a PUID that's linked only to the Epic account + + if (EosInterface.IdQueries.GetLoggedInEpicIds() is { Length: > 0 } loggedInEpicIds) + { + // Log out of any Epic accounts to reduce chances of ending up in an inconsistent state + await Task.WhenAll(loggedInEpicIds.Select(EosInterface.Login.LogoutEpicAccount)); + } + + var loginResult = await EosInterface.Login.LoginEpicWithLinkedSteamAccount( + loginPass == 0 + ? EosInterface.Login.LoginEpicFlags.None + : EosInterface.Login.LoginEpicFlags.FailWithoutOpeningBrowser); + + if (loginResult.TryUnwrapFailure(out var loginEpicFailure)) + { + return Result.Failure(new LoginError(LoginErrorDesc.FailedToLogInViaLinkedEpicAccount, LoginEosConnectError: Option.Some(loginEpicFailure))); + } + if (!loginResult.TryUnwrapSuccess(out var loginEpicSuccess)) + { + throw new UnreachableCodeException(); + } + + if (loginEpicSuccess.TryGet(out EosInterface.ProductUserId secondPuid)) + { + if (primaryPuid == secondPuid) + { + // Somehow we've got two external accounts linked + // to the same PUID, let's yank them apart + + // Given that the latest ID used to log into this account was + // the Epic account, this call to UnlinkExternalAccount will + // keep the SteamID linked to this PUID and only unlink the + // Epic account, which is what we want. + await EosInterface.Login.UnlinkExternalAccount(secondPuid); + + // Once that's done, log back into EOS Connect with + // the SteamID as the primary ID + if ((await makeSteamPrimaryExternalAccount()).TryUnwrapFailure(out var loginSteamError)) + { + return Result.Failure(loginSteamError); + } + } + else + { + // We already have a PUID for this Epic account. We're done here! + + return Result.Success(Unit.Value); + } + } + else if (loginEpicSuccess.TryGet(out EosInterface.EgsAuthContinuanceToken? egsAuthContinuanceToken)) + { + // We got an EGS Auth continuance token, which means that the player + // has provided an Epic account to link the current Steam account to. + var linkExternalToEpicResult = await EosInterface.Login.LinkExternalAccountToEpicAccount(egsAuthContinuanceToken); + if (linkExternalToEpicResult.TryUnwrapFailure(out var linkExternalToEpicError)) + { + return Result.Failure(new LoginError(LoginErrorDesc.FailedToLinkSteamAccountToEpicAccount, LinkExternalToEpicError: Option.Some(linkExternalToEpicError))); + } + } + else if (loginEpicSuccess.TryGet(out EosInterface.EosConnectContinuanceToken? eosConnectContinuanceToken)) + { + // We got an EOS Connect continuance token, we need + // a new Product User ID for the given Epic account. + + var createPuidResult = await EosInterface.Login.CreateProductAccount(eosConnectContinuanceToken); + if (createPuidResult.TryUnwrapFailure(out var createPuidError)) + { + return Result.Failure(new LoginError(LoginErrorDesc.FailedToCreatePuidForEpicAccount, CreatePuidError: Option.Some(createPuidError))); + } + + // PUID has been created! We're done here! + return Result.Success(Unit.Value); + } + else + { + throw new UnreachableCodeException(); + } + } + + return Result.Failure(new LoginError(LoginErrorDesc.UnhandledErrorCondition)); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Eos/PrimaryLogin/Steam/EosSteamPrimaryLogin.cs b/Barotrauma/BarotraumaClient/ClientSource/Eos/PrimaryLogin/Steam/EosSteamPrimaryLogin.cs new file mode 100644 index 000000000..f65a38331 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Eos/PrimaryLogin/Steam/EosSteamPrimaryLogin.cs @@ -0,0 +1,321 @@ +#nullable enable +using Barotrauma.Steam; +using Microsoft.Xna.Framework; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace Barotrauma.Eos; + +/// +/// Handles a player that owns a copy of Barotrauma on Steam (therefore they +/// will use their SteamID as their primary identity) logging into EOS. +/// +public static class EosSteamPrimaryLogin +{ + public static bool IsNewEosPlayer = false; + + public enum CrossplayChoice + { + Unknown, + Enabled, + Disabled + } + + public static CrossplayChoice EnableCrossplay + { + get => GameSettings.CurrentConfig.CrossplayChoice; + set + { + GameSettings.SetCurrentConfig(GameSettings.CurrentConfig with { CrossplayChoice = value }); + GameAnalyticsManager.AddDesignEvent("Crossplay:" + value); + GameSettings.SaveCurrentConfig(); + } + } + + public static void Start() + { + TaskPool.Add( + "EosSteamPrimaryLogin", + Initialize(), + OnTaskComplete); + } + + private static void OnTaskComplete(Task t) + { + if (t.Exception?.GetInnermost() is { } exception) + { + DebugConsole.ThrowError($"{nameof(EosSteamPrimaryLogin)}.{nameof(Initialize)} failed with exception {exception.Message} {exception.StackTrace.CleanupStackTrace()}"); + } + if (!t.TryGetResult(out Action? action)) { return; } + action(); + } + + private static void Success() + { + Eos.EosAccount.CloseMessageBox(); + Eos.EosAccount.RefreshSelfAccountIds(); + EosAccount.OnLoginSuccess(); + } + + private static async Task Initialize() + { + static void retry() => Start(); + static void cancel() => EosInterface.Core.CleanupAndQuit(); + var failedToInitializeIntro = TextManager.Get("EosFailedToInitialize"); + + if (EnableCrossplay is CrossplayChoice.Unknown) + { + // Don't even try to initialize EOS until we get the user's consent + return SteamAccountHasNoLinkedPuid(); + } + if (EnableCrossplay is CrossplayChoice.Disabled) + { + // Crossplay is disabled, return immediately + return Success; + } + + if (!SteamManager.IsInitialized) + { + return EosAccount.RetryAction(failedToInitializeIntro, "Steamworks is not initialized", retry, cancel); + } + + Result initResult = Result.Failure(EosInterface.Core.InitError.UnhandledErrorCondition); + CrossThread.RequestExecutionOnMainThread(() => initResult = EosInterface.Core.Init(EosInterface.ApplicationCredentials.Client, enableOverlay: false)); + if (initResult.TryUnwrapFailure(out var initError)) + { + return EosAccount.RetryAction(failedToInitializeIntro, GetErrorMessage(initError), retry, cancel); + } + + var steamPuidResult = await EosInterface.Login.LoginSteam(); + + if (!steamPuidResult.TryUnwrapSuccess(out var steamPuidOrContToken)) + { + return EosAccount.RetryAction(failedToInitializeIntro, $"Failed to log into EOS with Steam account: {steamPuidResult}", retry, cancel); + } + + if (steamPuidOrContToken.TryGet(out EosInterface.ProductUserId puid)) + { + return await SteamAccountHasLinkedPuid(puid); + } + else if (steamPuidOrContToken.TryGet(out EosInterface.EosConnectContinuanceToken _)) + { + return SteamAccountHasNoLinkedPuid(); + } + throw new UnreachableCodeException(); + } + + private static async Task SteamAccountHasLinkedPuid(EosInterface.ProductUserId _) + { + await EosEpicSecondaryLogin.ProbeLinkedEpicAccount(); + return Success; + } + + private static Action SteamAccountHasNoLinkedPuid() + { + return () => GameMain.ExecuteAfterContentFinishedLoading(AskPlayerToEnableCrossplay); + } + + private static void AskPlayerToEnableCrossplay() + { + LocalizedString[] options = + { + TextManager.Get("EnableCrossplay"), + TextManager.Get("DisableCrossplay") + }; + + var introText = "\n" + LocalizedString.Join( + "\n\n", + Enumerable.Range(0, 3).Select(static i => TextManager.Get($"EosIntro{i}"))) + "\n"; + + GUIMessageBox msgBox = new GUIMessageBox( + headerText: TextManager.Get("EosIntroHeader"), + text: introText, + Array.Empty(), + relativeSize: (0.8f, 0.5f)); + msgBox.Content.ChildAnchor = Anchor.TopCenter; + msgBox.Content.Stretch = true; + msgBox.InnerFrame.RectTransform.ScaleBasis = ScaleBasis.Smallest; + + int? selectedRadioButton = null; + var radioButtonLayout = new GUILayoutGroup(new RectTransform(Vector2.One, msgBox.Content.RectTransform)) { Stretch = true }; + var radioButtonGroup = new GUIRadioButtonGroup(); + for (int i = 0; i < options.Length; i++) + { + var radioButton = new GUITickBox( + new RectTransform(Vector2.One, radioButtonLayout.RectTransform), + label: options[i], + style: "GUIRadioButton"); + radioButtonGroup.AddRadioButton( + key: i, + radioButton: radioButton); + radioButton.RectTransform.MinSize = Point.Zero; + radioButton.RectTransform.MaxSize = new Point(int.MaxValue); + radioButton.RectTransform.ScaleBasis = ScaleBasis.Normal; + radioButton.RectTransform.RelativeSize = Vector2.One; + } + + //spacing + new GUIFrame(new RectTransform(new Point(0), radioButtonLayout.RectTransform) { MinSize = new Point(0, GUI.IntScale(30)) }, style: null); + + var submitButton = new GUIButton(new RectTransform(Vector2.One, radioButtonLayout.RectTransform), + TextManager.Get("Submit").Fallback("Submit")) { Enabled = false }; + + radioButtonGroup.OnSelect = (rbg, val) => + { + selectedRadioButton = val; + submitButton.Enabled = true; + }; + msgBox.ForceLayoutRecalculation(); + var maxOptionWidth = options.Select(o => GUIStyle.Font.MeasureString(o).X).Max(); + int extraWidth = (int)(GUIStyle.Font.LineHeight * 4f); + radioButtonLayout.RectTransform.IsFixedSize = true; + radioButtonLayout.RectTransform.NonScaledSize = new Point((int)maxOptionWidth + extraWidth, (int)(GUIStyle.Font.LineHeight * options.Length * 1.5f) + submitButton.Rect.Height * 2); + msgBox.ForceLayoutRecalculation(); + + static void textSizeFixHack(GUITextBlock textBlock, int width) + { + textBlock.RectTransform.IsFixedSize = true; + textBlock.RectTransform.MinSize = Point.Zero; + textBlock.RectTransform.MaxSize = new Point(int.MaxValue); + textBlock.RectTransform.NonScaledSize = new Point(width, 0); + textBlock.CalculateHeightFromText(); + } + textSizeFixHack(msgBox.Header, (int)(msgBox.InnerFrame.Rect.Width * 0.9f)); + textSizeFixHack(msgBox.Text, (int)(msgBox.InnerFrame.Rect.Width * 0.9f)); + msgBox.ForceLayoutRecalculation(); + + msgBox.InnerFrame.RectTransform.IsFixedSize = true; + msgBox.InnerFrame.RectTransform.NonScaledSize = new Point( + msgBox.InnerFrame.Rect.Width, + (int)((msgBox.Content.Children.Select(c => c.Rect.Height + GUI.IntScale(5)).Sum() + GUIStyle.Font.LineHeight) / 0.9f)); + + submitButton.OnClicked = delegate + { + switch (selectedRadioButton) + { + case 0: + PlayerWantsToEnableCrossplay(); + return false; + case 1: + PlayerWantsToDisableCrossplay(); + return false; + default: + throw new UnreachableCodeException(); + } + }; + + Eos.EosAccount.ReplaceMessageBox(msgBox); + } + + private static void PlayerWantsToEnableCrossplay() + { + Eos.EosAccount.CreateLoadingMessageBox(); + TaskPool.Add( + nameof(EnableCrossplayAndCreatePuidWithOneToken), + EnableCrossplayAndCreatePuidWithOneToken(), + OnTaskComplete); + } + + private static void PlayerWantsToDisableCrossplay() + { + EosInterface.Core.CleanupAndQuit(); + var action = DisableCrossplay(); + action(); + } + + private static async Task EnableCrossplayAndCreatePuidWithOneToken() + { + void retry() => PlayerWantsToEnableCrossplay(); + static void cancel() => EosInterface.Core.CleanupAndQuit(); + var failedToCreatePuidIntro = TextManager.Get("FailedToCreatePuid"); + + EnableCrossplay = CrossplayChoice.Enabled; + + if (!SteamManager.IsInitialized) + { + return EosAccount.RetryAction(failedToCreatePuidIntro, "Steamworks is not initialized", retry, cancel); + } + + Result initResult = Result.Failure(EosInterface.Core.InitError.UnhandledErrorCondition); + CrossThread.RequestExecutionOnMainThread(() => initResult = EosInterface.Core.Init(EosInterface.ApplicationCredentials.Client, enableOverlay: false)); + if (initResult.TryUnwrapFailure(out var initError)) + { + return EosAccount.RetryAction(failedToCreatePuidIntro, GetErrorMessage(initError), retry, cancel); + } + + EosInterface.EosConnectContinuanceToken steamEosContinuanceToken; + var steamLoginResult = await EosInterface.Login.LoginSteam(); + if (steamLoginResult.TryUnwrapSuccess(out var either)) + { + if (either.TryGet(out EosInterface.EosConnectContinuanceToken newSteamCt)) + { + steamEosContinuanceToken = newSteamCt; + } + else + { + await EosEpicSecondaryLogin.ProbeLinkedEpicAccount(); + SocialOverlay.Instance?.DisplayBindHintToPlayer(); + return Success; + } + } + else + { + return EosAccount.RetryAction(failedToCreatePuidIntro, $"Failed to refresh continuance token: {steamLoginResult}", retry, cancel); + } + + var newPuidResult = await EosInterface.Login.CreateProductAccount(steamEosContinuanceToken); + if (newPuidResult.IsFailure) + { + return EosAccount.RetryAction(failedToCreatePuidIntro, $"Failed to create PUID: {newPuidResult}", retry, cancel); + } + + IsNewEosPlayer = true; + SocialOverlay.Instance?.DisplayBindHintToPlayer(); + return Success; + } + + private static LocalizedString GetErrorMessage(EosInterface.Core.InitError errorCode) + { + return TextManager.Get($"EosInterface.Core.InitError.{errorCode}").Fallback($"Failed to initialize Epic Online Services (error code {errorCode})"); + } + + private static Action DisableCrossplay() + { + EnableCrossplay = CrossplayChoice.Disabled; + + return Success; + } + + public static void HandleCrossplayChoiceChange(CrossplayChoice newChoice) + { + if (StoreIntegration.CurrentStore != StoreIntegration.Store.Steam) { return; } + if (GameSettings.CurrentConfig.CrossplayChoice == newChoice) { return; } + + switch (newChoice) + { + case CrossplayChoice.Disabled: + EosInterface.Core.CleanupAndQuit(); + break; + case CrossplayChoice.Enabled: + if (EosInterface.Core.CurrentStatus == EosInterface.Core.Status.ShutDown) + { + var msgBox = new GUIMessageBox(TextManager.Get("EosAllowCrossplay"), + TextManager.Get("RestartRequiredGeneric"), new[] { TextManager.Get("ok") }) + { + DrawOnTop = true + }; + msgBox.Buttons[0].OnClicked = (_, _) => + { + msgBox.Close(); + return true; + }; + } + else + { + PlayerWantsToEnableCrossplay(); + } + break; + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs b/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs index 4afb04ff2..5a41e279f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs @@ -65,25 +65,11 @@ namespace Barotrauma private int texDims; private uint baseChar; - private readonly struct GlyphData - { - public readonly int TexIndex; - public readonly Vector2 DrawOffset; - public readonly float Advance; - public readonly Rectangle TexCoords; - - public GlyphData( - int texIndex = default, - Vector2 drawOffset = default, - float advance = default, - Rectangle texCoords = default) - { - TexIndex = texIndex; - DrawOffset = drawOffset; - Advance = advance; - TexCoords = texCoords; - } - } + public readonly record struct GlyphData( + int TexIndex = default, + Vector2 DrawOffset = default, + float Advance = default, + Rectangle TexCoords = default); public static TextManager.SpeciallyHandledCharCategory ExtractShccFromXElement(XElement element) => TextManager.SpeciallyHandledCharCategories @@ -94,7 +80,7 @@ namespace Barotrauma // For backwards compatibility, we assume that Cyrillic is supported by default TextManager.SpeciallyHandledCharCategory.Cyrillic => true, - + _ => throw new NotImplementedException($"nameof{category} not implemented.") })) .Aggregate(TextManager.SpeciallyHandledCharCategory.None, (current, category) => current | category); @@ -209,8 +195,8 @@ namespace Barotrauma if (glyphIndex == 0) { texCoords.Add(j, new GlyphData( - advance: 0, - texIndex: -1)); + Advance: 0, + TexIndex: -1)); continue; } face.LoadGlyph(glyphIndex, LoadFlags.Default, LoadTarget.Normal); @@ -218,8 +204,8 @@ namespace Barotrauma { //glyph is empty, but char might still apply advance GlyphData blankData = new GlyphData( - advance: Math.Max((float)face.Glyph.Metrics.HorizontalAdvance, 0f), - texIndex: -1); //indicates no texture because the glyph is empty + Advance: Math.Max((float)face.Glyph.Metrics.HorizontalAdvance, 0f), + TexIndex: -1); //indicates no texture because the glyph is empty texCoords.Add(j, blankData); continue; @@ -262,10 +248,10 @@ namespace Barotrauma } GlyphData newData = new GlyphData( - advance: (float)face.Glyph.Metrics.HorizontalAdvance, - texIndex: texIndex, - texCoords: new Rectangle((int)currentCoords.X, (int)currentCoords.Y, glyphWidth, glyphHeight), - drawOffset: new Vector2(face.Glyph.BitmapLeft, baseHeight * 14 / 10 - face.Glyph.BitmapTop) + Advance: (float)face.Glyph.Metrics.HorizontalAdvance, + TexIndex: texIndex, + TexCoords: new Rectangle((int)currentCoords.X, (int)currentCoords.Y, glyphWidth, glyphHeight), + DrawOffset: new Vector2(face.Glyph.BitmapLeft, baseHeight * 14 / 10 - face.Glyph.BitmapTop) ); texCoords.Add(j, newData); @@ -354,8 +340,8 @@ namespace Barotrauma if (glyphIndex == 0) { texCoords.Add(character, new GlyphData( - advance: 0, - texIndex: -1)); + Advance: 0, + TexIndex: -1)); continue; } @@ -365,8 +351,8 @@ namespace Barotrauma { //glyph is empty, but char might still apply advance GlyphData blankData = new GlyphData( - advance: Math.Max((float)face.Glyph.Metrics.HorizontalAdvance, 0f), - texIndex: -1); //indicates no texture because the glyph is empty + Advance: Math.Max((float)face.Glyph.Metrics.HorizontalAdvance, 0f), + TexIndex: -1); //indicates no texture because the glyph is empty texCoords.Add(character, blankData); continue; } @@ -403,10 +389,10 @@ namespace Barotrauma } GlyphData newData = new GlyphData( - advance: (float)horizontalAdvance, - texIndex: textures.Count - 1, - texCoords: new Rectangle((int)currentDynamicAtlasCoords.X, (int)currentDynamicAtlasCoords.Y, glyphWidth, glyphHeight), - drawOffset: drawOffset + Advance: (float)horizontalAdvance, + TexIndex: textures.Count - 1, + TexCoords: new Rectangle((int)currentDynamicAtlasCoords.X, (int)currentDynamicAtlasCoords.Y, glyphWidth, glyphHeight), + DrawOffset: drawOffset ); texCoords.Add(character, newData); @@ -490,7 +476,7 @@ namespace Barotrauma return gd; } - return new GlyphData(texIndex: -1); + return new GlyphData(TexIndex: -1); } public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth, Alignment alignment = Alignment.TopLeft, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit) @@ -814,14 +800,22 @@ namespace Barotrauma { Vector2 retVal = Vector2.Zero; retVal.Y = LineHeight; + + var (gd, _) = GetGlyphDataAndTextureForChar(c); + retVal.X = gd.Advance; + return retVal; + } + + public (GlyphData GlyphData, Texture2D Texture) GetGlyphDataAndTextureForChar(char c) + { if (DynamicLoading && !texCoords.ContainsKey(c)) { DynamicRenderAtlas(graphicsDevice, c); } GlyphData gd = GetGlyphData(c); - retVal.X = gd.Advance; - return retVal; + var tex = gd.TexIndex >= 0 ? textures[gd.TexIndex] : null; + return (gd, tex); } public void Dispose() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index 4a1bee7c4..cc59e8d2b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -199,19 +199,15 @@ namespace Barotrauma public static bool PauseMenuOpen { get; private set; } - public static bool InputBlockingMenuOpen - { - get - { - return PauseMenuOpen || - SettingsMenuOpen || - DebugConsole.IsOpen || - GameSession.IsTabMenuOpen || - GameMain.GameSession?.GameMode is { Paused: true } || - CharacterHUD.IsCampaignInterfaceOpen || - GameMain.GameSession?.Campaign is { SlideshowPlayer: { Finished: false, Visible: true } }; - } - } + public static bool InputBlockingMenuOpen => + PauseMenuOpen + || SettingsMenuOpen + || SocialOverlay.Instance is { IsOpen: true } + || DebugConsole.IsOpen + || GameSession.IsTabMenuOpen + || GameMain.GameSession?.GameMode is { Paused: true } + || CharacterHUD.IsCampaignInterfaceOpen + || GameMain.GameSession?.Campaign is { SlideshowPlayer: { Finished: false, Visible: true } }; public static bool PreventPauseMenuToggle = false; @@ -882,25 +878,30 @@ namespace Barotrauma private static void HandlePersistingElements(float deltaTime) { - lock (mutex) + bool currentMessageBoxIsVerificationPrompt = GUIMessageBox.VisibleBox is GUIMessageBox { DrawOnTop: true }; + + if (!currentMessageBoxIsVerificationPrompt) { GUIMessageBox.AddActiveToGUIUpdateList(); - GUIContextMenu.AddActiveToGUIUpdateList(); + } - if (PauseMenuOpen) - { - PauseMenu.AddToGUIUpdateList(); - } - if (SettingsMenuOpen) - { - SettingsMenuContainer.AddToGUIUpdateList(); - } + if (SettingsMenuOpen) + { + SettingsMenuContainer.AddToGUIUpdateList(); + } + else if (PauseMenuOpen) + { + PauseMenu.AddToGUIUpdateList(); + } - //the "are you sure you want to quit" prompts are drawn on top of everything else - if (GUIMessageBox.VisibleBox?.UserData as string == "verificationprompt" || GUIMessageBox.VisibleBox?.UserData as string == "bugreporter") - { - GUIMessageBox.VisibleBox.AddToGUIUpdateList(); - } + SocialOverlay.Instance?.AddToGuiUpdateList(); + + GUIContextMenu.AddActiveToGUIUpdateList(); + + //the "are you sure you want to quit" prompts are drawn on top of everything else + if (currentMessageBoxIsVerificationPrompt) + { + GUIMessageBox.VisibleBox.AddToGUIUpdateList(); } } @@ -2560,7 +2561,8 @@ namespace Barotrauma var msgBox = new GUIMessageBox("", TextManager.Get(textTag), new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }) { - UserData = "verificationprompt" + UserData = "verificationprompt", + DrawOnTop = true }; msgBox.Buttons[0].OnClicked = (_, __) => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs index a824218e5..c5e1a2b81 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs @@ -168,7 +168,7 @@ namespace Barotrauma public override bool PlaySoundOnSelect { get; set; } = true; - public GUIButton(RectTransform rectT, Alignment textAlignment = Alignment.Center, string style = "", Color? color = null) : this(rectT, new RawLString(""), textAlignment, style, color) { } + public GUIButton(RectTransform rectT, Alignment textAlignment = Alignment.Center, string style = "", Color? color = null) : this(rectT, LocalizedString.EmptyString, textAlignment, style, color) { } public GUIButton(RectTransform rectT, LocalizedString text, Alignment textAlignment = Alignment.Center, string style = "", Color? color = null) : base(style, rectT) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIColorPicker.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIColorPicker.cs index 4cc0ee1e7..26c441476 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIColorPicker.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIColorPicker.cs @@ -159,7 +159,7 @@ namespace Barotrauma SelectedValue = Math.Clamp(1f - (y / mainArea.Height), 0, 1); } - CurrentColor = ToolBox.HSVToRGB(SelectedHue, SelectedSaturation, SelectedValue); + CurrentColor = ToolBoxCore.HSVToRGB(SelectedHue, SelectedSaturation, SelectedValue); OnColorSelected?.Invoke(this, CurrentColor); } @@ -201,7 +201,7 @@ namespace Barotrauma } } - private Color DrawHVArea(float x, float y) => ToolBox.HSVToRGB(SelectedHue, x, 1.0f - y); - private Color DrawHueArea(float x, float y) => ToolBox.HSVToRGB(y * 360f, 1f, 1f); + private Color DrawHVArea(float x, float y) => ToolBoxCore.HSVToRGB(SelectedHue, x, 1.0f - y); + private Color DrawHueArea(float x, float y) => ToolBoxCore.HSVToRGB(y * 360f, 1f, 1f); } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index 762c4053c..d26b86738 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -8,8 +8,7 @@ using System.Xml.Linq; using Barotrauma.IO; using RestSharp; using System.Net; -using System.Collections.Immutable; -using Barotrauma.Tutorials; +using Barotrauma.Steam; namespace Barotrauma { @@ -1174,11 +1173,14 @@ namespace Barotrauma { try { -#if USE_STEAM - Steam.SteamManager.OverlayCustomUrl(url); -#else - ToolBox.OpenFileWithShell(url); -#endif + if (SteamManager.IsInitialized) + { + SteamManager.OverlayCustomUrl(url); + } + else + { + ToolBox.OpenFileWithShell(url); + } } catch (Exception e) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs index 3e9166fcd..bc59543f9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs @@ -76,12 +76,12 @@ namespace Barotrauma if (hasHeader) { - InflateSize(ref estimatedSize, header, headerFont); + InflateSize(ref estimatedSize, header, headerFont!); } foreach (ContextMenuOption option in options) { - Vector2 optionSize = InflateSize(ref estimatedSize, option.Label, font); + Vector2 optionSize = InflateSize(ref estimatedSize, option.Label, font!); optionsAndSizes.Add(option, optionSize); } @@ -104,7 +104,7 @@ namespace Barotrauma if (hasHeader) { Point sz = Point.Zero; - InflateSize(ref sz, header, headerFont); + InflateSize(ref sz, header, headerFont!); listSize.Y -= sz.Y; HeaderLabel = new GUITextBlock(new RectTransform(sz, background.RectTransform), header, font: headerFont) { Padding = headerPadding }; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs index 8a795ccd2..5185b7b63 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs @@ -168,7 +168,7 @@ namespace Barotrauma public GUIDropDown(RectTransform rectT, LocalizedString text = null, int elementCount = 4, string style = "", bool selectMultiple = false, bool dropAbove = false, Alignment textAlignment = Alignment.CenterLeft) : base(style, rectT) { - text ??= new RawLString(""); + text ??= LocalizedString.EmptyString; HoverCursor = CursorState.Hand; CanBeFocused = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs index d21951cc1..5ca757417 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs @@ -205,5 +205,11 @@ namespace Barotrauma Recalculate(); } } + + public override void ForceLayoutRecalculation() + { + Recalculate(); + base.ForceLayoutRecalculation(); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs index de557c274..c340cf1ad 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs @@ -86,6 +86,11 @@ namespace Barotrauma public Type MessageBoxType => type; + /// + /// If enabled, the box is always drawn in front of all other elements. + /// + public bool DrawOnTop; + public static GUIComponent VisibleBox => MessageBoxes.LastOrDefault(); public GUIMessageBox(LocalizedString headerText, LocalizedString text, Vector2? relativeSize = null, Point? minSize = null, Type type = Type.Default) @@ -477,13 +482,13 @@ namespace Barotrauma for (int i = 0; i < MessageBoxes.Count; i++) { if (MessageBoxes[i] == null) { continue; } - if (!(MessageBoxes[i] is GUIMessageBox messageBox)) + if (MessageBoxes[i] is not GUIMessageBox messageBox) { if (type == Type.Default) { // Message box not of type GUIMessageBox is likely the round summary MessageBoxes[i].AddToGUIUpdateList(); - if (!(MessageBoxes[i].UserData is RoundSummary)) { break; } + if (MessageBoxes[i].UserData is not RoundSummary) { break; } } continue; } @@ -494,8 +499,7 @@ namespace Barotrauma } // These are handled separately in GUI.HandlePersistingElements() - if (MessageBoxes[i].UserData as string == "verificationprompt") { continue; } - if (MessageBoxes[i].UserData as string == "bugreporter") { continue; } + if (messageBox.DrawOnTop) { continue; } messageBox.AddToGUIUpdateList(); break; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs index d1d4a470f..3080c482e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs @@ -1,4 +1,5 @@ -using Barotrauma.Extensions; +#nullable enable +using Barotrauma.Extensions; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; @@ -54,8 +55,8 @@ namespace Barotrauma public class GUIFontPrefab : GUIPrefab { private readonly ContentXElement element; - private ScalableFont font; - public ScalableFont Font + private ScalableFont? font; + public ScalableFont? Font { get { @@ -64,11 +65,13 @@ namespace Barotrauma } } - private ImmutableDictionary specialHandlingFonts; + private ImmutableDictionary? specialHandlingFonts; - public ScalableFont GetFontForCategory(TextManager.SpeciallyHandledCharCategory category) + public ScalableFont? GetFontForCategory(TextManager.SpeciallyHandledCharCategory category) { if (Language != GameSettings.CurrentConfig.Language) { LoadFont(); } + if (font is null) { return null; } + if (specialHandlingFonts is null) { return font; } if (font.SpeciallyHandledCharCategory.HasFlag(category)) { return font; } return specialHandlingFonts.TryGetValue(category, out var resultFont) ? resultFont @@ -87,7 +90,7 @@ namespace Barotrauma public void LoadFont() { - string fontPath = GetFontFilePath(element); + string? fontPath = GetFontFilePath(element); uint size = GetFontSize(element); bool dynamicLoading = GetFontDynamicLoading(element); var shcc = GetShcc(element); @@ -125,21 +128,21 @@ namespace Barotrauma specialHandlingFonts = null; } - private ScalableFont ExtractFont(TextManager.SpeciallyHandledCharCategory flag, ContentXElement element) + private ScalableFont? ExtractFont(TextManager.SpeciallyHandledCharCategory flag, ContentXElement element) { foreach (var subElement in element.Elements().Reverse()) { if (subElement.NameAsIdentifier() != "override") { continue; } if (ScalableFont.ExtractShccFromXElement(subElement).HasFlag(flag)) { - return new ScalableFont(subElement, font.Size, GameMain.Instance.GraphicsDevice); + return new ScalableFont(subElement, font?.Size ?? 14, GameMain.Instance.GraphicsDevice); } } ScalableFont hardcodedFallback(string path) => new ScalableFont( path, - font.Size, + font?.Size ?? 0, GameMain.Instance.GraphicsDevice, dynamicLoading: true, speciallyHandledCharCategory: flag); @@ -154,7 +157,7 @@ namespace Barotrauma }; } - private string GetFontFilePath(ContentXElement element) + private string? GetFontFilePath(ContentXElement element) { foreach (var subElement in element.Elements()) { @@ -227,20 +230,20 @@ namespace Barotrauma { public GUIFont(string identifier) : base(identifier) { } - public bool HasValue => !Prefabs.IsEmpty; - - public ScalableFont Value => Prefabs.ActivePrefab.Font; + public bool HasValue => Value is not null; - public static implicit operator ScalableFont(GUIFont reference) => reference.Value; + public ScalableFont? Value => Prefabs.ActivePrefab?.Font; + + public static implicit operator ScalableFont?(GUIFont reference) => reference.Value; public bool ForceUpperCase => Prefabs.ActivePrefab?.Font is { ForceUpperCase: true }; - public uint Size => HasValue ? Value.Size : 0; + public uint Size => Value?.Size ?? 0; - private ScalableFont GetFontForStr(LocalizedString str) => GetFontForStr(str.Value); + private ScalableFont? GetFontForStr(LocalizedString str) => GetFontForStr(str.Value); - public ScalableFont GetFontForStr(string str) => - Prefabs.ActivePrefab.GetFontForCategory(TextManager.GetSpeciallyHandledCategories(str)); + public ScalableFont? GetFontForStr(string str) => + Prefabs.ActivePrefab?.GetFontForCategory(TextManager.GetSpeciallyHandledCategories(str)); public void DrawString(SpriteBatch sb, LocalizedString text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects spriteEffects, float layerDepth) { @@ -249,7 +252,7 @@ namespace Barotrauma public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects spriteEffects, float layerDepth) { - GetFontForStr(text).DrawString(sb, text, position, color, rotation, origin, scale, spriteEffects, layerDepth); + GetFontForStr(text)?.DrawString(sb, text, position, color, rotation, origin, scale, spriteEffects, layerDepth); } public void DrawString(SpriteBatch sb, LocalizedString text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects spriteEffects, float layerDepth, Alignment alignment = Alignment.TopLeft) @@ -259,7 +262,7 @@ namespace Barotrauma public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects spriteEffects, float layerDepth, Alignment alignment = Alignment.TopLeft, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit) { - GetFontForStr(text).DrawString(sb, text, position, color, rotation, origin, scale, spriteEffects, layerDepth, alignment, forceUpperCase); + GetFontForStr(text)?.DrawString(sb, text, position, color, rotation, origin, scale, spriteEffects, layerDepth, alignment, forceUpperCase); } public void DrawString(SpriteBatch sb, LocalizedString text, Vector2 position, Color color, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit, bool italics = false) @@ -269,34 +272,46 @@ namespace Barotrauma public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit, bool italics = false) { - GetFontForStr(text).DrawString(sb, text, position, color, forceUpperCase, italics); + GetFontForStr(text)?.DrawString(sb, text, position, color, forceUpperCase, italics); } public void DrawStringWithColors(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects spriteEffects, float layerDepth, in ImmutableArray? richTextData, int rtdOffset = 0, Alignment alignment = Alignment.TopLeft, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit) { - GetFontForStr(text).DrawStringWithColors(sb, text, position, color, rotation, origin, scale, spriteEffects, layerDepth, richTextData, rtdOffset, alignment, forceUpperCase); + GetFontForStr(text)?.DrawStringWithColors(sb, text, position, color, rotation, origin, scale, spriteEffects, layerDepth, richTextData, rtdOffset, alignment, forceUpperCase); } public Vector2 MeasureString(LocalizedString str, bool removeExtraSpacing = false) { - return GetFontForStr(str).MeasureString(str, removeExtraSpacing); + return GetFontForStr(str)?.MeasureString(str, removeExtraSpacing) ?? Vector2.Zero; } public Vector2 MeasureChar(char c) { - return GetFontForStr($"{c}").MeasureChar(c); + return GetFontForStr($"{c}")?.MeasureChar(c) ?? Vector2.Zero; } public string WrapText(string text, float width) - => GetFontForStr(text).WrapText(text, width); + => GetFontForStr(text)?.WrapText(text, width) ?? text; public string WrapText(string text, float width, int requestCharPos, out Vector2 requestedCharPos) - => GetFontForStr(text).WrapText(text, width, requestCharPos, out requestedCharPos); - - public string WrapText(string text, float width, out Vector2[] allCharPositions) - => GetFontForStr(text).WrapText(text, width, out allCharPositions); + { + requestedCharPos = default; + return GetFontForStr(text)?.WrapText(text, width, requestCharPos, out requestedCharPos) ?? text; + } - public float LineHeight => Value.LineHeight; + public string WrapText(string text, float width, out Vector2[] allCharPositions) + { + var scalableFont = GetFontForStr(text); + if (scalableFont != null) + { + return scalableFont.WrapText(text, width, out allCharPositions); + } + + allCharPositions = Enumerable.Range(0, text.Length + 1).Select(_ => Vector2.Zero).ToArray(); + return text; + } + + public float LineHeight => Value?.LineHeight ?? 0; } public class GUIColorPrefab : GUIPrefab @@ -355,19 +370,19 @@ namespace Barotrauma { public GUISprite(string identifier) : base(identifier) { } - public UISprite Value + public UISprite? Value { get { - return Prefabs.ActivePrefab.Sprite; + return Prefabs.ActivePrefab?.Sprite; } } - public static implicit operator UISprite(GUISprite reference) => reference.Value; + public static implicit operator UISprite?(GUISprite reference) => reference.Value; public void Draw(SpriteBatch spriteBatch, Rectangle rect, Color color, SpriteEffects spriteEffects = SpriteEffects.None) { - Value.Draw(spriteBatch, rect, color, spriteEffects); + Value?.Draw(spriteBatch, rect, color, spriteEffects); } } @@ -390,33 +405,33 @@ namespace Barotrauma { public GUISpriteSheet(string identifier) : base(identifier) { } - public SpriteSheet Value + public SpriteSheet? Value { get { - return Prefabs.ActivePrefab.SpriteSheet; + return Prefabs.ActivePrefab?.SpriteSheet; } } - public int FrameCount => Value.FrameCount; - public Point FrameSize => Value.FrameSize; + public int FrameCount => Value?.FrameCount ?? 1; + public Point FrameSize => Value?.FrameSize ?? Point.Zero; public void Draw(ISpriteBatch spriteBatch, Vector2 pos, float rotate = 0, float scale = 1, SpriteEffects spriteEffects = SpriteEffects.None) { - Value.Draw(spriteBatch, pos, rotate, scale, spriteEffects); + Value?.Draw(spriteBatch, pos, rotate, scale, spriteEffects); } public void Draw(ISpriteBatch spriteBatch, Vector2 pos, Color color, Vector2 origin, float rotate = 0, float scale = 1, SpriteEffects spriteEffects = SpriteEffects.None, float? depth = null) { - Value.Draw(spriteBatch, pos, color, origin, rotate, scale, spriteEffects, depth); + Value?.Draw(spriteBatch, pos, color, origin, rotate, scale, spriteEffects, depth); } public void Draw(ISpriteBatch spriteBatch, int spriteIndex, Vector2 pos, Color color, Vector2 origin, float rotate, Vector2 scale, SpriteEffects spriteEffects = SpriteEffects.None, float? depth = null) { - Value.Draw(spriteBatch, spriteIndex, pos, color, origin, rotate, scale, spriteEffects, depth); + Value?.Draw(spriteBatch, spriteIndex, pos, color, origin, rotate, scale, spriteEffects, depth); } - public static implicit operator SpriteSheet(GUISpriteSheet reference) => reference.Value; + public static implicit operator SpriteSheet?(GUISpriteSheet reference) => reference.Value; } public class GUICursorPrefab : GUIPrefab @@ -446,6 +461,6 @@ namespace Barotrauma { public GUICursor(string identifier) : base(identifier) { } - public Sprite this[CursorState k] => Prefabs.ActivePrefab.Sprites[(int)k]; + public Sprite? this[CursorState k] => Prefabs.ActivePrefab?.Sprites[(int)k]; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs index c9b9d0626..4aa48fd6d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs @@ -433,14 +433,7 @@ namespace Barotrauma return Font.MeasureString(" "); } - Vector2 size = Vector2.Zero; - while (size == Vector2.Zero) - { - try { size = Font.MeasureString(string.IsNullOrEmpty(text) ? " " : text); } - catch { text = text.Length > 0 ? text.Substring(0, text.Length - 1) : ""; } - } - - return size; + return Font.MeasureString(string.IsNullOrEmpty(text) ? " " : text); } protected override void SetAlpha(float a) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/VideoPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/VideoPlayer.cs index 6152fa2dc..9be5c0d1f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/VideoPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/VideoPlayer.cs @@ -128,7 +128,7 @@ namespace Barotrauma public void LoadContent(string contentPath, VideoSettings videoSettings, TextSettings textSettings, Identifier contentId, bool startPlayback) { - LoadContent(contentPath, videoSettings, textSettings, contentId, startPlayback, new RawLString(""), null); + LoadContent(contentPath, videoSettings, textSettings, contentId, startPlayback, LocalizedString.EmptyString, null); } public void LoadContent(string contentPath, VideoSettings videoSettings, TextSettings textSettings, Identifier contentId, bool startPlayback, LocalizedString objective, Action onStop = null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs index 7ffcbfde4..1cd2bf977 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs @@ -9,7 +9,7 @@ namespace Barotrauma { static partial void CreateConsentPrompt() { - if (consentTextAvailable) + if (ConsentTextAvailable) { var background = new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, GUI.Canvas), style: "GUIBackgroundBlocker"); var frame = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.7f), background.RectTransform, Anchor.Center) { MinSize = new Point(800, 0), MaxSize = new Point(1500, int.MaxValue) }); @@ -20,8 +20,13 @@ namespace Barotrauma AbsoluteSpacing = GUI.IntScale(15) }; + string consentTextTag = "statisticsconsenttext"; + if (EosInterface.IdQueries.IsLoggedIntoEosConnect) + { + consentTextTag = "statisticsconsenteostext"; + } new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), TextManager.Get("statisticsconsentheader"), font: GUIStyle.SubHeadingFont, textColor: Color.White); - var mainText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), RichString.Rich(TextManager.Get("statisticsconsenttext")), wrap: true); + var mainText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), RichString.Rich(TextManager.Get(consentTextTag)), wrap: true); foreach (var data in mainText.RichTextData) { @@ -30,7 +35,7 @@ namespace Barotrauma Data = data, OnClick = (GUITextBlock component, GUITextBlock.ClickableArea area) => { - GameMain.ShowOpenUrlInWebBrowserPrompt("https://gameanalytics.com/privacy/"); + GameMain.ShowOpenUriPrompt("https://gameanalytics.com/privacy/"); } }); } @@ -70,7 +75,7 @@ namespace Barotrauma } yield return CoroutineStatus.Success; } - + buttonContainerSpacing(0.2f); var noBtn = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonContainer.RectTransform), TextManager.Get("No")); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 50dbc4178..57dd6dea9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -44,7 +44,9 @@ namespace Barotrauma public static readonly Version Version = Assembly.GetEntryAssembly().GetName().Version; - public static string[] ConsoleArguments; + public readonly ImmutableArray ConsoleArguments; + + public readonly Option EgsExchangeCode; public static GameScreen GameScreen; public static MainMenuScreen MainMenuScreen; @@ -214,6 +216,10 @@ namespace Barotrauma public static ChatMode ActiveChatMode { get; set; } = ChatMode.Radio; + private static bool contentLoaded; + + private static readonly Queue postContentLoadActions = new(); + public GameMain(string[] args) { Content.RootDirectory = "Content"; @@ -239,11 +245,13 @@ namespace Barotrauma GameSettings.Init(); CreatureMetrics.Init(); - ConsoleArguments = args; + ConsoleArguments = args.ToImmutableArray(); + + EgsExchangeCode = EosInterface.Login.ParseEgsExchangeCode(args); try { - ConnectCommand = ToolBox.ParseConnectCommand(ConsoleArguments); + ConnectCommand = Barotrauma.Networking.ConnectCommand.Parse(ConsoleArguments); } catch (IndexOutOfRangeException e) { @@ -270,6 +278,16 @@ namespace Barotrauma Window.FileDropped += OnFileDropped; } + public static void ExecuteAfterContentFinishedLoading(Action action) + { + if (contentLoaded) + { + action(); + return; + } + postContentLoadActions.Enqueue(action); + } + public static void OnFileDropped(object sender, FileDropEventArgs args) { if (!(Screen.Selected is { } screen)) { return; } @@ -416,6 +434,8 @@ namespace Barotrauma WaitForLanguageSelection = GameSettings.CurrentConfig.Language == LanguageIdentifier.None }; + Eos.EosAccount.LoginPlatformSpecific(); + initialLoadingThread = new Thread(Load); initialLoadingThread.Start(); } @@ -483,6 +503,7 @@ namespace Barotrauma TextManager.VerifyLanguageAvailable(); + SocialOverlay.Init(); DebugConsole.Init(); ContentPackageManager.LogEnabledRegularPackageErrors(); @@ -514,25 +535,27 @@ namespace Barotrauma TitleScreen.LoadState = 85.0f; -#if USE_STEAM if (SteamManager.IsInitialized) { - Steamworks.SteamFriends.OnGameRichPresenceJoinRequested += OnInvitedToGame; - Steamworks.SteamFriends.OnGameLobbyJoinRequested += OnLobbyJoinRequested; + Steamworks.SteamFriends.OnGameRichPresenceJoinRequested += OnInvitedToSteamGame; + Steamworks.SteamFriends.OnGameLobbyJoinRequested += OnSteamLobbyJoinRequested; if (SteamManager.TryGetUnlockedAchievements(out List achievements)) { //check the achievements too, so we don't consider people who've played the game before this "gamelaunchcount" stat was added as being 1st-time-players //(people who have played previous versions, but not unlocked any achievements, will be incorrectly considered 1st-time-players, but that should be a small enough group to not skew the statistics) - if (!achievements.Any() && SteamManager.GetStatInt("gamelaunchcount".ToIdentifier()) <= 0) + if (!achievements.Any() && SteamManager.GetStatInt(AchievementStat.GameLaunchCount) <= 0) { IsFirstLaunch = true; GameAnalyticsManager.AddDesignEvent("FirstLaunch"); } } - SteamManager.IncrementStat("gamelaunchcount".ToIdentifier(), 1); + SteamManager.IncrementStat(AchievementStat.GameLaunchCount, 1); } -#endif + + + + Eos.EosAccount.ExecuteAfterLogin(ProcessLaunchCountEos); SubEditorScreen = new SubEditorScreen(); TestScreen = new TestScreen(); @@ -564,7 +587,45 @@ namespace Barotrauma TitleScreen.LoadState = 100.0f; HasLoaded = true; + log("LOADING COROUTINE FINISHED"); + + contentLoaded = true; + while (postContentLoadActions.TryDequeue(out Action action)) + { + action(); + } + } + + private static void ProcessLaunchCountEos() + { + if (!EosInterface.Core.IsInitialized) { return; } + + static void trySetConnectCommand(string commandStr) + { + Instance.ConnectCommand = Instance.ConnectCommand.Fallback(Networking.ConnectCommand.Parse(commandStr)); + } + + EosInterface.Presence.OnJoinGame.Register("onJoinGame".ToIdentifier(), static jgi => trySetConnectCommand(jgi.JoinCommand)); + EosInterface.Presence.OnInviteAccepted.Register("onInviteAccepted".ToIdentifier(), static aii => trySetConnectCommand(aii.JoinCommand)); + + TaskPool.AddWithResult("Eos.GameMain.Load.QueryStats", EosInterface.Achievements.QueryStats(AchievementStat.GameLaunchCount), static result => + { + result.Match( + success: static stats => + { + if (!stats.TryGetValue(AchievementStat.GameLaunchCount, out int launchCount)) { return; } + + if (launchCount > 0) { return; } + + IsFirstLaunch = true; + GameAnalyticsManager.AddDesignEvent("FirstLaunch_Epic"); + }, + failure: static error => DebugConsole.ThrowError($"Failed to query stats for launch count: {error}") + ); + + TaskPool.Add("Eos.GameMain.Load.IngestStat", EosInterface.Achievements.IngestStats((AchievementStat.GameLaunchCount, 1)), TaskPool.IgnoredCallback); + }); } /// @@ -581,13 +642,13 @@ namespace Barotrauma MainThread = null; } - public void OnInvitedToGame(Steamworks.Friend friend, string connectCommand) => OnInvitedToGame(connectCommand); + private void OnInvitedToSteamGame(Steamworks.Friend friend, string connectCommand) => OnInvitedToSteamGame(connectCommand); - public void OnInvitedToGame(string connectCommand) + private void OnInvitedToSteamGame(string connectCommand) { try { - ConnectCommand = ToolBox.ParseConnectCommand(ToolBox.SplitCommand(connectCommand)); + ConnectCommand = Barotrauma.Networking.ConnectCommand.Parse(ToolBox.SplitCommand(connectCommand)); } catch (IndexOutOfRangeException e) { @@ -600,7 +661,7 @@ namespace Barotrauma } } - public void OnLobbyJoinRequested(Steamworks.Data.Lobby lobby, Steamworks.SteamId friendId) + private void OnSteamLobbyJoinRequested(Steamworks.Data.Lobby lobby, Steamworks.SteamId friendId) { SteamManager.JoinLobby(lobby.Id, true); } @@ -655,6 +716,8 @@ namespace Barotrauma PlayerInput.Update(Timing.Step); + SocialOverlay.Instance?.Update(); + if (loadingScreenOpen) { //reset accumulator if loading @@ -729,16 +792,16 @@ namespace Barotrauma } MainMenuScreen.Select(); - if (connectCommand.EndpointOrLobby.TryGet(out ulong lobbyId)) + if (connectCommand.SteamLobbyIdOption.TryUnwrap(out var lobbyId)) { - SteamManager.JoinLobby(lobbyId, joinServer: true); + SteamManager.JoinLobby(lobbyId.Value, joinServer: true); } - else if (connectCommand.EndpointOrLobby.TryGet(out ConnectCommand.NameAndEndpoint nameAndEndpoint) - && nameAndEndpoint is { ServerName: var serverName, Endpoint: var endpoint }) + else if (connectCommand.NameAndP2PEndpointsOption.TryUnwrap(out var nameAndEndpoint) + && nameAndEndpoint is { ServerName: var serverName, Endpoints: var endpoints }) { Client = new GameClient(MultiplayerPreferences.Instance.PlayerName.FallbackNullOrEmpty(SteamManager.GetUsername()), - endpoint, - string.IsNullOrWhiteSpace(serverName) ? endpoint.StringRepresentation : serverName, + endpoints.Cast().ToImmutableArray(), + string.IsNullOrWhiteSpace(serverName) ? endpoints.First().StringRepresentation : serverName, Option.None()); } @@ -747,6 +810,18 @@ namespace Barotrauma SoundPlayer.Update((float)Timing.Step); + if ((PlayerInput.KeyDown(Keys.LeftControl) || PlayerInput.KeyDown(Keys.RightControl)) + && (PlayerInput.KeyDown(Keys.LeftShift) || PlayerInput.KeyDown(Keys.RightShift)) + && PlayerInput.KeyHit(Keys.Tab) + && SocialOverlay.Instance is { } socialOverlay) + { + socialOverlay.IsOpen = !socialOverlay.IsOpen; + if (socialOverlay.IsOpen) + { + socialOverlay.RefreshFriendList(); + } + } + if (PlayerInput.KeyHit(Keys.Escape) && WindowActive) { // Check if a text input is selected. @@ -758,15 +833,16 @@ namespace Barotrauma } GUI.KeyboardDispatcher.Subscriber = null; } + else if (SocialOverlay.Instance is { IsOpen: true }) + { + SocialOverlay.Instance.IsOpen = false; + } //if a verification prompt (are you sure you want to x) is open, close it - else if (GUIMessageBox.VisibleBox as GUIMessageBox != null && - GUIMessageBox.VisibleBox.UserData as string == "verificationprompt") + else if (GUIMessageBox.VisibleBox is GUIMessageBox { UserData: "verificationprompt" }) { ((GUIMessageBox)GUIMessageBox.VisibleBox).Close(); } - else if (GUIMessageBox.VisibleBox?.UserData is RoundSummary roundSummary && - roundSummary.ContinueButton != null && - roundSummary.ContinueButton.Visible) + else if (GUIMessageBox.VisibleBox?.UserData is RoundSummary { ContinueButton.Visible: true }) { GUIMessageBox.MessageBoxes.Remove(GUIMessageBox.VisibleBox); } @@ -778,8 +854,7 @@ namespace Barotrauma { gameSession.ToggleTabMenu(); } - else if (GUIMessageBox.VisibleBox as GUIMessageBox != null && - GUIMessageBox.VisibleBox.UserData as string == "bugreporter") + else if (GUIMessageBox.VisibleBox is GUIMessageBox { UserData: "bugreporter" }) { ((GUIMessageBox)GUIMessageBox.VisibleBox).Close(); } @@ -904,6 +979,7 @@ namespace Barotrauma CoroutineManager.Update(Paused, (float)Timing.Step); SteamManager.Update((float)Timing.Step); + EosInterface.Core.Update(); TaskPool.Update(); @@ -1097,14 +1173,16 @@ namespace Barotrauma public void ShowBugReporter() { - if (GUIMessageBox.VisibleBox != null && GUIMessageBox.VisibleBox.UserData as string == "bugreporter") + if (GUIMessageBox.VisibleBox != null && + GUIMessageBox.VisibleBox.UserData as string == "bugreporter") { return; } var msgBox = new GUIMessageBox(TextManager.Get("bugreportbutton"), "") { - UserData = "bugreporter" + UserData = "bugreporter", + DrawOnTop = true }; var linkHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), msgBox.Content.RectTransform)) { Stretch = true, RelativeSpacing = 0.025f }; linkHolder.RectTransform.MaxSize = new Point(int.MaxValue, linkHolder.Rect.Height); @@ -1117,7 +1195,7 @@ namespace Barotrauma { if (!SteamManager.OverlayCustomUrl(userdata as string)) { - ShowOpenUrlInWebBrowserPrompt(userdata as string); + ShowOpenUriPrompt(userdata as string); } msgBox.Close(); return true; @@ -1130,7 +1208,7 @@ namespace Barotrauma UserData = "https://github.com/Regalis11/Barotrauma/issues/new/choose", OnClicked = (btn, userdata) => { - ShowOpenUrlInWebBrowserPrompt(userdata as string); + ShowOpenUriPrompt(userdata as string); msgBox.Close(); return true; } @@ -1173,19 +1251,23 @@ namespace Barotrauma base.OnExiting(sender, args); } - public static void ShowOpenUrlInWebBrowserPrompt(string url, string promptExtensionTag = null) + public static GUIMessageBox ShowOpenUriPrompt(string url, string promptTextTag = "openlinkinbrowserprompt", string promptExtensionTag = null) { - if (string.IsNullOrEmpty(url)) { return; } - if (GUIMessageBox.VisibleBox?.UserData as string == "verificationprompt") { return; } - - LocalizedString text = TextManager.GetWithVariable("openlinkinbrowserprompt", "[link]", url); + LocalizedString text = TextManager.GetWithVariable(promptTextTag, "[link]", url); LocalizedString extensionText = TextManager.Get(promptExtensionTag); if (!extensionText.IsNullOrEmpty()) { text += $"\n\n{extensionText}"; } + return ShowOpenUriPrompt(url, text); + } - var msgBox = new GUIMessageBox("", text, new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }) + public static GUIMessageBox ShowOpenUriPrompt(string url, LocalizedString promptText) + { + if (string.IsNullOrEmpty(url)) { return null; } + if (GUIMessageBox.VisibleBox?.UserData as string == "verificationprompt") { return null; } + + var msgBox = new GUIMessageBox("", promptText, new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }) { UserData = "verificationprompt" }; @@ -1203,6 +1285,7 @@ namespace Barotrauma return true; }; msgBox.Buttons[1].OnClicked = msgBox.Close; + return msgBox; } /* diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index cc4695375..803e3c275 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -175,13 +175,11 @@ namespace Barotrauma if (CheatsEnabled) { DebugConsole.CheatsEnabled = true; -#if USE_STEAM - if (!SteamAchievementManager.CheatsEnabled) + if (!AchievementManager.CheatsEnabled) { - SteamAchievementManager.CheatsEnabled = true; + AchievementManager.CheatsEnabled = true; new GUIMessageBox("Cheats enabled", "Cheat commands have been enabled on the campaign. You will not receive Steam Achievements until you restart the game."); } -#endif } if (map == null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs index 7f81d3a3c..61cf70993 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs @@ -93,13 +93,13 @@ namespace Barotrauma.Items.Components [Editable(0.0f, 10.0f), Serialize(1.0f, IsPropertySaveable.Yes, description: "The scale of the text displayed on the label.", alwaysUseInstanceValues: true)] public float TextScale { - get { return textBlock == null ? 1.0f : textBlock.TextScale; } + get { return textBlock == null ? 1.0f : textBlock.TextScale / BaseToRealTextScaleFactor; } set { if (textBlock != null) { float prevScale = TextBlock.TextScale; - textBlock.TextScale = MathHelper.Clamp(value, 0.1f, 10.0f); + textBlock.TextScale = MathHelper.Clamp(value * BaseToRealTextScaleFactor, 0.1f, 10.0f); if (!MathUtils.NearlyEqual(prevScale, TextBlock.TextScale)) { SetScrollingText(); @@ -210,6 +210,8 @@ namespace Barotrauma.Items.Components SetScrollingText(); } + private const float BaseTextSize = 12.0f; + private float BaseToRealTextScaleFactor => BaseTextSize / GUIStyle.UnscaledSmallFont.Size; private void RecreateTextBlock() { textBlock = new GUITextBlock(new RectTransform(item.Rect.Size), "", @@ -217,7 +219,7 @@ namespace Barotrauma.Items.Components { TextDepth = item.SpriteDepth - 0.00001f, RoundToNearestPixel = false, - TextScale = TextScale, + TextScale = TextScale * BaseToRealTextScaleFactor, Padding = padding * item.Scale }; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index 909becc9a..48b20a156 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -785,7 +785,7 @@ namespace Barotrauma.Items.Components if (item.CurrentHull is { } currentHull && currentHull == hull) { - Sprite pingCircle = GUIStyle.YouAreHereCircle.Value.Sprite; + Sprite? pingCircle = GUIStyle.YouAreHereCircle.Value?.Sprite; if (pingCircle is null) { continue; } Vector2 charPos = item.WorldPosition; @@ -1241,7 +1241,8 @@ namespace Barotrauma.Items.Components foreach (Vector2 blip in MiniMapBlips) { Vector2 parentSize = miniMapFrame.Rect.Size.ToVector2(); - Sprite pingCircle = GUIStyle.PingCircle.Value.Sprite; + Sprite? pingCircle = GUIStyle.PingCircle.Value?.Sprite; + if (pingCircle is null) { continue; } Vector2 targetSize = new Vector2(parentSize.X / 4f); Vector2 spriteScale = targetSize / pingCircle.size; float scale = Math.Min(blipState, maxBlipState / 2f); @@ -1525,7 +1526,7 @@ namespace Barotrauma.Items.Components float maxWidth = Math.Max(sizeX, sizeY); Vector2 drawPos = new Vector2(frame.Rect.Right - sizeX, frame.Rect.Y - sizeY / 2f); - UISprite icon = GUIStyle.IconOverflowIndicator; + UISprite? icon = GUIStyle.IconOverflowIndicator; if (icon != null) { const int iconPadding = 4; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index 4de4c7810..4c789dcf7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -381,7 +381,7 @@ namespace Barotrauma.Lights spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive, transformMatrix: spriteBatchTransform); Vector3 glowColorHSV = ToolBox.RGBToHSV(AmbientLight); glowColorHSV.Z = Math.Max(glowColorHSV.Z, 0.4f); - Color glowColor = ToolBox.HSVToRGB(glowColorHSV.X, glowColorHSV.Y, glowColorHSV.Z); + Color glowColor = ToolBoxCore.HSVToRGB(glowColorHSV.X, glowColorHSV.Y, glowColorHSV.Z); Vector2 glowSpriteSize = new Vector2(gapGlowTexture.Width, gapGlowTexture.Height); foreach (var gap in Gap.GapList) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index 3b847fece..92beaecdb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -184,7 +184,8 @@ namespace Barotrauma connection.CrackSegments.Clear(); connection.CrackSegments.AddRange(MathUtils.GenerateJaggedLine( connectionStart, connectionEnd, - iterations, connectionLength * generationParams.ConnectionIndicatorDisplacementMultiplier)); + iterations, connectionLength * generationParams.ConnectionIndicatorDisplacementMultiplier, + rng: Rand.GetRNG(Rand.RandSync.ServerAndClient))); } private void LocationChanged(Location prevLocation, Location newLocation) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Radiation.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Radiation.cs index 5f8fdb8ef..8fa773813 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Radiation.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Radiation.cs @@ -9,8 +9,8 @@ namespace Barotrauma { private static readonly LocalizedString radiationTooltip = TextManager.Get("RadiationTooltip"); private static float spriteIndex; - private readonly SpriteSheet sheet = GUIStyle.RadiationAnimSpriteSheet; - private int maxFrames => sheet.FrameCount + 1; + private readonly SpriteSheet? sheet = GUIStyle.RadiationAnimSpriteSheet; + private int maxFrames => (sheet?.FrameCount ?? 0) + 1; private bool isHovingOver; @@ -18,7 +18,7 @@ namespace Barotrauma { if (!Enabled) { return; } - UISprite uiSprite = GUIStyle.Radiation; + UISprite? uiSprite = GUIStyle.Radiation; var (offsetX, offsetY) = Map.DrawOffset * zoom; var (centerX, centerY) = container.Center.ToVector2(); var (halfSizeX, halfSizeY) = new Vector2(container.Width / 2f, container.Height / 2f) * zoom; @@ -29,18 +29,21 @@ namespace Barotrauma Vector2 spriteScale = new Vector2(zoom); - uiSprite.Sprite.DrawTiled(spriteBatch, topLeft, size, color: Params.RadiationAreaColor, startOffset: Vector2.Zero, textureScale: spriteScale); + uiSprite?.Sprite.DrawTiled(spriteBatch, topLeft, size, color: Params.RadiationAreaColor, startOffset: Vector2.Zero, textureScale: spriteScale); Vector2 topRight = topLeft + Vector2.UnitX * size.X; int index = 0; - for (float i = 0; i <= size.Y; i += sheet.FrameSize.Y / 2f * zoom) + if (sheet != null) { - bool isEven = ++index % 2 == 0; - Vector2 origin = new Vector2(0.5f, 0) * sheet.FrameSize.X; - // every other sprite's animation is reversed to make it seem more chaotic - int sprite = (int) MathF.Floor(isEven ? spriteIndex : maxFrames - spriteIndex); - sheet.Draw(spriteBatch, sprite, topRight + new Vector2(0, i), Params.RadiationBorderTint, origin, 0f, spriteScale); + for (float i = 0; i <= size.Y; i += sheet.FrameSize.Y / 2f * zoom) + { + bool isEven = ++index % 2 == 0; + Vector2 origin = new Vector2(0.5f, 0) * sheet.FrameSize.X; + // every other sprite's animation is reversed to make it seem more chaotic + int sprite = (int) MathF.Floor(isEven ? spriteIndex : maxFrames - spriteIndex); + sheet.Draw(spriteBatch, sprite, topRight + new Vector2(0, i), Params.RadiationBorderTint, origin, 0f, spriteScale); + } } isHovingOver = container.Contains(PlayerInput.MousePosition) && PlayerInput.MousePosition.X < topLeft.X + size.X; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index 4e97d4929..bfb858f90 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -888,12 +888,12 @@ namespace Barotrauma var hsvBase = hsv; hsvBase.Y *= 4f; hsvBase.Z *= 0.8f; - btn.Color = ToolBox.HSVToRGB(hsvBase.X, hsvBase.Y, hsvBase.Z); - btn.SelectedColor = ToolBox.HSVToRGB(hsvBase.X, hsvBase.Y, hsvBase.Z); + btn.Color = ToolBoxCore.HSVToRGB(hsvBase.X, hsvBase.Y, hsvBase.Z); + btn.SelectedColor = ToolBoxCore.HSVToRGB(hsvBase.X, hsvBase.Y, hsvBase.Z); var hsvHover = hsv; hsvHover.Z *= 1.2f; - btn.HoverColor = ToolBox.HSVToRGB(hsvHover.X, hsvHover.Y, hsvHover.Z); + btn.HoverColor = ToolBoxCore.HSVToRGB(hsvHover.X, hsvHover.Y, hsvHover.Z); } public static List FilteredSelectedList { get; private set; } = new List(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs index 61bf8cec3..4107d249c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs @@ -26,7 +26,9 @@ namespace Barotrauma.Networking PrivateStart(); - processInfo.Arguments += " -pipes " + writePipe.GetClientHandleAsString() + " " + readPipe.GetClientHandleAsString(); + processInfo.ArgumentList.Add("-pipes"); + processInfo.ArgumentList.Add(writePipe.GetClientHandleAsString()); + processInfo.ArgumentList.Add(readPipe.GetClientHandleAsString()); try { Process = Process.Start(processInfo); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ConnectCommand.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ConnectCommand.cs new file mode 100644 index 000000000..52a8d476c --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ConnectCommand.cs @@ -0,0 +1,135 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Barotrauma.Extensions; +using Barotrauma.Steam; + +namespace Barotrauma.Networking; + +readonly record struct ConnectCommand( + Option NameAndP2PEndpointsOption, + Option NameAndLidgrenEndpointOption, + Option SteamLobbyIdOption) +{ + public bool IsClientConnectedToEndpoint() + { + if (GameMain.Client?.ClientPeer == null) { return false; } + if (NameAndP2PEndpointsOption.TryUnwrap(out var nameAndP2PEndpoints)) + { + if (nameAndP2PEndpoints.Endpoints.Any(e => e.Equals(GameMain.Client.ClientPeer.ServerEndpoint))) { return true; } + } + if (NameAndLidgrenEndpointOption.TryUnwrap(out var nameAndLidgrenEndpoint)) + { + if (nameAndLidgrenEndpoint.Endpoint.Equals(GameMain.Client.ClientPeer.ServerEndpoint)) { return true; } + } + if (SteamLobbyIdOption.TryUnwrap(out var steamLobbyId)) + { + if (SteamManager.CurrentLobbyID == steamLobbyId.Value) { return true; } + } + return false; + } + + public readonly record struct NameAndP2PEndpoints( + string ServerName, + ImmutableArray Endpoints); + + public readonly record struct NameAndLidgrenEndpoint( + string ServerName, + LidgrenEndpoint Endpoint); + + public readonly record struct SteamLobbyId(ulong Value); + + public ConnectCommand(string serverName, Endpoint endpoint) + : this( + NameAndP2PEndpointsOption: endpoint is P2PEndpoint p2pEndpoint + ? Option.Some(new NameAndP2PEndpoints(ServerName: serverName, p2pEndpoint.ToEnumerable().ToImmutableArray())) + : Option.None, + NameAndLidgrenEndpointOption: endpoint is LidgrenEndpoint lidgrenEndpoint + ? Option.Some(new NameAndLidgrenEndpoint(ServerName: serverName, lidgrenEndpoint)) + : Option.None, + SteamLobbyIdOption: Option.None) { } + + public ConnectCommand(string serverName, ImmutableArray endpoints) + : this( + NameAndP2PEndpointsOption: Option.Some(new NameAndP2PEndpoints(ServerName: serverName, Endpoints: endpoints)), + NameAndLidgrenEndpointOption: Option.None, + SteamLobbyIdOption: Option.None) { } + + public ConnectCommand(string serverName, LidgrenEndpoint endpoint) + : this( + NameAndP2PEndpointsOption: Option.None, + NameAndLidgrenEndpointOption: Option.Some(new NameAndLidgrenEndpoint(ServerName: serverName, Endpoint: endpoint)), + SteamLobbyIdOption: Option.None) { } + + public ConnectCommand(SteamLobbyId lobbyId) + : this( + NameAndP2PEndpointsOption: Option.None, + NameAndLidgrenEndpointOption: Option.None, + SteamLobbyIdOption: Option.Some(lobbyId)) { } + + public static Option Parse(string str) + => Parse(ToolBox.SplitCommand(str)); + + public static Option Parse(IReadOnlyList? args) + { + if (args == null || args.Count < 2) { return Option.None; } + + if (args[0].Equals("-connect", StringComparison.OrdinalIgnoreCase)) + { + if (args.Count < 3) { return Option.None; } + + var serverName = args[1]; + + var endpointStrs = args[2].Split(","); + var endpoints = endpointStrs.Select(Endpoint.Parse).NotNone().ToImmutableArray(); + if (endpoints.Length != endpointStrs.Length) { return Option.None; } + + if (endpoints.All(e => e is P2PEndpoint)) + { + return Option.Some( + new ConnectCommand(serverName, endpoints.Cast().ToImmutableArray())); + } + else if (endpoints.Length == 1 && endpoints[0] is LidgrenEndpoint lidgrenEndpoint) + { + return Option.Some( + new ConnectCommand(serverName, lidgrenEndpoint)); + } + + return Option.None; + } + else if (args[0].Equals("+connect_lobby", StringComparison.OrdinalIgnoreCase)) + { + return UInt64.TryParse(args[1], out var lobbyId) + ? Option.Some(new ConnectCommand(new SteamLobbyId(lobbyId))) + : Option.None; + } + return Option.None; + } + + public override string ToString() + { + if (SteamLobbyIdOption.TryUnwrap(out var steamLobbyId)) + { + return $"+connect_lobby {steamLobbyId.Value}"; + } + + if (NameAndP2PEndpointsOption.TryUnwrap(out var nameAndP2PEndpoints)) + { + var escapedName = nameAndP2PEndpoints.ServerName.Replace("\"", "\\\""); + var escapedEndpoints = nameAndP2PEndpoints.Endpoints.Select(e => e.StringRepresentation).JoinEscaped(','); + return $"-connect \"{escapedName}\" {escapedEndpoints}"; + } + + if (NameAndLidgrenEndpointOption.TryUnwrap(out var nameAndLidgrenEndpoint)) + { + var escapedName = nameAndLidgrenEndpoint.ServerName.Replace("\"", "\\\""); + var endpoint = nameAndLidgrenEndpoint.Endpoint.StringRepresentation; + return $"-connect \"{escapedName}\" {endpoint}"; + } + + return ""; + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index c65b1da61..705f8d1b0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -162,7 +162,7 @@ namespace Barotrauma.Networking set; } - private readonly Endpoint serverEndpoint; + private readonly ImmutableArray serverEndpoints; private readonly Option ownerKey; public bool IsServerOwner => ownerKey.IsSome(); @@ -182,6 +182,9 @@ namespace Barotrauma.Networking public readonly NamedEvent OnPermissionChanged = new NamedEvent(); public GameClient(string newName, Endpoint endpoint, string serverName, Option ownerKey) + : this(newName, endpoint.ToEnumerable().ToImmutableArray(), serverName, ownerKey) { } + + public GameClient(string newName, ImmutableArray endpoints, string serverName, Option ownerKey) { //TODO: gui stuff should probably not be here? this.ownerKey = ownerKey; @@ -270,7 +273,7 @@ namespace Barotrauma.Networking ServerSettings = new ServerSettings(this, "Server", 0, 0, 0, false, false); Voting = new Voting(); - serverEndpoint = endpoint; + serverEndpoints = endpoints; InitiateServerJoin(serverName); //ServerLog = new ServerLog(""); @@ -281,7 +284,7 @@ namespace Barotrauma.Networking public ServerInfo CreateServerInfoFromSettings() { - var serverInfo = ServerInfo.FromServerConnection(ClientPeer.ServerConnection, ServerSettings); + var serverInfo = ServerInfo.FromServerEndpoints(ClientPeer.AllServerEndpoints, ServerSettings); GameMain.ServerListScreen.UpdateOrAddServerInfo(serverInfo); return serverInfo; } @@ -327,11 +330,14 @@ namespace Barotrauma.Networking ReadDataMessage, OnClientPeerDisconnect, OnConnectionInitializationComplete); - return serverEndpoint switch - { - LidgrenEndpoint lidgrenEndpoint => new LidgrenClientPeer(lidgrenEndpoint, callbacks, ownerKey), - SteamP2PEndpoint _ when ownerKey.TryUnwrap(out var key) => new SteamP2POwnerPeer(callbacks, key), - SteamP2PEndpoint steamP2PServerEndpoint when ownerKey.IsNone() => new SteamP2PClientPeer(steamP2PServerEndpoint, callbacks), + return serverEndpoints.First() switch + { + LidgrenEndpoint lidgrenEndpoint + => new LidgrenClientPeer(lidgrenEndpoint, callbacks, ownerKey), + P2PEndpoint when ownerKey.TryUnwrap(out int key) + => new P2POwnerPeer(callbacks, key, serverEndpoints.Cast().ToImmutableArray()), + P2PEndpoint when ownerKey.IsNone() + => new P2PClientPeer(serverEndpoints.Cast().ToImmutableArray(), callbacks), _ => throw new ArgumentOutOfRangeException() }; } @@ -800,6 +806,9 @@ namespace Barotrauma.Networking case ServerPacketHeader.ACHIEVEMENT: ReadAchievement(inc); break; + case ServerPacketHeader.ACHIEVEMENT_STAT: + ReadAchievementStat(inc); + break; case ServerPacketHeader.CHEATS_ENABLED: bool cheatsEnabled = inc.ReadBoolean(); inc.ReadPadBits(); @@ -810,7 +819,7 @@ namespace Barotrauma.Networking else { DebugConsole.CheatsEnabled = cheatsEnabled; - SteamAchievementManager.CheatsEnabled = cheatsEnabled; + AchievementManager.CheatsEnabled = cheatsEnabled; if (cheatsEnabled) { var cheatMessageBox = new GUIMessageBox(TextManager.Get("CheatsEnabledTitle"), TextManager.Get("CheatsEnabledDescription")); @@ -858,7 +867,7 @@ namespace Barotrauma.Networking private void ReadStartGameFinalize(IReadMessage inc) { - TaskPool.ListTasks(); + TaskPool.ListTasks(DebugConsole.Log); ushort contentToPreloadCount = inc.ReadUInt16(); List contentToPreload = new List(); for (int i = 0; i < contentToPreloadCount; i++) @@ -995,8 +1004,9 @@ namespace Barotrauma.Networking } else { - if (ClientPeer is SteamP2PClientPeer or SteamP2POwnerPeer) + if (ClientPeer is P2PClientPeer or P2POwnerPeer) { + Eos.EosSessionManager.LeaveSession(); SteamManager.LeaveLobby(); } @@ -1005,10 +1015,7 @@ namespace Barotrauma.Networking GameMain.GameSession?.Campaign?.CancelStartRound(); - if (SteamManager.IsInitialized) - { - Steamworks.SteamFriends.ClearRichPresence(); - } + UpdatePresence(""); foreach (var fileTransfer in FileReceiver.ActiveTransfers.ToArray()) { FileReceiver.StopTransfer(fileTransfer, deleteFile: true); @@ -1097,19 +1104,47 @@ namespace Barotrauma.Networking ClientPeer.ServerContentPackages = prevContentPackages; } } - - private void OnConnectionInitializationComplete() + + private void UpdatePresence(string connectCommand) { + #warning TODO: use store localization functionality + var desc = TextManager.GetWithVariable("FriendPlayingOnServer", "[servername]", ServerName); + + async Task updateEosPresence() + { + var epicIds = EosInterface.IdQueries.GetLoggedInEpicIds(); + if (!epicIds.FirstOrNone().TryUnwrap(out var epicAccountId)) { return; } + + var setPresenceResult = await EosInterface.Presence.SetJoinCommand( + epicAccountId: epicAccountId, + desc: desc.Value, + serverName: ServerName, + joinCommand: connectCommand); + DebugConsole.NewMessage($"Set connect command: {connectCommand}, result: {setPresenceResult}"); + } + + TaskPool.Add( + "UpdateEosPresence", + updateEosPresence(), + static _ => { }); + if (SteamManager.IsInitialized) { Steamworks.SteamFriends.ClearRichPresence(); - Steamworks.SteamFriends.SetRichPresence("servername", ServerName); - #warning TODO: use Steamworks localization functionality - Steamworks.SteamFriends.SetRichPresence("status", - TextManager.GetWithVariable("FriendPlayingOnServer", "[servername]", ServerName).Value); - Steamworks.SteamFriends.SetRichPresence("connect", - $"-connect \"{ToolBox.EscapeCharacters(ServerName)}\" {serverEndpoint}"); + if (!connectCommand.IsNullOrWhiteSpace()) + { + Steamworks.SteamFriends.SetRichPresence("servername", ServerName); + Steamworks.SteamFriends.SetRichPresence("status", + desc.Value); + Steamworks.SteamFriends.SetRichPresence("connect", + connectCommand); + } } + } + + private void OnConnectionInitializationComplete() + { + UpdatePresence($"-connect \"{ToolBox.EscapeCharacters(ServerName)}\" {string.Join(",", serverEndpoints.Select(e => e.StringRepresentation))}"); canStart = true; connected = true; @@ -1167,18 +1202,16 @@ namespace Barotrauma.Networking } - private void ReadAchievement(IReadMessage inc) + private static void ReadAchievement(IReadMessage inc) { Identifier achievementIdentifier = inc.ReadIdentifier(); - int amount = inc.ReadInt32(); - if (amount == 0) - { - SteamAchievementManager.UnlockAchievement(achievementIdentifier); - } - else - { - SteamAchievementManager.IncrementStat(achievementIdentifier, amount); - } + AchievementManager.UnlockAchievement(achievementIdentifier); + } + + private static void ReadAchievementStat(IReadMessage inc) + { + var netStat = INetSerializableStruct.Read(inc); + AchievementManager.IncrementStat(netStat.Stat, netStat.Amount); } private static void ReadCircuitBoxMessage(IReadMessage inc) @@ -1885,8 +1918,9 @@ namespace Barotrauma.Networking } if (updateClientListId) { LastClientListUpdateID = listId; } - if (ClientPeer is SteamP2POwnerPeer) + if (ClientPeer is P2POwnerPeer) { + Eos.EosSessionManager.UpdateOwnedSession(ClientPeer.ServerConnection.Endpoint, ServerSettings); TaskPool.Add("WaitForPingDataAsync (owner)", Steamworks.SteamNetworkingUtils.WaitForPingDataAsync(), (task) => { @@ -2029,8 +2063,9 @@ namespace Barotrauma.Networking ServerSettings.AllowSubVoting = allowSubVoting; ServerSettings.AllowModeVoting = allowModeVoting; - if (ClientPeer is SteamP2POwnerPeer) + if (ClientPeer is P2POwnerPeer) { + Eos.EosSessionManager.UpdateOwnedSession(ClientPeer.ServerConnection.Endpoint, ServerSettings); Steam.SteamManager.UpdateLobby(ServerSettings); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/P2PSocket/DualStackP2PSocket.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/P2PSocket/DualStackP2PSocket.cs new file mode 100644 index 000000000..613767851 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/P2PSocket/DualStackP2PSocket.cs @@ -0,0 +1,82 @@ +#nullable enable + +namespace Barotrauma.Networking; + +sealed class DualStackP2PSocket : P2PSocket +{ + private readonly Option eosSocket; + private readonly Option steamSocket; + + private DualStackP2PSocket( + Callbacks callbacks, + Option eosSocket, + Option steamSocket) : + base(callbacks) + { + this.eosSocket = eosSocket; + this.steamSocket = steamSocket; + } + + public static Result Create(Callbacks callbacks) + { + var eosP2PSocketResult = EosP2PSocket.Create(callbacks); + var steamP2PSocketResult = SteamListenSocket.Create(callbacks); + if (eosP2PSocketResult.TryUnwrapFailure(out var eosError) + && steamP2PSocketResult.TryUnwrapFailure(out var steamError)) + { + return Result.Failure(new Error(eosError, steamError)); + } + return Result.Success((P2PSocket)new DualStackP2PSocket( + callbacks, + eosP2PSocketResult.TryUnwrapSuccess(out var eosP2PSocket) + ? Option.Some((EosP2PSocket)eosP2PSocket) + : Option.None, + steamP2PSocketResult.TryUnwrapSuccess(out var steamP2PSocket) + ? Option.Some((SteamListenSocket)steamP2PSocket) + : Option.None)); + } + + public override void ProcessIncomingMessages() + { + if (eosSocket.TryUnwrap(out var eosP2PSocket)) { eosP2PSocket.ProcessIncomingMessages(); } + if (steamSocket.TryUnwrap(out var steamP2PSocket)) { steamP2PSocket.ProcessIncomingMessages(); } + } + + public override bool SendMessage(P2PEndpoint endpoint, IWriteMessage outMsg, DeliveryMethod deliveryMethod) + { + return endpoint switch + { + EosP2PEndpoint eosP2PEndpoint when eosSocket.TryUnwrap(out var eosP2PSocket) + => eosP2PSocket.SendMessage(eosP2PEndpoint, outMsg, deliveryMethod), + SteamP2PEndpoint steamP2PEndpoint when steamSocket.TryUnwrap(out var steamP2PSocket) + => steamP2PSocket.SendMessage(steamP2PEndpoint, outMsg, deliveryMethod), + _ + => false + }; + } + + public override void CloseConnection(P2PEndpoint endpoint) + { + switch (endpoint) + { + case EosP2PEndpoint eosP2PEndpoint: + if (eosSocket.TryUnwrap(out var eosP2PSocket)) + { + eosP2PSocket.CloseConnection(eosP2PEndpoint); + } + break; + case SteamP2PEndpoint steamP2PEndpoint: + if (steamSocket.TryUnwrap(out var steamP2PSocket)) + { + steamP2PSocket.CloseConnection(steamP2PEndpoint); + } + break; + } + } + + public override void Dispose() + { + if (eosSocket.TryUnwrap(out var eosP2PSocket)) { eosP2PSocket.Dispose(); } + if (steamSocket.TryUnwrap(out var steamP2PSocket)) { steamP2PSocket.Dispose(); } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/P2PSocket/EosP2PSocket.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/P2PSocket/EosP2PSocket.cs new file mode 100644 index 000000000..73531529b --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/P2PSocket/EosP2PSocket.cs @@ -0,0 +1,99 @@ +#nullable enable + +namespace Barotrauma.Networking; + +sealed class EosP2PSocket : P2PSocket +{ + private readonly EosInterface.P2PSocket eosSocket; + + private EosP2PSocket( + Callbacks callbacks, + EosInterface.P2PSocket eosSocket) + : base(callbacks) + { + this.eosSocket = eosSocket; + } + + public static Result Create(Callbacks callbacks) + { + if (!EosInterface.Core.IsInitialized) { return Result.Failure(new Error(ErrorCode.EosNotInitialized)); } + + var eosSocketId = new EosInterface.SocketId { SocketName = EosP2PEndpoint.SocketName }; + if (EosInterface.IdQueries.GetLoggedInPuids() is not { Length: > 0 } puids) + { + return Result.Failure(new Error(ErrorCode.EosNotLoggedIn)); + } + var socketCreateResult = EosInterface.P2PSocket.Create(puids[0], eosSocketId); + + if (!socketCreateResult.TryUnwrapSuccess(out var eosSocket)) { return Result.Failure(new Error(ErrorCode.FailedToCreateEosP2PSocket, socketCreateResult.ToString())); } + var retVal = new EosP2PSocket(callbacks, eosSocket); + + eosSocket.HandleIncomingConnection.Register("Event".ToIdentifier(), retVal.OnIncomingConnection); + eosSocket.HandleClosedConnection.Register("Event".ToIdentifier(), retVal.OnConnectionClosed); + + return Result.Success((P2PSocket)retVal); + } + + public override void ProcessIncomingMessages() + { + foreach (var msg in eosSocket.GetMessageBatch()) + { + callbacks.OnData(new EosP2PEndpoint(msg.Sender), new ReadWriteMessage(msg.Buffer, 0, msg.ByteLength * 8, false)); + } + } + + public override bool SendMessage(P2PEndpoint endpoint, IWriteMessage outMsg, DeliveryMethod deliveryMethod) + { + if (endpoint is not EosP2PEndpoint { ProductUserId: var puid }) { return false; } + var sendResult = eosSocket.SendMessage(new EosInterface.P2PSocket.OutgoingMessage( + Buffer: outMsg.Buffer, + ByteLength: outMsg.LengthBytes, + Destination: puid, + DeliveryMethod: deliveryMethod)); + return sendResult.IsSuccess; + } + + private void OnIncomingConnection(EosInterface.P2PSocket.IncomingConnectionRequest request) + { + var remoteEndpoint = new EosP2PEndpoint(request.RemoteUserId); + + if (callbacks.OnIncomingConnection(remoteEndpoint)) + { + request.Accept(); + } + } + + private void OnConnectionClosed(EosInterface.P2PSocket.RemoteConnectionClosed data) + { + var remoteEndpoint = new EosP2PEndpoint(data.RemoteUserId); + + var peerDisconnectPacket = PeerDisconnectPacket.WithReason(data.Reason switch + { + EosInterface.P2PSocket.RemoteConnectionClosed.ConnectionClosedReason.Unknown => DisconnectReason.Unknown, + EosInterface.P2PSocket.RemoteConnectionClosed.ConnectionClosedReason.ClosedByLocalUser => DisconnectReason.Disconnected, + EosInterface.P2PSocket.RemoteConnectionClosed.ConnectionClosedReason.ClosedByPeer => DisconnectReason.Disconnected, + EosInterface.P2PSocket.RemoteConnectionClosed.ConnectionClosedReason.TimedOut => DisconnectReason.Timeout, + EosInterface.P2PSocket.RemoteConnectionClosed.ConnectionClosedReason.TooManyConnections => DisconnectReason.ServerFull, + EosInterface.P2PSocket.RemoteConnectionClosed.ConnectionClosedReason.InvalidMessage => DisconnectReason.Unknown, + EosInterface.P2PSocket.RemoteConnectionClosed.ConnectionClosedReason.InvalidData => DisconnectReason.Unknown, + EosInterface.P2PSocket.RemoteConnectionClosed.ConnectionClosedReason.ConnectionFailed => DisconnectReason.AuthenticationFailed, + EosInterface.P2PSocket.RemoteConnectionClosed.ConnectionClosedReason.ConnectionClosed => DisconnectReason.Disconnected, + EosInterface.P2PSocket.RemoteConnectionClosed.ConnectionClosedReason.NegotiationFailed => DisconnectReason.AuthenticationFailed, + EosInterface.P2PSocket.RemoteConnectionClosed.ConnectionClosedReason.UnexpectedError => DisconnectReason.Unknown, + EosInterface.P2PSocket.RemoteConnectionClosed.ConnectionClosedReason.Unhandled => DisconnectReason.Unknown, + _ => DisconnectReason.Unknown + }); + callbacks.OnConnectionClosed(remoteEndpoint, peerDisconnectPacket); + } + + public override void CloseConnection(P2PEndpoint endpoint) + { + if (endpoint is not EosP2PEndpoint { ProductUserId: var puid }) { return; } + eosSocket.CloseConnection(puid); + } + + public override void Dispose() + { + eosSocket.Dispose(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/P2PSocket/P2PSocket.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/P2PSocket/P2PSocket.cs new file mode 100644 index 000000000..4c54b406d --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/P2PSocket/P2PSocket.cs @@ -0,0 +1,56 @@ +#nullable enable +using System; +using System.Collections.Immutable; +using System.Linq; +using Barotrauma.Extensions; + +namespace Barotrauma.Networking; + +abstract class P2PSocket : IDisposable +{ + public enum ErrorCode + { + EosNotInitialized, + EosNotLoggedIn, + FailedToCreateEosP2PSocket, + + SteamNotInitialized, + FailedToCreateSteamP2PSocket + } + + public readonly record struct Error( + ImmutableArray<(ErrorCode Code, string AdditionalInfo)> CodesAndInfo) + { + public Error(ErrorCode code, string? additionalInfo = "") : this((code, additionalInfo ?? "").ToEnumerable().ToImmutableArray()) { } + public Error(params Error[] innerErrors) : this(innerErrors.SelectMany(ie => ie.CodesAndInfo).ToImmutableArray()) { } + + public override string? ToString() + { + if (CodesAndInfo.IsDefault) + { + return "default(Error)"; + } + + return $"Errors({string.Join("; ", CodesAndInfo)})"; + } + } + + public readonly record struct Callbacks( + Predicate OnIncomingConnection, + Action OnConnectionClosed, + Action OnData); + protected readonly Callbacks callbacks; + + protected P2PSocket(Callbacks callbacks) + { + this.callbacks = callbacks; + } + + public abstract void ProcessIncomingMessages(); + + public abstract bool SendMessage(P2PEndpoint endpoint, IWriteMessage outMsg, DeliveryMethod deliveryMethod); + + public abstract void CloseConnection(P2PEndpoint endpoint); + + public abstract void Dispose(); +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/P2PSocket/SteamConnectSocket.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/P2PSocket/SteamConnectSocket.cs new file mode 100644 index 000000000..47e3b69e5 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/P2PSocket/SteamConnectSocket.cs @@ -0,0 +1,114 @@ +using System; +using System.Runtime.InteropServices; +using Barotrauma.Steam; +namespace Barotrauma.Networking; + +sealed class SteamConnectSocket : P2PSocket +{ + private sealed class ConnectionManager : Steamworks.ConnectionManager, Steamworks.IConnectionManager + { + private SteamP2PEndpoint endpoint; + private Callbacks callbacks; + public void SetEndpointAndCallbacks(SteamP2PEndpoint endpoint, Callbacks callbacks) + { + this.endpoint = endpoint; + this.callbacks = callbacks; + } + + public override void OnMessage(IntPtr data, int size, long messageNum, long recvTime, int channel) + { + var dataArray = new byte[size]; + Marshal.Copy(source: data, destination: dataArray, startIndex: 0, length: size); + + callbacks.OnData(endpoint, new ReadWriteMessage(dataArray, bitPos: 0, lBits: size * 8, copyBuf: false)); + } + + public override void OnDisconnected(Steamworks.Data.ConnectionInfo info) + { + if (!info.Identity.IsSteamId) { return; } + var remoteEndpoint = new SteamP2PEndpoint(new SteamId((Steamworks.SteamId)info.Identity)); + var peerDisconnectPacket = PeerDisconnectPacket.WithReason(info.EndReason switch + { + Steamworks.NetConnectionEnd.App_Generic => DisconnectReason.Disconnected, + Steamworks.NetConnectionEnd.AppException_Generic => DisconnectReason.Unknown, + + Steamworks.NetConnectionEnd.Local_OfflineMode => DisconnectReason.SteamP2PError, + Steamworks.NetConnectionEnd.Local_ManyRelayConnectivity => DisconnectReason.SteamP2PError, + Steamworks.NetConnectionEnd.Local_HostedServerPrimaryRelay => DisconnectReason.SteamP2PError, + Steamworks.NetConnectionEnd.Local_NetworkConfig => DisconnectReason.SteamP2PError, + Steamworks.NetConnectionEnd.Local_Rights => DisconnectReason.SteamP2PError, + Steamworks.NetConnectionEnd.Local_P2P_ICE_NoPublicAddresses => DisconnectReason.SteamP2PError, + + Steamworks.NetConnectionEnd.Remote_Timeout => DisconnectReason.SteamP2PTimeOut, + Steamworks.NetConnectionEnd.Remote_BadCrypt => DisconnectReason.SteamP2PError, + Steamworks.NetConnectionEnd.Remote_BadCert => DisconnectReason.SteamP2PError, + Steamworks.NetConnectionEnd.Remote_BadProtocolVersion => DisconnectReason.SteamP2PError, + Steamworks.NetConnectionEnd.Remote_P2P_ICE_NoPublicAddresses => DisconnectReason.SteamP2PError, + + Steamworks.NetConnectionEnd.Misc_Generic => DisconnectReason.Unknown, + Steamworks.NetConnectionEnd.Misc_InternalError => DisconnectReason.SteamP2PError, + Steamworks.NetConnectionEnd.Misc_Timeout => DisconnectReason.SteamP2PTimeOut, + Steamworks.NetConnectionEnd.Misc_SteamConnectivity => DisconnectReason.SteamP2PError, + Steamworks.NetConnectionEnd.Misc_NoRelaySessionsToClient => DisconnectReason.SteamP2PError, + Steamworks.NetConnectionEnd.Misc_P2P_Rendezvous => DisconnectReason.SteamP2PError, + Steamworks.NetConnectionEnd.Misc_P2P_NAT_Firewall => DisconnectReason.SteamP2PError, + Steamworks.NetConnectionEnd.Misc_PeerSentNoConnection => DisconnectReason.SteamP2PError, + + _ => DisconnectReason.Unknown + }); + callbacks.OnConnectionClosed(remoteEndpoint, peerDisconnectPacket); + base.OnDisconnected(info); + } + } + + private readonly SteamP2PEndpoint expectedEndpoint; + private readonly ConnectionManager connectionManager; + + private SteamConnectSocket(SteamP2PEndpoint expectedEndpoint, Callbacks callbacks, ConnectionManager connectionManager) : base(callbacks) + { + this.expectedEndpoint = expectedEndpoint; + this.connectionManager = connectionManager; + } + + public static Result Create(SteamP2PEndpoint endpoint, Callbacks callbacks) + { + if (!SteamManager.IsInitialized) { return Result.Failure(new Error(ErrorCode.SteamNotInitialized)); } + + var connectionManager = Steamworks.SteamNetworkingSockets.ConnectRelay(endpoint.SteamId.Value); + if (connectionManager is null) { return Result.Failure(new Error(ErrorCode.FailedToCreateSteamP2PSocket)); } + connectionManager.SetEndpointAndCallbacks(endpoint, callbacks); + + return Result.Success((P2PSocket)new SteamConnectSocket(endpoint, callbacks, connectionManager)); + } + + public override void ProcessIncomingMessages() + { + connectionManager.Receive(); + } + + public override bool SendMessage(P2PEndpoint endpoint, IWriteMessage outMsg, DeliveryMethod deliveryMethod) + { + if (endpoint != expectedEndpoint) { return false; } + var result = connectionManager.Connection.SendMessage( + data: outMsg.Buffer, + offset: 0, + length: outMsg.LengthBytes, + sendType: deliveryMethod switch + { + DeliveryMethod.Reliable => Steamworks.Data.SendType.Reliable, + _ => Steamworks.Data.SendType.Unreliable + }); + return result == Steamworks.Result.OK; + } + + public override void CloseConnection(P2PEndpoint endpoint) + { + if (endpoint != expectedEndpoint) { return; } + connectionManager.Close(); + } + + public override void Dispose() + { + connectionManager.Close(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/P2PSocket/SteamListenSocket.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/P2PSocket/SteamListenSocket.cs new file mode 100644 index 000000000..b1e7c8170 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/P2PSocket/SteamListenSocket.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Barotrauma.Steam; + +namespace Barotrauma.Networking; + +sealed class SteamListenSocket : P2PSocket +{ + private sealed class SocketManager : Steamworks.SocketManager, Steamworks.ISocketManager + { + private Callbacks callbacks; + private readonly Dictionary endpointToConnection = new(); + + public void SetCallbacks(Callbacks callbacks) + { + this.callbacks = callbacks; + } + + public override void OnConnecting(Steamworks.Data.Connection connection, Steamworks.Data.ConnectionInfo info) + { + if (!info.Identity.IsSteamId) { return; } + var remoteEndpoint = new SteamP2PEndpoint(new SteamId((Steamworks.SteamId)info.Identity)); + endpointToConnection[remoteEndpoint] = connection; + if (callbacks.OnIncomingConnection(remoteEndpoint)) + { + connection.Accept(); + } + } + + public override void OnDisconnected(Steamworks.Data.Connection connection, Steamworks.Data.ConnectionInfo info) + { + if (!info.Identity.IsSteamId) { return; } + var remoteEndpoint = new SteamP2PEndpoint(new SteamId((Steamworks.SteamId)info.Identity)); + endpointToConnection.Remove(remoteEndpoint); + var peerDisconnectPacket = PeerDisconnectPacket.WithReason(info.EndReason switch + { + Steamworks.NetConnectionEnd.App_Generic => DisconnectReason.Disconnected, + Steamworks.NetConnectionEnd.AppException_Generic => DisconnectReason.Unknown, + + Steamworks.NetConnectionEnd.Local_OfflineMode => DisconnectReason.SteamP2PError, + Steamworks.NetConnectionEnd.Local_ManyRelayConnectivity => DisconnectReason.SteamP2PError, + Steamworks.NetConnectionEnd.Local_HostedServerPrimaryRelay => DisconnectReason.SteamP2PError, + Steamworks.NetConnectionEnd.Local_NetworkConfig => DisconnectReason.SteamP2PError, + Steamworks.NetConnectionEnd.Local_Rights => DisconnectReason.SteamP2PError, + Steamworks.NetConnectionEnd.Local_P2P_ICE_NoPublicAddresses => DisconnectReason.SteamP2PError, + + Steamworks.NetConnectionEnd.Remote_Timeout => DisconnectReason.SteamP2PTimeOut, + Steamworks.NetConnectionEnd.Remote_BadCrypt => DisconnectReason.SteamP2PError, + Steamworks.NetConnectionEnd.Remote_BadCert => DisconnectReason.SteamP2PError, + Steamworks.NetConnectionEnd.Remote_BadProtocolVersion => DisconnectReason.SteamP2PError, + Steamworks.NetConnectionEnd.Remote_P2P_ICE_NoPublicAddresses => DisconnectReason.SteamP2PError, + + Steamworks.NetConnectionEnd.Misc_Generic => DisconnectReason.Unknown, + Steamworks.NetConnectionEnd.Misc_InternalError => DisconnectReason.SteamP2PError, + Steamworks.NetConnectionEnd.Misc_Timeout => DisconnectReason.SteamP2PTimeOut, + Steamworks.NetConnectionEnd.Misc_SteamConnectivity => DisconnectReason.SteamP2PError, + Steamworks.NetConnectionEnd.Misc_NoRelaySessionsToClient => DisconnectReason.SteamP2PError, + Steamworks.NetConnectionEnd.Misc_P2P_Rendezvous => DisconnectReason.SteamP2PError, + Steamworks.NetConnectionEnd.Misc_P2P_NAT_Firewall => DisconnectReason.SteamP2PError, + Steamworks.NetConnectionEnd.Misc_PeerSentNoConnection => DisconnectReason.SteamP2PError, + + _ => DisconnectReason.Unknown + }); + callbacks.OnConnectionClosed(remoteEndpoint, peerDisconnectPacket); + base.OnDisconnected(connection, info); + } + + public override void OnMessage(Steamworks.Data.Connection connection, Steamworks.Data.NetIdentity identity, IntPtr data, int size, long messageNum, long recvTime, int channel) + { + if (!identity.IsSteamId) { return; } + var endpoint = new SteamP2PEndpoint(new SteamId((Steamworks.SteamId)identity)); + + var dataArray = new byte[size]; + Marshal.Copy(source: data, destination: dataArray, startIndex: 0, length: size); + + callbacks.OnData(endpoint, new ReadWriteMessage(dataArray, bitPos: 0, lBits: size * 8, copyBuf: false)); + } + + internal bool SendMessage(SteamP2PEndpoint endpoint, IWriteMessage outMsg, DeliveryMethod deliveryMethod) + { + if (!endpointToConnection.TryGetValue(endpoint, out var connection)) + { + return false; + } + + var result = connection.SendMessage( + data: outMsg.Buffer, + offset: 0, + length: outMsg.LengthBytes, + sendType: deliveryMethod switch + { + DeliveryMethod.Reliable => Steamworks.Data.SendType.Reliable, + _ => Steamworks.Data.SendType.Unreliable + }); + return result == Steamworks.Result.OK; + } + + internal void CloseConnection(SteamP2PEndpoint endpoint) + { + if (!endpointToConnection.TryGetValue(endpoint, out var connection)) { return; } + connection.Close(); + } + } + + private readonly SocketManager socketManager; + + private SteamListenSocket( + Callbacks callbacks, + SocketManager socketManager) + : base(callbacks) + { + this.socketManager = socketManager; + } + + public static Result Create(Callbacks callbacks) + { + if (!SteamManager.IsInitialized) { return Result.Failure(new Error(ErrorCode.SteamNotInitialized)); } + + var socketManager = Steamworks.SteamNetworkingSockets.CreateRelaySocket(); + if (socketManager is null) { return Result.Failure(new Error(ErrorCode.FailedToCreateSteamP2PSocket)); } + socketManager.SetCallbacks(callbacks); + + return Result.Success((P2PSocket)new SteamListenSocket(callbacks, socketManager)); + } + + public override void ProcessIncomingMessages() + { + socketManager.Receive(); + } + + public override bool SendMessage(P2PEndpoint endpoint, IWriteMessage outMsg, DeliveryMethod deliveryMethod) + { + if (endpoint is not SteamP2PEndpoint steamP2PEndpoint) { return false; } + return socketManager.SendMessage(steamP2PEndpoint, outMsg, deliveryMethod); + } + + public override void CloseConnection(P2PEndpoint endpoint) + { + if (endpoint is not SteamP2PEndpoint steamP2PEndpoint) { return; } + socketManager.CloseConnection(steamP2PEndpoint); + } + + public override void Dispose() + { + socketManager.Close(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs index 6718bcbf4..ef0b76332 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs @@ -1,12 +1,18 @@ #nullable enable -using Barotrauma.Steam; -using System; using System.Collections.Immutable; using System.Linq; +using System.Threading.Tasks; using Microsoft.Xna.Framework; namespace Barotrauma.Networking { + internal abstract class ClientPeer : ClientPeer where TEndpoint : Endpoint + { + public new TEndpoint ServerEndpoint => (base.ServerEndpoint as TEndpoint)!; + protected ClientPeer(TEndpoint serverEndpoint, ImmutableArray allServerEndpoints, Callbacks callbacks, Option ownerKey) + : base(serverEndpoint, allServerEndpoints, callbacks, ownerKey) { } + } + internal abstract class ClientPeer { public ImmutableArray ServerContentPackages { get; set; } = @@ -25,21 +31,22 @@ namespace Barotrauma.Networking protected readonly Callbacks callbacks; public readonly Endpoint ServerEndpoint; + public readonly ImmutableArray AllServerEndpoints; public NetworkConnection? ServerConnection { get; protected set; } - protected readonly bool isOwner; + protected bool IsOwner => ownerKey.IsSome(); protected readonly Option ownerKey; public bool IsActive => isActive; protected bool isActive; - public ClientPeer(Endpoint serverEndpoint, Callbacks callbacks, Option ownerKey) + protected ClientPeer(Endpoint serverEndpoint, ImmutableArray allServerEndpoints, Callbacks callbacks, Option ownerKey) { ServerEndpoint = serverEndpoint; + AllServerEndpoints = allServerEndpoints; this.callbacks = callbacks; this.ownerKey = ownerKey; - isOwner = ownerKey.IsSome(); } public abstract void Start(); @@ -53,7 +60,7 @@ namespace Barotrauma.Networking protected ConnectionInitialization initializationStep; public bool ContentPackageOrderReceived { get; set; } protected int passwordSalt; - protected Option steamAuthTicket; + protected Option authTicket; private GUIMessageBox? passwordMsgBox; public bool WaitingForPassword @@ -67,43 +74,64 @@ namespace Barotrauma.Networking public IReadMessage Message; } + protected abstract Task> GetAccountId(); + + protected void OnInitializationComplete() + { + passwordMsgBox?.Close(); + if (initializationStep == ConnectionInitialization.Success) { return; } + + callbacks.OnInitializationComplete.Invoke(); + initializationStep = ConnectionInitialization.Success; + } + protected void ReadConnectionInitializationStep(IncomingInitializationMessage inc) { + if (inc.InitializationStep != ConnectionInitialization.Password) + { + passwordMsgBox?.Close(); + } + switch (inc.InitializationStep) { - case ConnectionInitialization.SteamTicketAndVersion: + case ConnectionInitialization.AuthInfoAndVersion: { - if (initializationStep != ConnectionInitialization.SteamTicketAndVersion) { return; } + if (initializationStep != ConnectionInitialization.AuthInfoAndVersion) { return; } - PeerPacketHeaders headers = new PeerPacketHeaders + TaskPool.Add($"{GetType().Name}.{nameof(GetAccountId)}", GetAccountId(), t => { - DeliveryMethod = DeliveryMethod.Reliable, - PacketHeader = PacketHeader.IsConnectionInitializationStep, - Initialization = ConnectionInitialization.SteamTicketAndVersion - }; + if (GameMain.Client?.ClientPeer is null) { return; } + + if (!t.TryGetResult(out Option accountId)) + { + Close(PeerDisconnectPacket.WithReason(DisconnectReason.AuthenticationFailed)); + } + + var headers = new PeerPacketHeaders + { + DeliveryMethod = DeliveryMethod.Reliable, + PacketHeader = PacketHeader.IsConnectionInitializationStep, + Initialization = ConnectionInitialization.AuthInfoAndVersion + }; - if (steamAuthTicket.TryUnwrap(out var authTicket) && authTicket is { Canceled: true }) - { - throw new InvalidOperationException("ReadConnectionInitializationStep failed: Steam auth ticket has been cancelled."); - } + var body = new ClientAuthTicketAndVersionPacket + { + Name = GameMain.Client.Name, + OwnerKey = ownerKey, + AccountId = accountId, + AuthTicket = authTicket, + GameVersion = GameMain.Version.ToString(), + Language = GameSettings.CurrentConfig.Language.Value + }; - ClientSteamTicketAndVersionPacket body = new ClientSteamTicketAndVersionPacket - { - Name = GameMain.Client.Name, - OwnerKey = ownerKey, - SteamId = SteamManager.GetSteamId().Select(id => (AccountId)id), - SteamAuthTicket = steamAuthTicket.Bind(t => t.Data != null ? Option.Some(t.Data) : Option.None), - GameVersion = GameMain.Version.ToString(), - Language = GameSettings.CurrentConfig.Language.Value - }; - - SendMsgInternal(headers, body); + SendMsgInternal(headers, body); + }); break; } case ConnectionInitialization.ContentPackageOrder: { if (initializationStep - is ConnectionInitialization.SteamTicketAndVersion + is ConnectionInitialization.AuthInfoAndVersion or ConnectionInitialization.Password) { initializationStep = ConnectionInitialization.ContentPackageOrder; @@ -136,7 +164,7 @@ namespace Barotrauma.Networking break; } case ConnectionInitialization.Password: - if (initializationStep == ConnectionInitialization.SteamTicketAndVersion) + if (initializationStep == ConnectionInitialization.AuthInfoAndVersion) { initializationStep = ConnectionInitialization.Password; } @@ -152,6 +180,7 @@ namespace Barotrauma.Networking LocalizedString pwMsg = TextManager.Get("PasswordRequired"); + passwordMsgBox?.Close(); passwordMsgBox = new GUIMessageBox(pwMsg, "", new LocalizedString[] { TextManager.Get("OK"), TextManager.Get("Cancel") }, relativeSize: new Vector2(0.25f, 0.1f), minSize: new Point(400, GUI.IntScale(170))); var passwordHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), passwordMsgBox.Content.RectTransform), childAnchor: Anchor.TopCenter); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs index 1ed7a4e53..a2a1b7120 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs @@ -1,26 +1,25 @@ #nullable enable using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; using System.Text; +using System.Threading.Tasks; +using Barotrauma.Extensions; using Lidgren.Network; using Barotrauma.Steam; using System.Net.Sockets; namespace Barotrauma.Networking { - internal sealed class LidgrenClientPeer : ClientPeer + internal sealed class LidgrenClientPeer : ClientPeer { private NetClient? netClient; private readonly NetPeerConfiguration netPeerConfiguration; private readonly List incomingLidgrenMessages; - private LidgrenEndpoint lidgrenEndpoint => - ServerConnection is LidgrenConnection { Endpoint: LidgrenEndpoint result } - ? result - : throw new InvalidOperationException(); - - public LidgrenClientPeer(LidgrenEndpoint endpoint, Callbacks callbacks, Option ownerKey) : base(endpoint, callbacks, ownerKey) + public LidgrenClientPeer(LidgrenEndpoint endpoint, Callbacks callbacks, Option ownerKey) : base(endpoint, ((Endpoint)endpoint).ToEnumerable().ToImmutableArray(), callbacks, ownerKey) { ServerConnection = null; @@ -56,18 +55,9 @@ namespace Barotrauma.Networking netClient = new NetClient(netPeerConfiguration); - if (SteamManager.IsInitialized) - { - steamAuthTicket = SteamManager.GetAuthSessionTicketForMultiplayer(ServerEndpoint); - if (steamAuthTicket.IsNone()) - { - throw new Exception("GetAuthSessionTicket returned null"); - } - } + initializationStep = ConnectionInitialization.AuthInfoAndVersion; - initializationStep = ConnectionInitialization.SteamTicketAndVersion; - - if (!(ServerEndpoint is LidgrenEndpoint lidgrenEndpointValue)) + if (ServerEndpoint is not { } lidgrenEndpointValue) { throw new InvalidCastException($"Endpoint is not {nameof(LidgrenEndpoint)}"); } @@ -79,12 +69,25 @@ namespace Barotrauma.Networking netClient.Start(); - var netConnection = netClient.Connect(lidgrenEndpointValue.NetEndpoint); + TaskPool.Add( + $"{nameof(LidgrenClientPeer)}.GetAuthTicket", + AuthenticationTicket.Create(ServerEndpoint), + t => + { + if (!t.TryGetResult(out Option authenticationTicket)) + { + Close(PeerDisconnectPacket.WithReason(DisconnectReason.AuthenticationFailed)); + return; + } + authTicket = authenticationTicket; - ServerConnection = new LidgrenConnection(netConnection) - { - Status = NetworkConnectionStatus.Connected - }; + var netConnection = netClient.Connect(lidgrenEndpointValue.NetEndpoint); + + ServerConnection = new LidgrenConnection(netConnection) + { + Status = NetworkConnectionStatus.Connected + }; + }); isActive = true; } @@ -96,7 +99,7 @@ namespace Barotrauma.Networking ToolBox.ThrowIfNull(netClient); ToolBox.ThrowIfNull(incomingLidgrenMessages); - if (isOwner && !ChildServerRelay.IsProcessAlive) + if (IsOwner && !ChildServerRelay.IsProcessAlive) { var gameClient = GameMain.Client; Close(PeerDisconnectPacket.WithReason(DisconnectReason.ServerCrashed)); @@ -112,9 +115,11 @@ namespace Barotrauma.Networking foreach (NetIncomingMessage inc in incomingLidgrenMessages) { - if (!inc.SenderConnection.RemoteEndPoint.EquivalentTo(lidgrenEndpoint.NetEndpoint)) + var remoteEndpoint = new LidgrenEndpoint(inc.SenderEndPoint); + + if (remoteEndpoint != ServerEndpoint) { - DebugConsole.AddWarning($"Mismatched endpoint: expected {lidgrenEndpoint.NetEndpoint}, got {inc.SenderConnection.RemoteEndPoint}"); + DebugConsole.AddWarning($"Mismatched endpoint: expected {ServerEndpoint.NetEndpoint}, got {inc.SenderConnection.RemoteEndPoint}"); continue; } @@ -152,11 +157,7 @@ namespace Barotrauma.Networking } else { - if (initializationStep != ConnectionInitialization.Success) - { - callbacks.OnInitializationComplete.Invoke(); - initializationStep = ConnectionInitialization.Success; - } + OnInitializationComplete(); var packet = INetSerializableStruct.Read(inc); callbacks.OnMessageReceived.Invoke(packet.GetReadMessage(packetHeader.IsCompressed(), ServerConnection)); @@ -212,9 +213,6 @@ namespace Barotrauma.Networking netClient.Shutdown(peerDisconnectPacket.ToLidgrenStringRepresentation()); netClient = null; - if (steamAuthTicket.TryUnwrap(out var ticket)) { ticket.Cancel(); } - steamAuthTicket = Option.None; - callbacks.OnDisconnect.Invoke(peerDisconnectPacket); } @@ -270,7 +268,21 @@ namespace Barotrauma.Networking return netClient.SendMessage(msg.ToLidgren(netClient), deliveryMethod.ToLidgren()); } + protected override async Task> GetAccountId() + { + if (!EosInterface.Core.IsInitialized) { return SteamManager.GetSteamId().Select(id => (AccountId)id); } + + var selfPuids = EosInterface.IdQueries.GetLoggedInPuids(); + if (selfPuids.None()) { return Option.None; } + var accountIdsResult = await EosInterface.IdQueries.GetSelfExternalAccountIds(selfPuids.First()); + return accountIdsResult.TryUnwrapSuccess(out var accountIds) && accountIds.Length > 0 + ? Option.Some(accountIds[0]) + : Option.None; + } + #if DEBUG + + public override void ForceTimeOut() { netClient?.ServerConnection?.ForceTimeOut(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/P2PClientPeer.cs similarity index 59% rename from Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs rename to Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/P2PClientPeer.cs index 08318d13f..d6624b4cf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/P2PClientPeer.cs @@ -1,136 +1,142 @@ #nullable enable using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Text; -using Barotrauma.Steam; using System.Threading; +using System.Threading.Tasks; +using Barotrauma.Extensions; +using Barotrauma.Steam; namespace Barotrauma.Networking { - internal sealed class SteamP2PClientPeer : ClientPeer + internal sealed class P2PClientPeer : ClientPeer { - private readonly SteamId hostSteamId; private double timeout; private double heartbeatTimer; - private double connectionStatusTimer; private long sentBytes, receivedBytes; private readonly List incomingInitializationMessages = new List(); private readonly List incomingDataMessages = new List(); + private readonly MessageFragmenter fragmenter = new(); + private readonly MessageDefragmenter defragmenter = new(); - public SteamP2PClientPeer(SteamP2PEndpoint endpoint, Callbacks callbacks) : base(endpoint, callbacks, Option.None()) + private P2PSocket? socket; + + private static P2PEndpoint GetPrimaryEndpoint(ImmutableArray allEndpoints) + { + var steamEndpointOption = allEndpoints.OfType().FirstOrNone(); + var eosEndpointOption = allEndpoints.OfType().FirstOrNone(); + if (SteamManager.IsInitialized) + { + if (steamEndpointOption.TryUnwrap(out var steamEndpoint)) { return steamEndpoint; } + } + if (EosInterface.Core.IsInitialized) + { + if (eosEndpointOption.TryUnwrap(out var eosEndpoint)) { return eosEndpoint; } + } + + throw new Exception($"Couldn't pick out a primary endpoint: {string.Join(", ", allEndpoints.Select(e => e.GetType().Name))}"); + } + + public P2PClientPeer(ImmutableArray allEndpoints, Callbacks callbacks) + : base( + GetPrimaryEndpoint(allEndpoints), + allEndpoints.Cast().ToImmutableArray(), + callbacks, + Option.None) { ServerConnection = null; isActive = false; - - if (!(ServerEndpoint is SteamP2PEndpoint steamIdEndpoint)) - { - throw new InvalidCastException("endPoint is not SteamId"); - } - - hostSteamId = steamIdEndpoint.SteamId; } public override void Start() { - if (isActive) { return; } - ContentPackageOrderReceived = false; - steamAuthTicket = SteamManager.GetAuthSessionTicketForMultiplayer(ServerEndpoint); - //TODO: wait for GetAuthSessionTicketResponse_t + ServerConnection = ServerEndpoint.MakeConnectionFromEndpoint(); - if (steamAuthTicket == null) + var socketCallbacks = new P2PSocket.Callbacks(OnIncomingConnection, OnConnectionClosed, OnP2PData); + var socketCreateResult = ServerEndpoint switch { - throw new Exception("GetAuthSessionTicket returned null"); - } - - Steamworks.SteamNetworking.ResetActions(); - Steamworks.SteamNetworking.OnP2PSessionRequest = OnIncomingConnection; - Steamworks.SteamNetworking.OnP2PConnectionFailed = OnConnectionFailed; - - Steamworks.SteamNetworking.AllowP2PPacketRelay(true); - - ServerConnection = new SteamP2PConnection(hostSteamId); - ServerConnection.SetAccountInfo(new AccountInfo(hostSteamId)); - - var headers = new PeerPacketHeaders - { - DeliveryMethod = DeliveryMethod.Reliable, - PacketHeader = PacketHeader.IsConnectionInitializationStep, - Initialization = ConnectionInitialization.ConnectionStarted + EosP2PEndpoint => EosP2PSocket.Create(socketCallbacks), + SteamP2PEndpoint steamP2PEndpoint => SteamConnectSocket.Create(steamP2PEndpoint, socketCallbacks), + _ => throw new Exception($"Invalid server endpoint: {ServerEndpoint.GetType()} {ServerEndpoint}") }; - SendMsgInternal(headers, null); + socket = socketCreateResult.TryUnwrapSuccess(out var s) + ? s + : throw new Exception($"Failed to create socket for {ServerEndpoint}: {socketCreateResult}"); + TaskPool.Add( + $"{nameof(P2PClientPeer)}.GetAuthTicket", + AuthenticationTicket.Create(ServerEndpoint), + t => + { + if (!t.TryGetResult(out Option authenticationTicket)) + { + Close(PeerDisconnectPacket.WithReason(DisconnectReason.AuthenticationFailed)); + return; + } + authTicket = authenticationTicket; - initializationStep = ConnectionInitialization.SteamTicketAndVersion; + var headers = new PeerPacketHeaders + { + DeliveryMethod = DeliveryMethod.Reliable, + PacketHeader = PacketHeader.IsConnectionInitializationStep, + Initialization = ConnectionInitialization.ConnectionStarted + }; + SendMsgInternal(headers, null); + }); + initializationStep = ConnectionInitialization.AuthInfoAndVersion; timeout = NetworkConnection.TimeoutThreshold; heartbeatTimer = 1.0; - connectionStatusTimer = 0.0; isActive = true; } - private void OnIncomingConnection(Steamworks.SteamId steamId) + private bool OnIncomingConnection(P2PEndpoint remoteEndpoint) { - if (!isActive) { return; } + if (remoteEndpoint == ServerEndpoint) + { + return true; + } - if (steamId == hostSteamId.Value) + if (initializationStep != ConnectionInitialization.Password && + initializationStep != ConnectionInitialization.ContentPackageOrder && + initializationStep != ConnectionInitialization.Success) { - Steamworks.SteamNetworking.AcceptP2PSessionWithUser(steamId); - } - else if (initializationStep != ConnectionInitialization.Password && - initializationStep != ConnectionInitialization.ContentPackageOrder && - initializationStep != ConnectionInitialization.Success) - { - DebugConsole.ThrowError("Connection from incorrect SteamID was rejected: " + - $"expected {hostSteamId}," + - $"got {new SteamId(steamId)}"); + DebugConsole.AddWarning( + "Connection from incorrect endpoint was rejected: " + + $"expected {ServerEndpoint}, " + + $"got {remoteEndpoint}"); } + + return false; } - private void OnConnectionFailed(Steamworks.SteamId steamId, Steamworks.P2PSessionError error) + private void OnConnectionClosed(P2PEndpoint remoteEndpoint, PeerDisconnectPacket peerDisconnectPacket) { - if (!isActive) { return; } + if (remoteEndpoint != ServerEndpoint) { return; } - if (steamId != hostSteamId.Value) { return; } - - Close(PeerDisconnectPacket.SteamP2PError(error)); + Close(peerDisconnectPacket); } - private void OnP2PData(ulong steamId, byte[] data, int dataLength) + private void OnP2PData(P2PEndpoint senderEndpoint, IReadMessage inc) { if (!isActive) { return; } - if (steamId != hostSteamId.Value) { return; } + receivedBytes += inc.LengthBytes; + + if (senderEndpoint != ServerEndpoint) { return; } timeout = Screen.Selected == GameMain.GameScreen ? NetworkConnection.TimeoutThresholdInGame : NetworkConnection.TimeoutThreshold; - try - { - IReadMessage inc = new ReadOnlyMessage(data, false, 0, dataLength, ServerConnection); - ProcessP2PData(inc); - } - catch (Exception e) - { - string errorMsg = $"Client failed to read an incoming P2P message. {{{e}}}\n{e.StackTrace.CleanupStackTrace()}"; - GameAnalyticsManager.AddErrorEventOnce($"SteamP2PClientPeer.OnP2PData:ClientReadException{e.TargetSite}", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); -#if DEBUG - DebugConsole.ThrowError(errorMsg); -#else - if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.ThrowError(errorMsg); } -#endif - } - } - - private void ProcessP2PData(IReadMessage inc) - { - var (deliveryMethod, packetHeader, initialization) = INetSerializableStruct.Read(inc); + var (_, packetHeader, initialization) = INetSerializableStruct.Read(inc); if (!packetHeader.IsServerMessage()) { return; } @@ -138,9 +144,8 @@ namespace Barotrauma.Networking { if (!initialization.HasValue) { return; } - var relayPacket = INetSerializableStruct.Read(inc); + var relayPacket = INetSerializableStruct.Read(inc); - SteamManager.JoinLobby(relayPacket.LobbyID, false); if (initializationStep != ConnectionInitialization.Success) { incomingInitializationMessages.Add(new IncomingInitializationMessage @@ -150,6 +155,14 @@ namespace Barotrauma.Networking }); } } + else if (packetHeader.IsDataFragment()) + { + var completeMessageOption = defragmenter.ProcessIncomingFragment(INetSerializableStruct.Read(inc)); + if (!completeMessageOption.TryUnwrap(out var completeMessage)) { return; } + + int completeMessageLengthBits = completeMessage.Length * 8; + incomingDataMessages.Add(new ReadWriteMessage(completeMessage.ToArray(), 0, completeMessageLengthBits, copyBuf: false)); + } else if (packetHeader.IsHeartbeatMessage()) { return; //TODO: implement heartbeats @@ -177,41 +190,7 @@ namespace Barotrauma.Networking heartbeatTimer -= deltaTime; - if (initializationStep != ConnectionInitialization.Password && - initializationStep != ConnectionInitialization.ContentPackageOrder && - initializationStep != ConnectionInitialization.Success) - { - connectionStatusTimer -= deltaTime; - if (connectionStatusTimer <= 0.0) - { - if (Steamworks.SteamNetworking.GetP2PSessionState(hostSteamId.Value) is { } state) - { - if (state.P2PSessionError != Steamworks.P2PSessionError.None) - { - Close(PeerDisconnectPacket.SteamP2PError(state.P2PSessionError)); - } - } - else - { - Close(PeerDisconnectPacket.WithReason(DisconnectReason.Timeout)); - } - - connectionStatusTimer = 1.0f; - } - } - - for (int i = 0; i < 100; i++) - { - if (!Steamworks.SteamNetworking.IsP2PPacketAvailable()) { break; } - - var packet = Steamworks.SteamNetworking.ReadP2PPacket(); - if (packet is { SteamId: var steamId, Data: var data }) - { - OnP2PData(steamId, data, data.Length); - if (!isActive) { return; } - receivedBytes += data.Length; - } - } + socket?.ProcessIncomingMessages(); GameMain.Client?.NetStats?.AddValue(NetStats.NetStatType.ReceivedBytes, receivedBytes); GameMain.Client?.NetStats?.AddValue(NetStats.NetStatType.SentBytes, sentBytes); @@ -258,8 +237,7 @@ namespace Barotrauma.Networking analyticsTag: "NoContentPackages"); return; } - callbacks.OnInitializationComplete.Invoke(); - initializationStep = ConnectionInitialization.Success; + OnInitializationComplete(); } else { @@ -287,6 +265,21 @@ namespace Barotrauma.Networking if (!isActive) { return; } byte[] bufAux = msg.PrepareForSending(compressPastThreshold, out bool isCompressed, out _); + if (bufAux.Length > MessageFragment.MaxSize) + { + var fragments = fragmenter.FragmentMessage(msg.Buffer.AsSpan()[..msg.LengthBytes]); + foreach (var fragment in fragments) + { + var fragmentHeaders = new PeerPacketHeaders + { + DeliveryMethod = DeliveryMethod.Reliable, + PacketHeader = PacketHeader.IsDataFragment, + Initialization = null + }; + SendMsgInternal(fragmentHeaders, fragment); + } + return; + } var headers = new PeerPacketHeaders { @@ -346,8 +339,6 @@ namespace Barotrauma.Networking { if (!isActive) { return; } - SteamManager.LeaveLobby(); - isActive = false; var headers = new PeerPacketHeaders @@ -360,11 +351,9 @@ namespace Barotrauma.Networking Thread.Sleep(100); - Steamworks.SteamNetworking.ResetActions(); - Steamworks.SteamNetworking.CloseP2PSessionWithUser(hostSteamId.Value); - - if (steamAuthTicket.TryUnwrap(out var ticket)) { ticket.Cancel(); } - steamAuthTicket = Option.None; + socket?.CloseConnection(ServerEndpoint); + socket?.Dispose(); + socket = null; callbacks.OnDisconnect.Invoke(peerDisconnectPacket); } @@ -374,33 +363,62 @@ namespace Barotrauma.Networking IWriteMessage msgToSend = new WriteOnlyMessage(); msgToSend.WriteNetSerializableStruct(headers); body?.Write(msgToSend); - ForwardToSteamP2P(msgToSend, headers.DeliveryMethod); + ForwardToRemotePeer(msgToSend, headers.DeliveryMethod); } - private void ForwardToSteamP2P(IWriteMessage msg, DeliveryMethod deliveryMethod) + private void ForwardToRemotePeer(IWriteMessage msg, DeliveryMethod deliveryMethod) { + if (!isActive) { return; } + if (socket is null) { return; } + heartbeatTimer = 5.0; + int length = msg.LengthBytes; - bool successSend = Steamworks.SteamNetworking.SendP2PPacket(hostSteamId.Value, msg.Buffer, length, 0, deliveryMethod.ToSteam()); + if (length + 4 >= MsgConstants.MTU) + { + DebugConsole.Log($"WARNING: message length comes close to exceeding MTU, forcing reliable send ({length} bytes)"); + deliveryMethod = DeliveryMethod.Reliable; + } + + bool success = socket.SendMessage(ServerEndpoint, msg, deliveryMethod); + sentBytes += length; - if (successSend) { return; } + if (success) { return; } if (deliveryMethod is DeliveryMethod.Unreliable) { DebugConsole.Log($"WARNING: message couldn't be sent unreliably, forcing reliable send ({length} bytes)"); - successSend = Steamworks.SteamNetworking.SendP2PPacket(hostSteamId.Value, msg.Buffer, length, 0, DeliveryMethod.Reliable.ToSteam()); + success = socket.SendMessage(ServerEndpoint, msg, DeliveryMethod.Reliable); sentBytes += length; } - if (!successSend) + if (!success) { DebugConsole.AddWarning($"Failed to send message to remote peer! ({length} bytes)"); } } + protected override async Task> GetAccountId() + { + if (SteamManager.IsInitialized) { return SteamManager.GetSteamId().Select(id => (AccountId)id); } + + if (EosInterface.IdQueries.GetLoggedInPuids() is not { Length: > 0 } puids) + { + return Option.None; + } + var externalAccountIdsResult = await EosInterface.IdQueries.GetSelfExternalAccountIds(puids[0]); + if (!externalAccountIdsResult.TryUnwrapSuccess(out var externalAccountIds) + || externalAccountIds is not { Length: > 0 }) + { + return Option.None; + } + return Option.Some(externalAccountIds[0]); + } + #if DEBUG + public override void ForceTimeOut() { timeout = 0.0f; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/P2POwnerPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/P2POwnerPeer.cs new file mode 100644 index 000000000..0b2a340da --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/P2POwnerPeer.cs @@ -0,0 +1,557 @@ +#nullable enable +using Barotrauma.Extensions; +using Barotrauma.Steam; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Barotrauma.Networking +{ + sealed class P2POwnerPeer : ClientPeer + { + private P2PSocket? socket; + private readonly ImmutableDictionary authenticators; + + private readonly P2PEndpoint selfPrimaryEndpoint; + private AccountInfo selfAccountInfo; + + private long sentBytes, receivedBytes; + + private sealed class RemotePeer + { + public enum AuthenticationStatus + { + NotAuthenticated, + AuthenticationPending, + SuccessfullyAuthenticated + } + + public readonly P2PEndpoint Endpoint; + public AccountInfo AccountInfo; + + public readonly record struct DisconnectInfo( + double TimeToGiveUp, + PeerDisconnectPacket Packet); + + public Option PendingDisconnect; + public AuthenticationStatus AuthStatus; + + public readonly record struct UnauthedMessage(byte[] Bytes, int LengthBytes); + + public readonly List UnauthedMessages; + + public RemotePeer(P2PEndpoint endpoint) + { + Endpoint = endpoint; + AccountInfo = AccountInfo.None; + PendingDisconnect = Option.None; + AuthStatus = AuthenticationStatus.NotAuthenticated; + + UnauthedMessages = new List(); + } + } + + private readonly List remotePeers = new(); + + public P2POwnerPeer(Callbacks callbacks, int ownerKey, ImmutableArray allEndpoints) : + base(new PipeEndpoint(), allEndpoints.Cast().ToImmutableArray(), callbacks, Option.Some(ownerKey)) + { + ServerConnection = null; + + isActive = false; + + var selfSteamEndpoint = allEndpoints.FirstOrNone(e => e is SteamP2PEndpoint); + var selfEosEndpoint = allEndpoints.FirstOrNone(e => e is EosP2PEndpoint); + var selfPrimaryEndpointOption = selfSteamEndpoint.Fallback(selfEosEndpoint); + if (!selfPrimaryEndpointOption.TryUnwrap(out var selfPrimaryEndpointNotNull)) + { + throw new Exception("Could not determine endpoint for P2POwnerPeer"); + } + selfPrimaryEndpoint = selfPrimaryEndpointNotNull; + selfAccountInfo = AccountInfo.None; + authenticators = Authenticator.GetAuthenticatorsForHost(Option.Some(selfPrimaryEndpoint)); + } + + public override void Start() + { + if (isActive) { return; } + + initializationStep = ConnectionInitialization.AuthInfoAndVersion; + + ServerConnection = new PipeConnection(Option.None) + { + Status = NetworkConnectionStatus.Connected + }; + + remotePeers.Clear(); + + var socketCallbacks = new P2PSocket.Callbacks(OnIncomingConnection, OnConnectionClosed, OnP2PData); + var socketCreateResult = DualStackP2PSocket.Create(socketCallbacks); + socket = socketCreateResult.TryUnwrapSuccess(out var s) + ? s + : throw new Exception($"Failed to create dual-stack socket: {socketCreateResult}"); + + TaskPool.Add("P2POwnerPeer.GetAccountId", GetAccountId(), t => + { + if (t.TryGetResult(out Option accountIdOption) && accountIdOption.TryUnwrap(out var accountId)) + { + selfAccountInfo = new AccountInfo(accountId); + } + + if (selfAccountInfo.IsNone) + { + Close(PeerDisconnectPacket.WithReason(DisconnectReason.AuthenticationFailed)); + } + }); + + isActive = true; + } + + private bool OnIncomingConnection(P2PEndpoint remoteEndpoint) + { + if (!isActive) { return false; } + + if (remotePeers.None(p => p.Endpoint == remoteEndpoint)) + { + remotePeers.Add(new RemotePeer(remoteEndpoint)); + } + + return true; + } + + private void OnConnectionClosed(P2PEndpoint remoteEndpoint, PeerDisconnectPacket disconnectPacket) + { + var remotePeer + = remotePeers.Find(p => p.Endpoint == remoteEndpoint); + if (remotePeer is null) { return; } + CommunicatePeerDisconnectToServerProcess( + remotePeer, + remotePeer.PendingDisconnect.Select(d => d.Packet).Fallback(disconnectPacket)); + } + + private void OnP2PData(P2PEndpoint senderEndpoint, IReadMessage inc) + { + if (!isActive) { return; } + + receivedBytes += inc.LengthBytes; + + var remotePeer = remotePeers.Find(p => p.Endpoint == senderEndpoint); + if (remotePeer is null) { return; } + if (remotePeer.PendingDisconnect.IsSome()) { return; } + + var peerPacketHeaders = INetSerializableStruct.Read(inc); + + PacketHeader packetHeader = peerPacketHeaders.PacketHeader; + + if (packetHeader.IsConnectionInitializationStep()) + { + ConnectionInitialization initialization = peerPacketHeaders.Initialization ?? throw new Exception("Initialization step missing"); + if (initialization == ConnectionInitialization.AuthInfoAndVersion + && remotePeer.AuthStatus == RemotePeer.AuthenticationStatus.NotAuthenticated) + { + StartAuthTask(inc, remotePeer); + } + } + + if (remotePeer.AuthStatus == RemotePeer.AuthenticationStatus.AuthenticationPending) + { + remotePeer.UnauthedMessages.Add(new RemotePeer.UnauthedMessage(inc.Buffer, inc.LengthBytes)); + } + else + { + IWriteMessage outMsg = new WriteOnlyMessage(); + outMsg.WriteNetSerializableStruct(new P2POwnerToServerHeader + { + EndpointStr = remotePeer.Endpoint.StringRepresentation, + AccountInfo = remotePeer.AccountInfo + }); + outMsg.WriteBytes(inc.Buffer, 0, inc.LengthBytes); + + ForwardToServerProcess(outMsg); + } + } + + private void StartAuthTask(IReadMessage inc, RemotePeer remotePeer) + { + remotePeer.AuthStatus = RemotePeer.AuthenticationStatus.AuthenticationPending; + + var packet = INetSerializableStruct.Read(inc); + + void failAuth() + { + CommunicateDisconnectToRemotePeer(remotePeer, PeerDisconnectPacket.WithReason(DisconnectReason.AuthenticationFailed)); + } + + if (!packet.AuthTicket.TryUnwrap(out var authenticationTicket)) + { + failAuth(); + return; + } + if (!authenticators.TryGetValue(authenticationTicket.Kind, out var authenticator)) + { + failAuth(); + return; + } + TaskPool.Add($"P2POwnerPeer.VerifyRemotePeerAccountId", + authenticator.VerifyTicket(authenticationTicket), + t => + { + if (!t.TryGetResult(out AccountInfo accountInfo) + || accountInfo.IsNone) + { + failAuth(); + return; + } + + remotePeer.AccountInfo = accountInfo; + remotePeer.AuthStatus = RemotePeer.AuthenticationStatus.SuccessfullyAuthenticated; + foreach (var unauthedMessage in remotePeer.UnauthedMessages) + { + IWriteMessage msg = new WriteOnlyMessage(); + msg.WriteNetSerializableStruct(new P2POwnerToServerHeader + { + EndpointStr = remotePeer.Endpoint.StringRepresentation, + AccountInfo = accountInfo + }); + msg.WriteBytes(unauthedMessage.Bytes, 0, unauthedMessage.LengthBytes); + ForwardToServerProcess(msg); + } + remotePeer.UnauthedMessages.Clear(); + }); + } + + public override void Update(float deltaTime) + { + if (!isActive) { return; } + + if (ChildServerRelay.HasShutDown || ChildServerRelay.Process is not { HasExited: false }) + { + Close(PeerDisconnectPacket.WithReason(DisconnectReason.ServerCrashed)); + var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage); + msgBox.Buttons[0].OnClicked += (btn, obj) => + { + GameMain.MainMenuScreen.Select(); + return false; + }; + return; + } + + if (selfAccountInfo.IsNone) { return; } + + for (int i = remotePeers.Count - 1; i >= 0; i--) + { + if (remotePeers[i].PendingDisconnect.TryUnwrap(out var pendingDisconnect) && pendingDisconnect.TimeToGiveUp < Timing.TotalTime) + { + CommunicatePeerDisconnectToServerProcess(remotePeers[i], pendingDisconnect.Packet); + } + } + + socket?.ProcessIncomingMessages(); + + GameMain.Client?.NetStats?.AddValue(NetStats.NetStatType.ReceivedBytes, receivedBytes); + GameMain.Client?.NetStats?.AddValue(NetStats.NetStatType.SentBytes, sentBytes); + + foreach (var incBuf in ChildServerRelay.Read()) + { + ChildServerRelay.DisposeLocalHandles(); + IReadMessage inc = new ReadOnlyMessage(incBuf, false, 0, incBuf.Length, ServerConnection); + HandleServerMessage(inc); + } + } + + private void HandleServerMessage(IReadMessage inc) + { + if (!isActive) { return; } + + var recipientInfo = INetSerializableStruct.Read(inc); + if (!recipientInfo.Endpoint.TryUnwrap(out var recipientEndpoint)) { return; } + var peerPacketHeaders = INetSerializableStruct.Read(inc); + + if (recipientEndpoint != selfPrimaryEndpoint) + { + HandleMessageForRemotePeer(peerPacketHeaders, recipientEndpoint, inc); + } + else + { + HandleMessageForOwner(peerPacketHeaders, inc); + } + } + + private static byte[] GetRemainingBytes(IReadMessage msg) + { + return msg.Buffer[msg.BytePosition..msg.LengthBytes]; + } + + private void HandleMessageForRemotePeer(PeerPacketHeaders peerPacketHeaders, P2PEndpoint recipientEndpoint, IReadMessage inc) + { + var (deliveryMethod, packetHeader, initialization) = peerPacketHeaders; + + if (!packetHeader.IsServerMessage()) + { + DebugConsole.ThrowError("Received non-server message meant for remote peer"); + return; + } + + RemotePeer? peer = remotePeers.Find(p => p.Endpoint == recipientEndpoint); + if (peer is null) { return; } + + if (packetHeader.IsDisconnectMessage()) + { + var packet = INetSerializableStruct.Read(inc); + CommunicateDisconnectToRemotePeer(peer, packet); + return; + } + + IWriteMessage outMsg = new WriteOnlyMessage(); + + outMsg.WriteNetSerializableStruct(new PeerPacketHeaders + { + DeliveryMethod = deliveryMethod, + PacketHeader = packetHeader, + Initialization = initialization + }); + + if (packetHeader.IsConnectionInitializationStep()) + { + var initRelayPacket = new P2PInitializationRelayPacket + { + LobbyID = 0, + Message = new PeerPacketMessage + { + Buffer = GetRemainingBytes(inc) + } + }; + + outMsg.WriteNetSerializableStruct(initRelayPacket); + } + else + { + byte[] userMessage = GetRemainingBytes(inc); + outMsg.WriteBytes(userMessage, 0, userMessage.Length); + } + + ForwardToRemotePeer(deliveryMethod, recipientEndpoint, outMsg); + } + + private void HandleMessageForOwner(PeerPacketHeaders peerPacketHeaders, IReadMessage inc) + { + var (_, packetHeader, _) = peerPacketHeaders; + + if (packetHeader.IsDisconnectMessage()) + { + DebugConsole.ThrowError("Received disconnect message from owned server"); + return; + } + + if (!packetHeader.IsServerMessage()) + { + DebugConsole.ThrowError("Received non-server message from owned server"); + return; + } + + if (packetHeader.IsHeartbeatMessage()) + { + return; //no timeout since we're using pipes, ignore this message + } + + if (packetHeader.IsConnectionInitializationStep()) + { + if (selfAccountInfo.IsNone) { throw new InvalidOperationException($"Cannot initialize {nameof(P2POwnerPeer)} because {nameof(selfAccountInfo)} is not defined"); } + IWriteMessage outMsg = new WriteOnlyMessage(); + outMsg.WriteNetSerializableStruct(new P2POwnerToServerHeader + { + EndpointStr = selfPrimaryEndpoint.StringRepresentation, + AccountInfo = selfAccountInfo + }); + outMsg.WriteNetSerializableStruct(new PeerPacketHeaders + { + DeliveryMethod = DeliveryMethod.Reliable, + PacketHeader = PacketHeader.IsConnectionInitializationStep, + Initialization = ConnectionInitialization.AuthInfoAndVersion + }); + outMsg.WriteNetSerializableStruct(new P2PInitializationOwnerPacket( + Name: GameMain.Client.Name, + AccountId: selfAccountInfo.AccountId.Fallback(default(AccountId)!))); + ForwardToServerProcess(outMsg); + } + else + { + OnInitializationComplete(); + + PeerPacketMessage packet = INetSerializableStruct.Read(inc); + IReadMessage msg = new ReadOnlyMessage(packet.Buffer, packetHeader.IsCompressed(), 0, packet.Length, ServerConnection); + callbacks.OnMessageReceived.Invoke(msg); + } + } + + private void CommunicateDisconnectToRemotePeer(RemotePeer peer, PeerDisconnectPacket peerDisconnectPacket) + { + if (peer.PendingDisconnect.IsNone()) + { + peer.PendingDisconnect = Option.Some( + new RemotePeer.DisconnectInfo( + Timing.TotalTime + 3f, + peerDisconnectPacket)); + } + + IWriteMessage outMsg = new WriteOnlyMessage(); + outMsg.WriteNetSerializableStruct(new PeerPacketHeaders + { + DeliveryMethod = DeliveryMethod.Reliable, + PacketHeader = PacketHeader.IsServerMessage | PacketHeader.IsDisconnectMessage + }); + outMsg.WriteNetSerializableStruct(peerDisconnectPacket); + + ForwardToRemotePeer(DeliveryMethod.Reliable, peer.Endpoint, outMsg); + } + + private void CommunicatePeerDisconnectToServerProcess(RemotePeer peer, PeerDisconnectPacket peerDisconnectPacket) + { + if (!remotePeers.Remove(peer)) { return; } + + IWriteMessage outMsg = new WriteOnlyMessage(); + outMsg.WriteNetSerializableStruct(new P2POwnerToServerHeader + { + EndpointStr = peer.Endpoint.StringRepresentation, + AccountInfo = peer.AccountInfo + }); + outMsg.WriteNetSerializableStruct(new PeerPacketHeaders + { + DeliveryMethod = DeliveryMethod.Reliable, + PacketHeader = PacketHeader.IsDisconnectMessage + }); + outMsg.WriteNetSerializableStruct(peerDisconnectPacket); + if (peer.AccountInfo.AccountId.TryUnwrap(out var accountId)) + { + authenticators.Values.ForEach(authenticator => authenticator.EndAuthSession(accountId)); + } + + ForwardToServerProcess(outMsg); + + socket?.CloseConnection(peer.Endpoint); + } + + public override void SendPassword(string password) + { + //owner doesn't send passwords + } + + public override void Close(PeerDisconnectPacket peerDisconnectPacket) + { + if (!isActive) { return; } + + isActive = false; + + for (int i = remotePeers.Count - 1; i >= 0; i--) + { + CommunicateDisconnectToRemotePeer(remotePeers[i], peerDisconnectPacket); + } + + Thread.Sleep(100); + + for (int i = remotePeers.Count - 1; i >= 0; i--) + { + CommunicatePeerDisconnectToServerProcess(remotePeers[i], peerDisconnectPacket); + } + + socket?.Dispose(); + socket = null; + + callbacks.OnDisconnect.Invoke(peerDisconnectPacket); + } + + public override void Send(IWriteMessage msg, DeliveryMethod deliveryMethod, bool compressPastThreshold = true) + { + if (!isActive) { return; } + + IWriteMessage msgToSend = new WriteOnlyMessage(); + byte[] msgData = msg.PrepareForSending(compressPastThreshold, out bool isCompressed, out _); + msgToSend.WriteNetSerializableStruct(new P2POwnerToServerHeader + { + EndpointStr = selfPrimaryEndpoint.StringRepresentation, + AccountInfo = selfAccountInfo + }); + msgToSend.WriteNetSerializableStruct(new PeerPacketHeaders + { + DeliveryMethod = deliveryMethod, + PacketHeader = isCompressed ? PacketHeader.IsCompressed : PacketHeader.None + }); + msgToSend.WriteNetSerializableStruct(new PeerPacketMessage + { + Buffer = msgData + }); + ForwardToServerProcess(msgToSend); + } + + protected override void SendMsgInternal(PeerPacketHeaders headers, INetSerializableStruct? body) + { + //not currently used by P2POwnerPeer + throw new NotImplementedException(); + } + + private static void ForwardToServerProcess(IWriteMessage msg) + { + byte[] bufToSend = new byte[msg.LengthBytes]; + msg.Buffer[..msg.LengthBytes].CopyTo(bufToSend.AsSpan()); + ChildServerRelay.Write(bufToSend); + } + + private void ForwardToRemotePeer(DeliveryMethod deliveryMethod, P2PEndpoint recipient, IWriteMessage outMsg) + { + if (socket is null) { return; } + + int length = outMsg.LengthBytes; + + if (length + 4 >= MsgConstants.MTU) + { + DebugConsole.Log($"WARNING: message length comes close to exceeding MTU, forcing reliable send ({length} bytes)"); + deliveryMethod = DeliveryMethod.Reliable; + } + + var success = socket.SendMessage(recipient, outMsg, deliveryMethod); + + sentBytes += length; + + if (success) { return; } + + if (deliveryMethod is DeliveryMethod.Unreliable) + { + DebugConsole.Log($"WARNING: message couldn't be sent unreliably, forcing reliable send ({length} bytes)"); + success = socket.SendMessage(recipient, outMsg, DeliveryMethod.Reliable); + sentBytes += length; + } + + if (!success) + { + DebugConsole.AddWarning($"Failed to send message to remote peer! ({length} bytes)"); + } + } + + protected override async Task> GetAccountId() + { + if (SteamManager.IsInitialized) { return SteamManager.GetSteamId().Select(id => (AccountId)id); } + + if (EosInterface.IdQueries.GetLoggedInPuids() is not { Length: > 0 } puids) + { + return Option.None; + } + var externalAccountIdsResult = await EosInterface.IdQueries.GetSelfExternalAccountIds(puids[0]); + if (!externalAccountIdsResult.TryUnwrapSuccess(out var externalAccountIds) + || externalAccountIds is not { Length: > 0 }) + { + return Option.None; + } + return Option.Some(externalAccountIds[0]); + } + +#if DEBUG + public override void ForceTimeOut() + { + //TODO: reimplement? + } +#endif + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs deleted file mode 100644 index 27f6e1724..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs +++ /dev/null @@ -1,492 +0,0 @@ -#nullable enable -using Barotrauma.Steam; -using System; -using System.Collections.Generic; -using System.Threading; -using Barotrauma.Extensions; - -namespace Barotrauma.Networking -{ - sealed class SteamP2POwnerPeer : ClientPeer - { - private readonly SteamId selfSteamID; - private UInt64 ownerKey64 => unchecked((UInt64)ownerKey.Fallback(0)); - - private SteamId ReadSteamId(IReadMessage inc) => new SteamId(inc.ReadUInt64() ^ ownerKey64); - private void WriteSteamId(IWriteMessage msg, SteamId val) => msg.WriteUInt64(val.Value ^ ownerKey64); - - private long sentBytes, receivedBytes; - - private sealed class RemotePeer - { - public readonly SteamId SteamId; - public Option OwnerSteamId; - public double? DisconnectTime; - public bool Authenticating; - public bool Authenticated; - - public readonly struct UnauthedMessage - { - public readonly SteamId Sender; - public readonly byte[] Bytes; - public readonly int Length; - - public UnauthedMessage(SteamId sender, byte[] bytes) - { - Sender = sender; - Bytes = bytes; - Length = bytes.Length; - } - } - - public readonly List UnauthedMessages; - - public RemotePeer(SteamId steamId) - { - SteamId = steamId; - OwnerSteamId = Option.None(); - DisconnectTime = null; - Authenticating = false; - Authenticated = false; - - UnauthedMessages = new List(); - } - } - - private List remotePeers = null!; - - public SteamP2POwnerPeer(Callbacks callbacks, int ownerKey) : base(new PipeEndpoint(), callbacks, Option.Some(ownerKey)) - { - ServerConnection = null; - - isActive = false; - - selfSteamID = SteamManager.GetSteamId().TryUnwrap(out var steamId) - ? steamId - : throw new InvalidOperationException("Steamworks not initialized"); - } - - - public override void Start() - { - if (isActive) { return; } - - initializationStep = ConnectionInitialization.SteamTicketAndVersion; - - ServerConnection = new PipeConnection(selfSteamID) - { - Status = NetworkConnectionStatus.Connected - }; - - remotePeers = new List(); - - Steamworks.SteamNetworking.ResetActions(); - Steamworks.SteamNetworking.OnP2PSessionRequest = OnIncomingConnection; - Steamworks.SteamUser.OnValidateAuthTicketResponse += OnAuthChange; - - Steamworks.SteamNetworking.AllowP2PPacketRelay(true); - - isActive = true; - } - - private void OnAuthChange(Steamworks.SteamId steamId, Steamworks.SteamId ownerId, Steamworks.AuthResponse status) - { - RemotePeer? remotePeer = remotePeers.Find(p => p.SteamId.Value == steamId); - - if (remotePeer == null) { return; } - - if (status == Steamworks.AuthResponse.OK) - { - if (remotePeer.Authenticated) { return; } - - SteamId ownerSteamId = new SteamId(ownerId); - remotePeer.OwnerSteamId = Option.Some(ownerSteamId); - remotePeer.Authenticated = true; - remotePeer.Authenticating = false; - foreach (var unauthedMessage in remotePeer.UnauthedMessages) - { - IWriteMessage msg = new WriteOnlyMessage(); - WriteSteamId(msg, unauthedMessage.Sender); - WriteSteamId(msg, ownerSteamId); - msg.WriteBytes(unauthedMessage.Bytes, 0, unauthedMessage.Length); - ForwardToServerProcess(msg); - } - - remotePeer.UnauthedMessages.Clear(); - } - else - { - DisconnectPeer(remotePeer, PeerDisconnectPacket.SteamAuthError(status)); - } - } - - private void OnIncomingConnection(Steamworks.SteamId steamId) - { - if (!isActive) { return; } - - if (remotePeers.None(p => p.SteamId.Value == steamId)) - { - remotePeers.Add(new RemotePeer(new SteamId(steamId))); - } - - Steamworks.SteamNetworking.AcceptP2PSessionWithUser(steamId); //accept all connections, the server will figure things out later - } - - private void OnP2PData(ulong steamId, IReadMessage inc) - { - if (!isActive) { return; } - - RemotePeer? remotePeer = remotePeers.Find(p => p.SteamId.Value == steamId); - if (remotePeer == null) { return; } - - if (remotePeer.DisconnectTime != null) { return; } - - try - { - ProcessP2PData(steamId, remotePeer, inc); - } - catch (Exception e) - { - string errorMsg = $"Server failed to read an incoming P2P message. {{{e}}}\n{e.StackTrace.CleanupStackTrace()}"; - GameAnalyticsManager.AddErrorEventOnce($"SteamP2POwnerPeer.OnP2PData:OwnerReadException{e.TargetSite}", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); -#if DEBUG - DebugConsole.ThrowError(errorMsg); -#else - if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.ThrowError(errorMsg); } -#endif - } - } - - private void ProcessP2PData(ulong steamId, RemotePeer remotePeer, IReadMessage inc) - { - var (deliveryMethod, packetHeader, connectionInitialization) = INetSerializableStruct.Read(inc); - - if (remotePeer is { Authenticated: false, Authenticating: false } && packetHeader.IsConnectionInitializationStep()) - { - remotePeer.DisconnectTime = null; - - ConnectionInitialization initialization = connectionInitialization ?? throw new Exception("Initialization step missing"); - if (initialization == ConnectionInitialization.SteamTicketAndVersion) - { - remotePeer.Authenticating = true; - - var packet = INetSerializableStruct.Read(inc); - - packet.SteamAuthTicket.TryUnwrap(out var ticket); - - Steamworks.BeginAuthResult authSessionStartState = SteamManager.StartAuthSession(ticket, steamId); - if (authSessionStartState != Steamworks.BeginAuthResult.OK) - { - DisconnectPeer(remotePeer, PeerDisconnectPacket.SteamAuthError(authSessionStartState)); - return; - } - } - } - - var steamUserId = new SteamId(steamId); - if (remotePeer.Authenticating) - { - remotePeer.UnauthedMessages.Add(new RemotePeer.UnauthedMessage(steamUserId, inc.Buffer)); - } - else - { - IWriteMessage outMsg = new WriteOnlyMessage(); - WriteSteamId(outMsg, steamUserId); - WriteSteamId(outMsg, remotePeer.OwnerSteamId.Fallback(steamUserId)); - outMsg.WriteBytes(inc.Buffer, 0, inc.LengthBytes); - - ForwardToServerProcess(outMsg); - } - - } - - public override void Update(float deltaTime) - { - if (!isActive) { return; } - - if (ChildServerRelay.HasShutDown || !ChildServerRelay.IsProcessAlive) - { - var gameClient = GameMain.Client; - Close(PeerDisconnectPacket.WithReason(DisconnectReason.ServerCrashed)); - gameClient?.CreateServerCrashMessage(); - return; - } - - for (int i = remotePeers.Count - 1; i >= 0; i--) - { - if (remotePeers[i].DisconnectTime != null && remotePeers[i].DisconnectTime < Timing.TotalTime) - { - ClosePeerSession(remotePeers[i]); - } - } - - for (int i = 0; i < 100; i++) - { - if (!Steamworks.SteamNetworking.IsP2PPacketAvailable()) { break; } - - var packet = Steamworks.SteamNetworking.ReadP2PPacket(); - if (packet is { SteamId: var steamId, Data: var data }) - { - OnP2PData(steamId, new ReadWriteMessage(data, 0, data.Length * 8, false)); - receivedBytes += data.Length; - } - } - - GameMain.Client?.NetStats?.AddValue(NetStats.NetStatType.ReceivedBytes, receivedBytes); - GameMain.Client?.NetStats?.AddValue(NetStats.NetStatType.SentBytes, sentBytes); - - while (ChildServerRelay.Read(out byte[] incBuf)) - { - ChildServerRelay.DisposeLocalHandles(); - IReadMessage inc = new ReadOnlyMessage(incBuf, false, 0, incBuf.Length, ServerConnection); - HandleDataMessage(inc); - } - } - - private void HandleDataMessage(IReadMessage inc) - { - if (!isActive) { return; } - - SteamId recipientSteamId = ReadSteamId(inc); - - var peerPacketHeaders = INetSerializableStruct.Read(inc); - - if (recipientSteamId != selfSteamID) - { - HandleMessageForRemotePeer(peerPacketHeaders, recipientSteamId, inc); - } - else - { - HandleMessageForOwner(peerPacketHeaders, inc); - } - } - - private static byte[] GetRemainingBytes(IReadMessage msg) - { - return msg.Buffer[msg.BytePosition..msg.LengthBytes]; - } - - private void HandleMessageForRemotePeer(PeerPacketHeaders peerPacketHeaders, SteamId recipientSteamId, IReadMessage inc) - { - var (deliveryMethod, packetHeader, initialization) = peerPacketHeaders; - - if (!packetHeader.IsServerMessage()) - { - DebugConsole.ThrowError("Received non-server message meant for remote peer"); - return; - } - - RemotePeer? peer = remotePeers.Find(p => p.SteamId == recipientSteamId); - if (peer is null) { return; } - - if (packetHeader.IsDisconnectMessage()) - { - var packet = INetSerializableStruct.Read(inc); - DisconnectPeer(peer, packet); - return; - } - - IWriteMessage outMsg = new WriteOnlyMessage(); - - outMsg.WriteNetSerializableStruct(new PeerPacketHeaders - { - DeliveryMethod = deliveryMethod, - PacketHeader = packetHeader, - Initialization = initialization - }); - - if (packetHeader.IsConnectionInitializationStep()) - { - var initRelayPacket = new SteamP2PInitializationRelayPacket - { - LobbyID = SteamManager.CurrentLobbyID, - Message = new PeerPacketMessage - { - Buffer = GetRemainingBytes(inc) - } - }; - - outMsg.WriteNetSerializableStruct(initRelayPacket); - } - else - { - byte[] userMessage = GetRemainingBytes(inc); - outMsg.WriteBytes(userMessage, 0, userMessage.Length); - } - - ForwardToRemotePeer(deliveryMethod, recipientSteamId, outMsg); - } - - private void HandleMessageForOwner(PeerPacketHeaders peerPacketHeaders, IReadMessage inc) - { - var (_, packetHeader, _) = peerPacketHeaders; - - if (packetHeader.IsDisconnectMessage()) - { - DebugConsole.ThrowError("Received disconnect message from owned server"); - return; - } - - if (!packetHeader.IsServerMessage()) - { - DebugConsole.ThrowError("Received non-server message from owned server"); - return; - } - - if (packetHeader.IsHeartbeatMessage()) - { - return; //no timeout since we're using pipes, ignore this message - } - - if (packetHeader.IsConnectionInitializationStep()) - { - IWriteMessage outMsg = new WriteOnlyMessage(); - WriteSteamId(outMsg, selfSteamID); - WriteSteamId(outMsg, selfSteamID); - outMsg.WriteNetSerializableStruct(new PeerPacketHeaders - { - DeliveryMethod = DeliveryMethod.Reliable, - PacketHeader = PacketHeader.IsConnectionInitializationStep, - Initialization = ConnectionInitialization.SteamTicketAndVersion - }); - outMsg.WriteNetSerializableStruct(new SteamP2PInitializationOwnerPacket - { - OwnerName = GameMain.Client.Name - }); - ForwardToServerProcess(outMsg); - } - else - { - if (initializationStep != ConnectionInitialization.Success) - { - callbacks.OnInitializationComplete.Invoke(); - initializationStep = ConnectionInitialization.Success; - } - - PeerPacketMessage packet = INetSerializableStruct.Read(inc); - IReadMessage msg = new ReadOnlyMessage(packet.Buffer, packetHeader.IsCompressed(), 0, packet.Length, ServerConnection); - callbacks.OnMessageReceived.Invoke(msg); - } - } - - private void DisconnectPeer(RemotePeer peer, PeerDisconnectPacket peerDisconnectPacket) - { - peer.DisconnectTime ??= Timing.TotalTime + 1.0; - - IWriteMessage outMsg = new WriteOnlyMessage(); - outMsg.WriteNetSerializableStruct(new PeerPacketHeaders - { - DeliveryMethod = DeliveryMethod.Reliable, - PacketHeader = PacketHeader.IsServerMessage | PacketHeader.IsDisconnectMessage - }); - outMsg.WriteNetSerializableStruct(peerDisconnectPacket); - - Steamworks.SteamNetworking.SendP2PPacket(peer.SteamId.Value, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable); - sentBytes += outMsg.LengthBytes; - } - - private void ClosePeerSession(RemotePeer peer) - { - Steamworks.SteamNetworking.CloseP2PSessionWithUser(peer.SteamId.Value); - remotePeers.Remove(peer); - } - - public override void SendPassword(string password) - { - //owner doesn't send passwords - } - - public override void Close(PeerDisconnectPacket peerDisconnectPacket) - { - if (!isActive) { return; } - - isActive = false; - - for (int i = remotePeers.Count - 1; i >= 0; i--) - { - DisconnectPeer(remotePeers[i], PeerDisconnectPacket.WithReason(DisconnectReason.ServerShutdown)); - } - - Thread.Sleep(100); - - for (int i = remotePeers.Count - 1; i >= 0; i--) - { - ClosePeerSession(remotePeers[i]); - } - - callbacks.OnDisconnect.Invoke(peerDisconnectPacket); - - SteamManager.LeaveLobby(); - Steamworks.SteamNetworking.ResetActions(); - Steamworks.SteamUser.OnValidateAuthTicketResponse -= OnAuthChange; - } - - public override void Send(IWriteMessage msg, DeliveryMethod deliveryMethod, bool compressPastThreshold = true) - { - if (!isActive) { return; } - - IWriteMessage msgToSend = new WriteOnlyMessage(); - byte[] msgData = msg.PrepareForSending(compressPastThreshold, out bool isCompressed, out _); - WriteSteamId(msgToSend, selfSteamID); - WriteSteamId(msgToSend, selfSteamID); - msgToSend.WriteNetSerializableStruct(new PeerPacketHeaders - { - DeliveryMethod = deliveryMethod, - PacketHeader = isCompressed ? PacketHeader.IsCompressed : PacketHeader.None - }); - msgToSend.WriteNetSerializableStruct(new PeerPacketMessage - { - Buffer = msgData - }); - ForwardToServerProcess(msgToSend); - } - - protected override void SendMsgInternal(PeerPacketHeaders headers, INetSerializableStruct? body) - { - //not currently used by SteamP2POwnerPeer - throw new NotImplementedException(); - } - - private static void ForwardToServerProcess(IWriteMessage msg) - { - byte[] bufToSend = new byte[msg.LengthBytes]; - msg.Buffer[..msg.LengthBytes].CopyTo(bufToSend.AsSpan()); - ChildServerRelay.Write(bufToSend); - } - - private void ForwardToRemotePeer(DeliveryMethod deliveryMethod, SteamId recipent, IWriteMessage outMsg) - { - byte[] buf = outMsg.PrepareForSending(compressPastThreshold: false, out _, out int length); - - if (length + 4 >= MsgConstants.MTU) - { - DebugConsole.Log($"WARNING: message length comes close to exceeding MTU, forcing reliable send ({length} bytes)"); - deliveryMethod = DeliveryMethod.Reliable; - } - - bool successSend = Steamworks.SteamNetworking.SendP2PPacket(recipent.Value, buf, length, 0, deliveryMethod.ToSteam()); - sentBytes += length; - - if (successSend) { return; } - - if (deliveryMethod is DeliveryMethod.Unreliable) - { - DebugConsole.Log($"WARNING: message couldn't be sent unreliably, forcing reliable send ({length} bytes)"); - successSend = Steamworks.SteamNetworking.SendP2PPacket(recipent.Value, buf, length, 0, DeliveryMethod.Reliable.ToSteam()); - sentBytes += length; - } - - if (!successSend) - { - DebugConsole.AddWarning($"Failed to send message to remote peer! ({length} bytes)"); - } - } - -#if DEBUG - public override void ForceTimeOut() - { - //TODO: reimplement? - } -#endif - } -} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/FriendProviders/FriendProvider.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/FriendProviders/FriendProvider.cs deleted file mode 100644 index 1a1753091..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/FriendProviders/FriendProvider.cs +++ /dev/null @@ -1,11 +0,0 @@ -#nullable enable - -namespace Barotrauma -{ - abstract class FriendProvider - { - public abstract ServerListScreen.FriendInfo[] RetrieveFriends(); - public abstract void RetrieveAvatar(ServerListScreen.FriendInfo friend, ServerListScreen.AvatarSize avatarSize); - public abstract string GetUserName(); - } -} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/FriendProviders/SteamFriendProvider.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/FriendProviders/SteamFriendProvider.cs deleted file mode 100644 index 9026de250..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/FriendProviders/SteamFriendProvider.cs +++ /dev/null @@ -1,67 +0,0 @@ -#nullable enable -using System; -using System.Linq; -using System.Threading.Tasks; -using Barotrauma.Networking; -using Barotrauma.Steam; -using Microsoft.Xna.Framework.Graphics; - -namespace Barotrauma -{ - class SteamFriendProvider : FriendProvider - { - private static ServerListScreen.FriendInfo FromSteamFriend(Steamworks.Friend steamFriend) - => new ServerListScreen.FriendInfo( - steamFriend.Name, - new SteamId(steamFriend.Id), - steamFriend.State switch - { - Steamworks.FriendState.Offline => ServerListScreen.FriendInfo.Status.Offline, - Steamworks.FriendState.Invisible => ServerListScreen.FriendInfo.Status.Offline, - _ when steamFriend.IsPlayingThisGame => ServerListScreen.FriendInfo.Status.PlayingBarotrauma, - _ when steamFriend.GameInfo is { GameID: var gameId } && gameId > 0 => ServerListScreen.FriendInfo.Status.PlayingAnotherGame, - _ => ServerListScreen.FriendInfo.Status.NotPlaying - }) - { - ServerName = steamFriend.GetRichPresence("servername"), - ConnectCommand = steamFriend.GetRichPresence("connect") is { } connectCmd - ? ToolBox.ParseConnectCommand(ToolBox.SplitCommand(connectCmd)) - : Option.None() - }; - - public override ServerListScreen.FriendInfo[] RetrieveFriends() - => SteamManager.IsInitialized - ? Steamworks.SteamFriends.GetFriends().Select(FromSteamFriend).ToArray() - : Array.Empty(); - - public override void RetrieveAvatar(ServerListScreen.FriendInfo friend, ServerListScreen.AvatarSize avatarSize) - { - if (!(friend.Id is SteamId steamId)) { return; } - - Func> avatarFunc = avatarSize switch - { - ServerListScreen.AvatarSize.Small => Steamworks.SteamFriends.GetSmallAvatarAsync, - ServerListScreen.AvatarSize.Medium => Steamworks.SteamFriends.GetMediumAvatarAsync, - ServerListScreen.AvatarSize.Large => Steamworks.SteamFriends.GetLargeAvatarAsync, - }; - TaskPool.Add($"Get{avatarSize}AvatarAsync", avatarFunc(steamId.Value), task => - { - if (!task.TryGetResult(out Steamworks.Data.Image? img)) { return; } - if (!(img is { } avatarImage)) { return; } - - if (friend.Avatar.TryUnwrap(out var prevAvatar)) - { - prevAvatar.Remove(); - } - - #warning TODO: create an avatar atlas? - var avatarTexture = new Texture2D(GameMain.Instance.GraphicsDevice, (int)avatarImage.Width, (int)avatarImage.Height); - avatarTexture.SetData(avatarImage.Data); - friend.Avatar = Option.Some(new Sprite(avatarTexture, null, null)); - }); - } - - public override string GetUserName() - => SteamManager.GetUsername(); - } -} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/PingUtils.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/PingUtils.cs index 259399b16..b2423ab37 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/PingUtils.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/PingUtils.cs @@ -6,6 +6,7 @@ using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Threading.Tasks; +using Barotrauma.Extensions; using Steamworks.Data; using Color = Microsoft.Xna.Framework.Color; using Socket = System.Net.Sockets.Socket; @@ -34,19 +35,21 @@ namespace Barotrauma.Networking { if (CoroutineManager.IsCoroutineRunning("ConnectToServer")) { return; } - switch (serverInfo.Endpoint) + var endpointOption = serverInfo.Endpoints.FirstOrNone(e => e is not EosP2PEndpoint); + if (!endpointOption.TryUnwrap(out var endpoint)) { return; } + + switch (endpoint) { case LidgrenEndpoint { NetEndpoint: var endPoint }: - GetIPAddressPing(serverInfo, endPoint, onPingDiscovered); break; - case SteamP2PEndpoint steamP2PEndpoint: - TaskPool.Add($"EstimateSteamLobbyPing ({steamP2PEndpoint.StringRepresentation})", + case SteamP2PEndpoint: + TaskPool.Add($"EstimateSteamLobbyPing ({endpoint.StringRepresentation})", EstimateSteamLobbyPing(serverInfo), t => { - if (!t.TryGetResult(out Option ping)) { return; } - serverInfo.Ping = ping; + if (!t.TryGetResult(out Result ping)) { return; } + serverInfo.Ping = ping.TryUnwrapSuccess(out var ms) ? Option.Some(ms) : Option.None; onPingDiscovered(serverInfo); }); break; @@ -99,35 +102,57 @@ namespace Barotrauma.Networking return loadedLobby; } - - private static async Task> EstimateSteamLobbyPing(ServerInfo serverInfo) - { - if (!(serverInfo.Endpoint is SteamP2PEndpoint { SteamId: var ownerId })) { return Option.None(); } - while (!steamPingInfoReady) { await Task.Delay(50); } - Lobby lobby; + private enum SteamLobbyPingError + { + SteamPingUnsupported, + FailedToGetHostLocationData, + FailedToParseHostLocationData, + PingEstimationFailed + } + + private static async Task> EstimateSteamLobbyPing(ServerInfo serverInfo) + { + while (!steamPingInfoReady) + { + if (!SteamManager.IsInitialized) { return Result.Failure(SteamLobbyPingError.SteamPingUnsupported); } + await Task.Delay(50); + } + + string pingLocationStr = ""; if (serverInfo.MetadataSource.TryUnwrap(out SteamP2PServerProvider.DataSource src)) { - lobby = src.Lobby; + var lobby = src.Lobby; + pingLocationStr = lobby.GetData("steampinglocation"); + if (pingLocationStr.IsNullOrEmpty()) { pingLocationStr = lobby.GetData("pinglocation"); } } - else + else if (serverInfo.MetadataSource.TryUnwrap(out EosServerProvider.DataSource srcEos)) { - var friendLobby = await GetSteamLobbyForUser(ownerId); - if (friendLobby is null) { return Option.None(); } - lobby = friendLobby.Value; + pingLocationStr = srcEos.SteamPingLocation; + } + else if (serverInfo.Endpoints.OfType().FirstOrNone().TryUnwrap(out var steamP2PEndpoint)) + { + var friendLobby = await GetSteamLobbyForUser(steamP2PEndpoint.SteamId); + pingLocationStr = friendLobby?.GetData("steampinglocation") ?? ""; } - var pingLocation = NetPingLocation.TryParseFromString(lobby.GetData("pinglocation")); - + if (pingLocationStr.IsNullOrEmpty()) + { + return Result.Failure(SteamLobbyPingError.FailedToGetHostLocationData); + } + + var pingLocation = NetPingLocation.TryParseFromString(pingLocationStr); + if (pingLocation.HasValue && Steamworks.SteamNetworkingUtils.LocalPingLocation.HasValue) { int ping = Steamworks.SteamNetworkingUtils.LocalPingLocation.Value.EstimatePingTo(pingLocation.Value); - return ping >= 0 ? Option.Some(ping) : Option.None(); + if (ping < 0) { return Result.Failure(SteamLobbyPingError.PingEstimationFailed); } + return Result.Success(ping); } else { - return Option.None(); + return Result.Failure(SteamLobbyPingError.FailedToParseHostLocationData); } } @@ -173,7 +198,7 @@ namespace Barotrauma.Networking } if (endPoint?.Address == null) { return Option.None(); } - + //don't attempt to ping if the address is IPv6 and it's not supported if (endPoint.Address.AddressFamily == AddressFamily.InterNetworkV6 && !Socket.OSSupportsIPv6) { return Option.None(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs index 4ce4dd2e0..1111afa09 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs @@ -22,9 +22,9 @@ namespace Barotrauma.Networking public abstract void Write(XElement element); } - public Endpoint Endpoint { get; private set; } + public ImmutableArray Endpoints { get; } - public Option MetadataSource = Option.None(); + public Option MetadataSource = Option.None; [Serialize("", IsPropertySaveable.Yes)] public string ServerName { get; set; } = ""; @@ -75,6 +75,8 @@ namespace Barotrauma.Networking [Serialize("", IsPropertySaveable.Yes)] public LanguageIdentifier Language { get; set; } + public bool EosCrossplay { get; set; } + [Serialize("", IsPropertySaveable.Yes)] public string SelectedSub { get; set; } = string.Empty; @@ -84,49 +86,30 @@ namespace Barotrauma.Networking public bool Checked = false; - public readonly struct ContentPackageInfo - { - public readonly string Name; - public readonly string Hash; - public readonly Option Id; - - public ContentPackageInfo(string name, string hash, Option id) - { - Name = name; - Hash = hash; - Id = id; - } - - public ContentPackageInfo(ContentPackage pkg) - { - Name = pkg.Name; - Hash = pkg.Hash.StringRepresentation; - Id = pkg.UgcId; - } - } - - public ImmutableArray ContentPackages; + public ImmutableArray ContentPackages; public int ContentPackageCount; public bool IsModded => ContentPackages.Any(p => !GameMain.VanillaContent.NameMatches(p.Name)); - public ServerInfo(Endpoint endpoint) + public ServerInfo(params Endpoint[] endpoint) : this(endpoint.ToImmutableArray()) { } + + public ServerInfo(ImmutableArray endpoints) { SerializableProperties = SerializableProperty.GetProperties(this); - Endpoint = endpoint; - ContentPackages = ImmutableArray.Empty; + Endpoints = endpoints; + ContentPackages = ImmutableArray.Empty; } - public static ServerInfo FromServerConnection(NetworkConnection connection, ServerSettings serverSettings) + public static ServerInfo FromServerEndpoints(ImmutableArray endpoints, ServerSettings serverSettings) { - var serverInfo = new ServerInfo(connection.Endpoint) + var serverInfo = new ServerInfo(endpoints) { GameMode = GameMain.NetLobbyScreen.SelectedMode?.Identifier ?? Identifier.Empty, GameStarted = Screen.Selected != GameMain.NetLobbyScreen, GameVersion = GameMain.Version, PlayerCount = GameMain.Client.ConnectedClients.Count, - ContentPackages = ContentPackageManager.EnabledPackages.All.Select(p => new ContentPackageInfo(p)).ToImmutableArray(), + ContentPackages = ContentPackageManager.EnabledPackages.All.Select(p => new ServerListContentPackageInfo(p)).ToImmutableArray(), Ping = GameMain.Client.Ping, // ------------------------------------- @@ -225,7 +208,7 @@ namespace Barotrauma.Networking playStyleName.RectTransform.IsFixedSize = true; var serverType = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform), - Endpoint?.ServerTypeString ?? string.Empty, + Endpoints.First().ServerTypeString, textAlignment: Alignment.TopLeft) { CanBeFocused = false @@ -460,8 +443,12 @@ namespace Barotrauma.Networking GameVersion = version; } if (int.TryParse(valueGetter("playercount"), out int playerCount)) { PlayerCount = playerCount; } - if (int.TryParse(valueGetter("maxplayernum"), out int maxPlayers)) { MaxPlayers = maxPlayers; } + + if (int.TryParse(valueGetter("maxplayers"), out int maxPlayers)) { MaxPlayers = maxPlayers; } + else if (int.TryParse(valueGetter("maxplayernum"), out maxPlayers)) { MaxPlayers = maxPlayers; } + if (Enum.TryParse(valueGetter("modeselectionmode"), out SelectionMode modeSelectionMode)) { ModeSelectionMode = modeSelectionMode; } + if (Enum.TryParse(valueGetter("subselectionmode"), out SelectionMode subSelectionMode)) { SubSelectionMode = subSelectionMode; } HasPassword = getBool("haspassword"); @@ -471,6 +458,8 @@ namespace Barotrauma.Networking AllowSpectating = getBool("allowspectating"); AllowRespawn = getBool("allowrespawn"); VoipEnabled = getBool("voicechatenabled"); + EosCrossplay = getBool("eoscrossplay"); + GameMode = valueGetter("gamemode")?.ToIdentifier() ?? Identifier.Empty; if (float.TryParse(valueGetter("traitors"), NumberStyles.Any, CultureInfo.InvariantCulture, out float traitorProbability)) { TraitorProbability = traitorProbability; } if (Enum.TryParse(valueGetter("playstyle"), out PlayStyle playStyle)) { PlayStyle = playStyle; } @@ -488,27 +477,21 @@ namespace Barotrauma.Networking } } - private static ContentPackageInfo[] ExtractContentPackageInfo(string serverName, Func valueGetter) + private static ServerListContentPackageInfo[] ExtractContentPackageInfo(string serverName, Func valueGetter) { //workaround to ServerRules queries truncating the values to 255 bytes int individualPackageIndex = 0; string? individualPackage = valueGetter($"contentpackage{individualPackageIndex}"); if (!individualPackage.IsNullOrEmpty()) { - List contentPackages = new List(); + List contentPackages = new List(); do { - string[] splitPackageInfo = individualPackage.Split(','); - if (splitPackageInfo.Length != 3) + if (!ServerListContentPackageInfo.ParseSingleEntry(individualPackage).TryUnwrap(out var info)) { - DebugConsole.Log( - $"Error in a server's content package list: malformed content package info ({individualPackage})."); - return Array.Empty(); + return Array.Empty(); } - string name = splitPackageInfo[0]; - string hash = splitPackageInfo[1]; - ulong.TryParse(splitPackageInfo[2], out ulong id); - contentPackages.Add(new ContentPackageInfo(name, hash, Option.Some(new SteamWorkshopId(id)))); + contentPackages.Add(info); individualPackageIndex++; individualPackage = valueGetter($"contentpackage{individualPackageIndex}"); @@ -518,43 +501,58 @@ namespace Barotrauma.Networking string? joinedNames = valueGetter("contentpackage"); string? joinedHashes = valueGetter("contentpackagehash"); - string? joinedWorkshopIds = valueGetter("contentpackageid"); + string? joinedUgcIds = valueGetter("contentpackageid"); - string[] contentPackageNames = joinedNames.IsNullOrEmpty() ? Array.Empty() : joinedNames.Split(','); - string[] contentPackageHashes = joinedHashes.IsNullOrEmpty() ? Array.Empty() : joinedHashes.Split(','); - #warning TODO: genericize - ulong[] contentPackageIds = joinedWorkshopIds.IsNullOrEmpty() ? new ulong[1] : SteamManager.ParseWorkshopIds(joinedWorkshopIds).ToArray(); + var contentPackageNames = joinedNames.IsNullOrEmpty() ? Array.Empty() : joinedNames.SplitEscaped(','); + var contentPackageHashes = joinedHashes.IsNullOrEmpty() ? Array.Empty() : joinedHashes.SplitEscaped(','); + var contentPackageIds = joinedUgcIds.IsNullOrEmpty() ? new string[1] { string.Empty } : joinedUgcIds.SplitEscaped(','); - if (contentPackageNames.Length != contentPackageHashes.Length || contentPackageHashes.Length != contentPackageIds.Length) + if (contentPackageNames.Count != contentPackageHashes.Count || contentPackageHashes.Count != contentPackageIds.Count) { DebugConsole.Log( - $"The number of names, hashes and Workshop IDs on server \"{serverName}\"" + - $" doesn't match: {contentPackageNames.Length} names ({string.Join(", ", contentPackageNames)}), {contentPackageHashes.Length} hashes, {contentPackageIds.Length} ids)"); - return Array.Empty(); + $"The number of names, hashes and UGC IDs on server \"{serverName}\"" + + $" doesn't match: {contentPackageNames.Count} names ({string.Join(", ", contentPackageNames)}), {contentPackageHashes.Count} hashes, {contentPackageIds.Count} ids)"); + return Array.Empty(); } return contentPackageNames .Zip(contentPackageHashes, (name, hash) => (name, hash)) .Zip(contentPackageIds, (t1, id) => - new ContentPackageInfo( + new ServerListContentPackageInfo( t1.name, t1.hash, - Option.Some(new SteamWorkshopId(id)))) + ContentPackageId.Parse(id))) .ToArray(); } public static Option FromXElement(XElement element) { + var endpoints = new List(); + string endpointStr = element.GetAttributeString("Endpoint", null) ?? element.GetAttributeString("OwnerID", null) ?? $"{element.GetAttributeString("IP", "")}:{element.GetAttributeInt("Port", 0)}"; + + if (Endpoint.Parse(endpointStr).TryUnwrap(out var endpoint)) + { + endpoints.Add(endpoint); + } + else + { + var multipleEndpointStrs + = element.GetAttributeStringArray("Endpoints", Array.Empty()); + endpoints.AddRange( + multipleEndpointStrs + .Select(Endpoint.Parse) + .NotNone()); + } - if (!Endpoint.Parse(endpointStr).TryUnwrap(out var endpoint)) { return Option.None(); } + if (endpoints.Count == 0) { return Option.None; } var gameVersionStr = element.GetAttributeString("GameVersion", ""); if (!Version.TryParse(gameVersionStr, out var gameVersion)) { gameVersion = GameMain.Version; } - var info = new ServerInfo(endpoint) + var info = new ServerInfo(endpoints.ToImmutableArray()) { GameVersion = gameVersion }; @@ -562,14 +560,14 @@ namespace Barotrauma.Networking info.MetadataSource = DataSource.Parse(element); - return Option.Some(info); + return Option.Some(info); } public XElement ToXElement() { XElement element = new XElement(GetType().Name); - element.SetAttributeValue("Endpoint", Endpoint.ToString()); + element.SetAttributeValue("Endpoints", string.Join(",", Endpoints.Select(e => e.StringRepresentation))); element.SetAttributeValue("GameVersion", GameVersion.ToString()); SerializableProperty.SerializeProperties(this, element, saveIfDefault: true); @@ -588,9 +586,9 @@ namespace Barotrauma.Networking } public bool Equals(ServerInfo other) - => other.Endpoint == Endpoint; + => other.Endpoints.Any(e => Endpoints.Contains(e)); - public override int GetHashCode() => Endpoint.GetHashCode(); + public override int GetHashCode() => Endpoints.First().GetHashCode(); string ISerializableEntity.Name => "ServerInfo"; public Dictionary SerializableProperties { get; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/CompositeServerProvider.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/CompositeServerProvider.cs index 205d4f034..fe4313003 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/CompositeServerProvider.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/CompositeServerProvider.cs @@ -15,7 +15,7 @@ namespace Barotrauma this.providers = providers.ToImmutableArray(); } - protected override void RetrieveServersImpl(Action onServerDataReceived, Action onQueryCompleted) + protected override void RetrieveServersImpl(Action onServerDataReceived, Action onQueryCompleted) { int providersFinished = 0; void ackFinishedProvider() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/EosServerProvider.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/EosServerProvider.cs new file mode 100644 index 000000000..c7a400b21 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/EosServerProvider.cs @@ -0,0 +1,139 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using System.Xml.Linq; +using Barotrauma.Networking; + +namespace Barotrauma; + +sealed class EosServerProvider : ServerProvider +{ + public sealed class DataSource : ServerInfo.DataSource + { + public readonly string SteamPingLocation; + + public DataSource(string steamPingLocation) + { + SteamPingLocation = steamPingLocation; + } + + public override void Write(XElement element) { /* do nothing */ } + } + + protected override void RetrieveServersImpl(Action onServerDataReceived, Action onQueryCompleted) + { + if (EosInterface.IdQueries.GetLoggedInPuids() is not { Length: > 0 } loggedInPuids) { return; } + + int finishedTaskCount = 0; + int totalTaskCount = EosInterface.Sessions.MaxBucketIndex + 1 - EosInterface.Sessions.MinBucketIndex; + + void countTaskFinished() + { + finishedTaskCount++; + if (finishedTaskCount == totalTaskCount) + { + onQueryCompleted(); + } + } + + void onTaskFinished(Task t) + { + using var janitor = Janitor.Start(); + janitor.AddAction(countTaskFinished); + + if (!t.TryGetResult( + out Result, EosInterface.Sessions.RemoteSession.Query.Error>? result)) + { + return; + } + + if (!result.TryUnwrapSuccess(out var sessions)) + { + return; + } + + var addedEndpoints = new HashSet(); + foreach (var session in sessions) + { + if (!session.Attributes.TryGetValue("ServerName".ToIdentifier(), out var serverName)) + { + continue; + } + + var endpointOption = Endpoint.Parse(session.HostAddress); + if (!endpointOption.TryUnwrap(out var primaryEndpoint)) + { + continue; + } + + var endpoints = new List { primaryEndpoint }; + if (primaryEndpoint is EosP2PEndpoint + && session.Attributes.TryGetValue("SteamP2PEndpoint".ToIdentifier(), out var steamIdStr) + && SteamP2PEndpoint.Parse(steamIdStr).TryUnwrap(out var steamP2PEndpoint)) + { + endpoints.Add(steamP2PEndpoint); + } + else if (primaryEndpoint is LidgrenEndpoint + { + Address: LidgrenAddress address, Port: NetConfig.DefaultPort + } + && session.Attributes.TryGetValue("Port".ToIdentifier(), out var portStr) + && ushort.TryParse(portStr, out var port)) + { + // Port isn't included as part of the host address + // because it's filled in by EOS automatically, + // so extract the port from a separate attribute and + // fix up the endpoint here + primaryEndpoint = new LidgrenEndpoint(address.NetAddress, port); + endpoints[0] = primaryEndpoint; + } + + // Prevent duplicate entries + if (endpoints.Intersect(addedEndpoints).Any()) + { + continue; + } + + addedEndpoints.UnionWith(endpoints); + + var serverInfo = new ServerInfo(endpoints.ToImmutableArray()) + { + ServerName = serverName + }; + serverInfo.UpdateInfo(key => + session.Attributes.TryGetValue(key.ToIdentifier(), out var value) ? value : string.Empty); + serverInfo.EosCrossplay = true; + serverInfo.Checked = true; + + if (session.Attributes.TryGetValue("steampinglocation".ToIdentifier(), out var steamPingLocation)) + { + serverInfo.MetadataSource = Option.Some((ServerInfo.DataSource)new DataSource(steamPingLocation)); + } + + onServerDataReceived(serverInfo, this); + } + }; + + for (int bucketIndex = EosInterface.Sessions.MinBucketIndex; bucketIndex <= EosInterface.Sessions.MaxBucketIndex; bucketIndex++) + { + var query = new EosInterface.Sessions.RemoteSession.Query( + BucketIndex: bucketIndex, + LocalUserId: loggedInPuids.First(), + MaxResults: 200, + Attributes: ImmutableDictionary.Empty); + + TaskPool.Add( + $"{nameof(EosServerProvider)}.{nameof(RetrieveServersImpl)}", + query.Run(), + onTaskFinished); + } + } + + public override void Cancel() + { + + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/ServerProvider.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/ServerProvider.cs index 8664c58ed..7f80cd096 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/ServerProvider.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/ServerProvider.cs @@ -6,12 +6,12 @@ namespace Barotrauma { abstract class ServerProvider { - public void RetrieveServers(Action onServerDataReceived, Action onQueryCompleted) + public void RetrieveServers(Action onServerDataReceived, Action onQueryCompleted) { Cancel(); RetrieveServersImpl(onServerDataReceived, onQueryCompleted); } - protected abstract void RetrieveServersImpl(Action onServerDataReceived, Action onQueryCompleted); + protected abstract void RetrieveServersImpl(Action action, Action onQueryCompleted); public abstract void Cancel(); } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamDedicatedServerProvider.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamDedicatedServerProvider.cs index 678d89cdc..e31ccd06f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamDedicatedServerProvider.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamDedicatedServerProvider.cs @@ -46,42 +46,43 @@ namespace Barotrauma MetadataSource = Option.Some(new DataSource((UInt16)entry.QueryPort)) }); - private static void HandleResponsiveServer(Steamworks.Data.ServerInfo entry, Action onServerDataReceived) + private void HandleResponsiveServer(Steamworks.Data.ServerInfo entry, Action onServerDataReceived) { TaskPool.Add($"QueryServerRules (GetServers, {entry.Name}, {entry.Address})", entry.QueryRulesAsync(), t => { if (t.Status == TaskStatus.Faulted) { - TaskPool.PrintTaskExceptions(t, $"Failed to retrieve rules for {entry.Name}"); + TaskPool.PrintTaskExceptions(t, $"Failed to retrieve rules for {entry.Name}", msg => DebugConsole.ThrowError(msg)); return; } - if (!t.TryGetResult(out Dictionary rules)) { return; } + if (!t.TryGetResult(out Dictionary? rules)) { return; } if (rules is null) { return; } + if (!InfoFromListEntry(entry).TryUnwrap(out var serverInfo)) { return; } serverInfo.UpdateInfo(key => { if (rules.TryGetValue(key, out var val)) { return val; } return null; }); - serverInfo.Checked = true; //rules != null; + serverInfo.Checked = true; - onServerDataReceived(serverInfo); + onServerDataReceived(serverInfo, this); }); } - private static void HandleUnresponsiveServer(Steamworks.Data.ServerInfo entry, Action onServerDataReceived) + private void HandleUnresponsiveServer(Steamworks.Data.ServerInfo entry, Action onServerDataReceived) { //TODO: do we still want to list unresponsive servers? if (!InfoFromListEntry(entry).TryUnwrap(out var serverInfo)) { return; } - onServerDataReceived(serverInfo); + onServerDataReceived(serverInfo, this); } private Steamworks.ServerList.Internet? serverQuery; private CoroutineHandle? queryCoroutine; - protected override void RetrieveServersImpl(Action onServerDataReceived, Action onQueryCompleted) + protected override void RetrieveServersImpl(Action onServerDataReceived, Action onQueryCompleted) { if (!SteamManager.IsInitialized) { @@ -139,7 +140,7 @@ namespace Barotrauma CoroutineManager.StopCoroutines(selfQueryCoroutine); dequeue(); - if (t.Status == TaskStatus.Faulted) { TaskPool.PrintTaskExceptions(t, "Failed to retrieve servers"); } + if (t.Status == TaskStatus.Faulted) { TaskPool.PrintTaskExceptions(t, "Failed to retrieve servers", msg => DebugConsole.ThrowError(msg)); } selfServerQuery.Dispose(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamP2PServerProvider.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamP2PServerProvider.cs index caf2e0a20..f76e9b435 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamP2PServerProvider.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamP2PServerProvider.cs @@ -1,6 +1,7 @@ #nullable enable using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Threading.Tasks; using System.Xml.Linq; using Barotrauma.Networking; @@ -24,7 +25,7 @@ namespace Barotrauma private object? queryRef = null; - protected override void RetrieveServersImpl(Action onServerDataReceived, Action onQueryCompleted) + protected override void RetrieveServersImpl(Action onServerDataReceived, Action onQueryCompleted) { if (!SteamManager.IsInitialized) { @@ -61,7 +62,7 @@ namespace Barotrauma // If queryRef != selfQueryRef, this query was cancelled if (!ReferenceEquals(selfQueryRef, queryRef)) { return; } - if (!t.TryGetResult(out Steamworks.Data.Lobby[] lobbies) + if (!t.TryGetResult(out Steamworks.Data.Lobby[]? lobbies) || lobbies is null || lobbies.Length == 0) { @@ -74,16 +75,23 @@ namespace Barotrauma string lobbyOwnerStr = lobby.GetData("lobbyowner") ?? ""; lobbyQuery = lobbyQuery.WithoutKeyValue("lobbyowner", lobbyOwnerStr); - string serverName = lobby.GetData("name") ?? ""; + string serverName = lobby.GetData("servername").FallbackNullOrEmpty(lobby.GetData("name")) ?? ""; if (string.IsNullOrEmpty(serverName)) { continue; } var ownerId = SteamId.Parse(lobbyOwnerStr); if (!ownerId.TryUnwrap(out var lobbyOwnerId)) { continue; } + + var eosP2PEndpointOption = EosP2PEndpoint + .Parse(lobby.GetData("EosEndpoint") ?? "") + .Select(e => (Endpoint)e); if (retrieved.Contains(lobbyOwnerId)) { continue; } retrieved.Add(lobbyOwnerId); - var serverInfo = new ServerInfo(new SteamP2PEndpoint(lobbyOwnerId)) + var endpoints = new List { new SteamP2PEndpoint(lobbyOwnerId) }; + if (eosP2PEndpointOption.TryUnwrap(out var eosP2PEndpoint)) { endpoints.Add(eosP2PEndpoint); } + + var serverInfo = new ServerInfo(endpoints.ToImmutableArray()) { ServerName = serverName, MetadataSource = Option.Some(new DataSource(lobby)) @@ -91,7 +99,7 @@ namespace Barotrauma serverInfo.UpdateInfo(key => lobby.GetData(key)); serverInfo.Checked = true; - onServerDataReceived(serverInfo); + onServerDataReceived(serverInfo, this); } startQuery(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Program.cs b/Barotrauma/BarotraumaClient/ClientSource/Program.cs index b541794b2..ad60eb631 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Program.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Program.cs @@ -8,6 +8,7 @@ using Barotrauma.Steam; using System.Diagnostics; using System.Runtime.InteropServices; using System.Xml.Linq; +using Barotrauma.Debugging; #if WINDOWS using SharpDX; @@ -17,7 +18,6 @@ using SharpDX; namespace Barotrauma { -#if WINDOWS || LINUX || OSX /// /// The main class. /// @@ -52,7 +52,10 @@ namespace Barotrauma Game = null; executableDir = Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location); Directory.SetCurrentDirectory(executableDir); - SteamManager.Initialize(); + DebugConsoleCore.Init( + newMessage: (s, c) => DebugConsole.NewMessage(s, c), + log: DebugConsole.Log); + StoreIntegration.Init(ref args); EnableNvOptimus(); Game = new GameMain(args); Game.Run(); @@ -188,6 +191,10 @@ namespace Barotrauma { sb.AppendLine("SteamManager initialized"); } + else if (EosInterface.IdQueries.IsLoggedIntoEosConnect) + { + sb.AppendLine("Logged in to EOS connect"); + } if (GameMain.Client != null) { @@ -344,6 +351,4 @@ namespace Barotrauma } } -#endif - - } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignEndScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignEndScreen.cs index fc8859439..4ca192355 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignEndScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignEndScreen.cs @@ -49,7 +49,7 @@ namespace Barotrauma static void UnlockAchievement(string id) { - SteamAchievementManager.UnlockAchievement(id.ToIdentifier(), unlockClients: true); + AchievementManager.UnlockAchievement(id.ToIdentifier(), unlockClients: true); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs index 8400fab7d..f2706ca31 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs @@ -9,7 +9,7 @@ using System.Xml.Linq; namespace Barotrauma { - class SinglePlayerCampaignSetupUI : CampaignSetupUI + sealed class SinglePlayerCampaignSetupUI : CampaignSetupUI { private GUIListBox subList; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EditorNode.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EditorNode.cs index d0406a244..3210e1122 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EditorNode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EditorNode.cs @@ -550,7 +550,10 @@ namespace Barotrauma width -= 16; } - valueText = ToolBox.WrapText(valueText, width, GUIStyle.SubHeadingFont.Value); + if (GUIStyle.SubHeadingFont.Value != null) + { + valueText = ToolBox.WrapText(valueText, width, GUIStyle.SubHeadingFont.Value); + } wrappedText = valueText; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs index 4a1816928..e8806ca32 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs @@ -974,7 +974,7 @@ namespace Barotrauma spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState); GUI.Draw(Cam, spriteBatch); - if (!string.IsNullOrWhiteSpace(DrawnTooltip)) + if (!string.IsNullOrWhiteSpace(DrawnTooltip) && GUIStyle.SmallFont.Value != null) { string tooltip = ToolBox.WrapText(DrawnTooltip, 256.0f, GUIStyle.SmallFont.Value); GUI.DrawString(spriteBatch, PlayerInput.MousePosition + new Vector2(32, 32), tooltip, Color.White, Color.Black * 0.8f, 4, GUIStyle.SmallFont); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen/MainMenuScreen.cs similarity index 90% rename from Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs rename to Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen/MainMenuScreen.cs index 0982130cf..76d16400e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen/MainMenuScreen.cs @@ -9,7 +9,7 @@ using Microsoft.Xna.Framework.Graphics; using RestSharp; using System; using System.Collections.Generic; -using System.Data.Common; +using System.Collections.Immutable; using System.Diagnostics; using Barotrauma.IO; using System.Linq; @@ -21,7 +21,7 @@ using Barotrauma.Steam; namespace Barotrauma { - class MainMenuScreen : Screen + sealed class MainMenuScreen : Screen { private enum Tab { @@ -33,7 +33,7 @@ namespace Barotrauma JoinServer = 5, CharacterEditor = 6, SubmarineEditor = 7, - SteamWorkshop = 8, + Mods = 8, Credits = 9, Empty = 10 } @@ -59,6 +59,8 @@ namespace Barotrauma private GUIImage playstyleBanner; private GUITextBlock playstyleDescription; + private static string RemoteContentUrl => GameSettings.CurrentConfig.RemoteMainMenuContentUrl; + private readonly GUIComponent remoteContentContainer; private XDocument remoteContentDoc; @@ -76,11 +78,76 @@ namespace Barotrauma private GUITextBlock tutorialHeader, tutorialDescription; private GUIListBox tutorialList; + private readonly GUITextBlock gameAnalyticsStatusText; + + private readonly GUILayoutGroup leftTextFooterLayout; + private readonly GUILayoutGroup rightTextFooterLayout; + private GUIComponent versionMismatchWarning; #region Creation - public MainMenuScreen(GameMain game) + public MainMenuScreen(GameMain game) : base() { + leftTextFooterLayout = createTextFooter(); + rightTextFooterLayout = createTextFooter(); + + gameAnalyticsStatusText = createLeftText(TextManager.Get($"GameAnalyticsStatus.{GameAnalyticsManager.Consent.Unknown}")); + createLeftText("Barotrauma v" + GameMain.Version + " (" + AssemblyInfo.BuildString + ", branch " + AssemblyInfo.GitBranch + ", revision " + AssemblyInfo.GitRevision + ")"); + + var privacyPolicyText = createRightText(TextManager.Get("privacypolicy").Fallback("Privacy policy")); + (Rectangle Rect, bool MouseOn) getPrivacyPolicyHoverRect() + { + var textSize = privacyPolicyText.Font.MeasureString(privacyPolicyText.Text); + var bottomRight = privacyPolicyText.Rect.Location.ToVector2() + + privacyPolicyText.TextPos + + privacyPolicyText.TextOffset; + var rect = new Rectangle((bottomRight - textSize).ToPoint(), textSize.ToPoint()); + bool mouseOn = rect.Contains(PlayerInput.LatestMousePosition) && GUI.IsMouseOn(privacyPolicyText); + return (rect, mouseOn); + } + new GUICustomComponent(new RectTransform(Vector2.One, privacyPolicyText.RectTransform), + onUpdate: (dt, component) => + { + var (_, mouseOn) = getPrivacyPolicyHoverRect(); + if (mouseOn && PlayerInput.PrimaryMouseButtonClicked()) + { + GameMain.ShowOpenUriPrompt("https://privacypolicy.daedalic.com"); + } + }, + onDraw: (sb, component) => + { + var (rect, mouseOn) = getPrivacyPolicyHoverRect(); + Color color = mouseOn ? Color.White : Color.White * 0.7f; + privacyPolicyText.TextColor = color; + GUI.DrawLine(sb, new Vector2(rect.Left, rect.Bottom), new Vector2(rect.Right, rect.Bottom), color); + }); + + createRightText("© " + DateTime.Now.Year + " Undertow Games & FakeFish. All rights reserved."); + createRightText("© " + DateTime.Now.Year + " Daedalic Entertainment GmbH. The Daedalic logo is a trademark of Daedalic Entertainment GmbH, Germany. All rights reserved."); + + GUILayoutGroup createTextFooter() + => new GUILayoutGroup(new RectTransform((1.0f, 0.06f), Frame.RectTransform, Anchor.BottomCenter)) + { + ChildAnchor = Anchor.BottomLeft + }; + + GUITextBlock createTextInFooter(GUILayoutGroup footer, LocalizedString str, Alignment textAlignment) + { + var textBlock = new GUITextBlock( + rectT: new RectTransform((1.0f, 0.3f), footer.RectTransform), + text: str, + textAlignment: textAlignment, + font: GUIStyle.SmallFont, + textColor: Color.White * 0.7f); + textBlock.RectTransform.SetAsFirstChild(); + return textBlock; + } + + GUITextBlock createLeftText(LocalizedString str) + => createTextInFooter(leftTextFooterLayout, str, Alignment.BottomLeft); + GUITextBlock createRightText(LocalizedString str) + => createTextInFooter(rightTextFooterLayout, str, Alignment.BottomRight); + GameMain.Instance.ResolutionChanged += () => { SetMenuTabPositioning(); @@ -306,7 +373,7 @@ namespace Barotrauma { ForceUpperCase = ForceUpperCase.Yes, Enabled = true, - UserData = Tab.SteamWorkshop, + UserData = Tab.Mods, OnClicked = SelectTab }; @@ -321,7 +388,7 @@ namespace Barotrauma }, Visible = false }; - + new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), customizeList.RectTransform), TextManager.Get("SubEditorButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { ForceUpperCase = ForceUpperCase.Yes, @@ -378,7 +445,7 @@ namespace Barotrauma OnClicked = (button, userData) => { string url = TextManager.Get("EditorDisclaimerWikiUrl").Fallback("https://barotraumagame.com/wiki").Value; - GameMain.ShowOpenUrlInWebBrowserPrompt(url, promptExtensionTag: "wikinotice"); + GameMain.ShowOpenUriPrompt(url, promptExtensionTag: "wikinotice"); return true; } }; @@ -456,39 +523,40 @@ namespace Barotrauma menuTabs = new Dictionary { - [Tab.Settings] = new GUIFrame(new RectTransform(new Vector2(relativeSize.X, 0.8f), GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeOffset }, + [Tab.Settings] = new GUIFrame(new RectTransform(new Vector2(relativeSize.X, 0.8f), Frame.RectTransform, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeOffset }, style: null) { CanBeFocused = false }, - [Tab.NewGame] = new GUIFrame(new RectTransform(relativeSize * new Vector2(1.0f, 1.15f), GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeOffset }), - [Tab.LoadGame] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeOffset }) + [Tab.NewGame] = new GUIFrame(new RectTransform(relativeSize * new Vector2(1.0f, 1.15f), Frame.RectTransform, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeOffset }), + [Tab.LoadGame] = new GUIFrame(new RectTransform(relativeSize, Frame.RectTransform, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeOffset }) }; CreateCampaignSetupUI(); var hostServerScale = new Vector2(0.7f, 1.2f); menuTabs[Tab.HostServer] = new GUIFrame(new RectTransform( - Vector2.Multiply(relativeSize, hostServerScale), GUI.Canvas, anchor, pivot, minSize.Multiply(hostServerScale), maxSize.Multiply(hostServerScale)) + Vector2.Multiply(relativeSize, hostServerScale), Frame.RectTransform, anchor, pivot, minSize.Multiply(hostServerScale), maxSize.Multiply(hostServerScale)) { RelativeOffset = relativeOffset }); CreateHostServerFields(); //---------------------------------------------------------------------- - menuTabs[Tab.Tutorials] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeOffset }); + menuTabs[Tab.Tutorials] = new GUIFrame(new RectTransform(relativeSize, Frame.RectTransform, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeOffset }); CreateTutorialTab(); this.game = game; - menuTabs[Tab.Credits] = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null) + menuTabs[Tab.Credits] = new GUIFrame(new RectTransform(Vector2.One, Frame.RectTransform, Anchor.Center), style: null) { CanBeFocused = false }; - new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, menuTabs[Tab.Credits].RectTransform, Anchor.Center), style: "GUIBackgroundBlocker") + var blockerFrame = new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, menuTabs[Tab.Credits].RectTransform, Anchor.Center), style: "GUIBackgroundBlocker") { CanBeFocused = false }; + blockerFrame.RectTransform.RelativeOffset = GUI.IsUltrawide ? Vector2.Zero : new Vector2(0.05f, 0.0f); var creditsContainer = new GUIFrame(new RectTransform(new Vector2(0.75f, 1.5f), menuTabs[Tab.Credits].RectTransform, Anchor.CenterRight), style: "OuterGlow", color: Color.Black * 0.8f); creditsPlayer = new CreditsPlayer(new RectTransform(Vector2.One, creditsContainer.RectTransform), "Content/Texts/Credits.xml"); @@ -499,6 +567,7 @@ namespace Barotrauma }; SetMenuTabPositioning(); + SelectTab(Tab.Empty); } private void SetMenuTabPositioning() @@ -591,7 +660,7 @@ namespace Barotrauma public override void Select() { ResetModUpdateButton(); - + if (WorkshopItemsToUpdate.Any()) { while (WorkshopItemsToUpdate.TryDequeue(out ulong workshopId)) @@ -616,6 +685,8 @@ namespace Barotrauma versionMismatchWarning.Visible = GameMain.Version < ContentPackageManager.VanillaCorePackage.GameVersion; ResetButtonStates(null); + + Eos.EosAccount.ExecuteAfterLogin(AchievementManager.SyncBetweenPlatforms); } public override void Deselect() @@ -731,7 +802,7 @@ namespace Barotrauma case Tab.SubmarineEditor: CoroutineManager.StartCoroutine(SelectScreenWithWaitCursor(GameMain.SubEditorScreen)); break; - case Tab.SteamWorkshop: + case Tab.Mods: var settings = SettingsMenu.Create(menuTabs[Tab.Settings].RectTransform); settings.SelectTab(SettingsMenu.Tab.Mods); tab = Tab.Settings; @@ -747,6 +818,14 @@ namespace Barotrauma } selectedTab = tab; + leftTextFooterLayout.Visible = tab != Tab.Credits; + rightTextFooterLayout.Visible = tab != Tab.Credits; + + foreach (var tabFrame in menuTabs.Values) + { + tabFrame.Visible = false; + } + if (menuTabs.TryGetValue(selectedTab, out var visibleTab)) { visibleTab.Visible = true; } return true; } @@ -871,6 +950,17 @@ namespace Barotrauma tutorialSkipWarning.Buttons[1].OnClicked += proceedToTab(Tab.Tutorials); } + public override void AddToGUIUpdateList() + { + base.AddToGUIUpdateList(); + switch (selectedTab) + { + case Tab.NewGame: + campaignSetupUI.CharacterMenus?.ForEach(static m => m.AddToGUIUpdateList()); + break; + } + } + private void UpdateTutorialList() { foreach (GUITextBlock tutorialText in tutorialList.Content.Children) @@ -951,35 +1041,55 @@ namespace Barotrauma #endif } - string arguments = - "-name \"" + ToolBox.EscapeCharacters(name) + "\"" + - " -public " + isPublicBox.Selected.ToString() + - " -playstyle " + ((PlayStyle)playstyleBanner.UserData).ToString() + - " -banafterwrongpassword " + wrongPasswordBanBox.Selected.ToString() + - " -karmaenabled " + (!karmaBox.Selected).ToString() + - " -maxplayers " + maxPlayersBox.Text + - $" -language \"{(LanguageIdentifier)languageDropdown.SelectedData}\""; + var arguments = new List + { + "-name", name, + "-public", isPublicBox.Selected.ToString(), + "-playstyle", ((PlayStyle)playstyleBanner.UserData).ToString(), + "-banafterwrongpassword", wrongPasswordBanBox.Selected.ToString(), + "-karmaenabled", (!karmaBox.Selected).ToString(), + "-maxplayers", maxPlayersBox.Text, + "-language", languageDropdown.SelectedData.ToString() + }; if (!string.IsNullOrWhiteSpace(passwordBox.Text)) { - arguments += " -password \"" + ToolBox.EscapeCharacters(passwordBox.Text) + "\""; + arguments.Add("-password"); + arguments.Add(passwordBox.Text); } else { - arguments += " -nopassword"; + arguments.Add("-nopassword"); } - if (SteamManager.GetSteamId().TryUnwrap(out var steamId1)) + var puids = EosInterface.IdQueries.GetLoggedInPuids(); + + var endpoints = new List(); + if (SteamManager.GetSteamId().TryUnwrap(out var steamId)) { - arguments += " -steamid " + steamId1.Value; + endpoints.Add(new SteamP2PEndpoint(steamId)); + } + if (puids.Length > 0) + { + endpoints.Add(new EosP2PEndpoint(puids[0])); + } + if (endpoints.Count == 0) + { + endpoints.Add(new LidgrenEndpoint(IPAddress.Loopback, NetConfig.DefaultPort)); + } + + if (endpoints.First() is P2PEndpoint firstEndpoint) + { + arguments.Add("-endpoint"); + arguments.Add(firstEndpoint.StringRepresentation); } int ownerKey = Math.Max(CryptoRandom.Instance.Next(), 1); - arguments += " -ownerkey " + ownerKey; - + arguments.Add("-ownerkey"); + arguments.Add(ownerKey.ToString()); + var processInfo = new ProcessStartInfo { FileName = fileName, - Arguments = arguments, WorkingDirectory = Directory.GetCurrentDirectory(), #if !DEBUG CreateNoWindow = true, @@ -987,16 +1097,15 @@ namespace Barotrauma WindowStyle = ProcessWindowStyle.Hidden #endif }; + arguments.ForEach(processInfo.ArgumentList.Add); ChildServerRelay.Start(processInfo); Thread.Sleep(1000); //wait until the server is ready before connecting GameMain.Client = new GameClient(MultiplayerPreferences.Instance.PlayerName.FallbackNullOrEmpty( SteamManager.GetUsername().FallbackNullOrEmpty(name)), - SteamManager.GetSteamId().TryUnwrap(out var steamId) - ? new SteamP2PEndpoint(steamId) - : (Endpoint)new LidgrenEndpoint(IPAddress.Loopback, NetConfig.DefaultPort), + endpoints.ToImmutableArray(), name, - Option.Some(ownerKey)); + Option.Some(ownerKey)); } catch (Exception e) { @@ -1010,21 +1119,6 @@ namespace Barotrauma return true; } - public override void AddToGUIUpdateList() - { - Frame.AddToGUIUpdateList(); - if (selectedTab < Tab.Empty && menuTabs.TryGetValue(selectedTab, out GUIFrame tab) && tab != null) - { - tab.AddToGUIUpdateList(); - switch (selectedTab) - { - case Tab.NewGame: - campaignSetupUI.CharacterMenus?.ForEach(m => m.AddToGUIUpdateList()); - break; - } - } - } - private void UpdateOutOfDateWorkshopItemCount() { if (DateTime.Now < modUpdateStatus.WhenToRefresh) { return; } @@ -1059,16 +1153,16 @@ namespace Barotrauma modUpdateStatus = (DateTime.Now + ModUpdateInterval, count); } + private static bool CanHostServer() + => EosInterface.IdQueries.IsLoggedIntoEosConnect + || SteamManager.IsInitialized + || AssemblyInfo.CurrentConfiguration == AssemblyInfo.Configuration.Debug; + public override void Update(double deltaTime) { -#if DEBUG - hostServerButton.Enabled = true; -#else - if (GameSettings.CurrentConfig.UseSteamMatchmaking) - { - hostServerButton.Enabled = SteamManager.IsInitialized; - } -#endif + hostServerButton.Enabled = CanHostServer(); + + gameAnalyticsStatusText.Text = TextManager.Get($"GameAnalyticsStatus.{GameAnalyticsManager.UserConsented}"); UpdateOutOfDateWorkshopItemCount(); modUpdatesButton.Visible = modUpdateStatus.Count > 0; @@ -1125,13 +1219,6 @@ namespace Barotrauma } } - readonly LocalizedString[] legalCrap = new LocalizedString[] - { - TextManager.Get("privacypolicy").Fallback("Privacy policy"), - "© " + DateTime.Now.Year + " Undertow Games & FakeFish. All rights reserved.", - "© " + DateTime.Now.Year + " Daedalic Entertainment GmbH. The Daedalic logo is a trademark of Daedalic Entertainment GmbH, Germany. All rights reserved." - }; - public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) { spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); @@ -1140,42 +1227,6 @@ namespace Barotrauma GUI.Draw(Cam, spriteBatch); - if (selectedTab != Tab.Credits) - { -#if !UNSTABLE - string versionString = "Barotrauma v" + GameMain.Version + " (" + AssemblyInfo.BuildString + ", branch " + AssemblyInfo.GitBranch + ", revision " + AssemblyInfo.GitRevision + ")"; - GUIStyle.SmallFont.DrawString(spriteBatch, versionString, new Vector2(HUDLayoutSettings.Padding, GameMain.GraphicsHeight - GUIStyle.SmallFont.MeasureString(versionString).Y - HUDLayoutSettings.Padding * 0.75f), Color.White * 0.7f); -#endif - LocalizedString gameAnalyticsStatus = TextManager.Get($"GameAnalyticsStatus.{GameAnalyticsManager.UserConsented}"); - Vector2 textSize = GUIStyle.SmallFont.MeasureString(gameAnalyticsStatus).ToPoint().ToVector2(); - GUIStyle.SmallFont.DrawString(spriteBatch, gameAnalyticsStatus, new Vector2(HUDLayoutSettings.Padding, GameMain.GraphicsHeight - GUIStyle.SmallFont.LineHeight * 2 - HUDLayoutSettings.Padding * 0.75f), Color.White * 0.7f); - - - Vector2 textPos = new Vector2(GameMain.GraphicsWidth - HUDLayoutSettings.Padding, GameMain.GraphicsHeight - HUDLayoutSettings.Padding * 0.75f); - for (int i = legalCrap.Length - 1; i >= 0; i--) - { - textSize = GUIStyle.SmallFont.MeasureString(legalCrap[i]) - .ToPoint().ToVector2(); - bool mouseOn = i == 0 && - PlayerInput.MousePosition.X > textPos.X - textSize.X && PlayerInput.MousePosition.X < textPos.X && - PlayerInput.MousePosition.Y > textPos.Y - textSize.Y && PlayerInput.MousePosition.Y < textPos.Y; - - GUIStyle.SmallFont.DrawString(spriteBatch, - legalCrap[i], textPos - textSize, - mouseOn ? Color.White : Color.White * 0.7f); - - if (i == 0) - { - GUI.DrawLine(spriteBatch, textPos, textPos - Vector2.UnitX * textSize.X, mouseOn ? Color.White : Color.White * 0.7f); - if (mouseOn && PlayerInput.PrimaryMouseButtonClicked() && GUI.MouseOn == null) - { - GameMain.ShowOpenUrlInWebBrowserPrompt("http://privacypolicy.daedalic.com"); - } - } - textPos.Y -= textSize.Y; - } - } - spriteBatch.End(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index f35e8c9d7..af8541d80 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -2320,12 +2320,12 @@ namespace Barotrauma List options = new List(); - if (client.AccountId.TryUnwrap(out var accountId) && accountId is SteamId steamId) + if (client.AccountId.TryUnwrap(out var accountId)) { - options.Add(new ContextMenuOption("ViewSteamProfile", isEnabled: hasAccountId, onSelected: () => - { - SteamManager.OverlayProfile(steamId); - })); + options.Add(new ContextMenuOption(accountId.ViewProfileLabel(), isEnabled: hasAccountId, onSelected: () => + { + accountId.OpenProfile(); + })); } options.Add(new ContextMenuOption("ModerationMenu.ManagePlayer", isEnabled: true, onSelected: () => @@ -2702,17 +2702,17 @@ namespace Barotrauma } } - if (selectedClient.AccountId.TryUnwrap(out var accountId) && accountId is SteamId steamId && Steam.SteamManager.IsInitialized) + if (selectedClient.AccountId.TryUnwrap(out var accountId)) { var viewSteamProfileButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), headerContainer.RectTransform, Anchor.TopCenter) { MaxSize = new Point(int.MaxValue, (int)(40 * GUI.Scale)) }, - TextManager.Get("ViewSteamProfile")) + accountId.ViewProfileLabel()) { UserData = selectedClient }; viewSteamProfileButton.TextBlock.AutoScaleHorizontal = true; viewSteamProfileButton.OnClicked = (bt, userdata) => { - SteamManager.OverlayProfile(steamId); + accountId.OpenProfile(); return true; }; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs index cdce5489e..7421e6f76 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs @@ -7,25 +7,20 @@ namespace Barotrauma { abstract partial class Screen { - private GUIFrame frame; - public GUIFrame Frame - { - get - { - if (frame == null) - { - frame = new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, GUI.Canvas), style: null) - { - CanBeFocused = false - }; + public readonly GUIFrame Frame; - } - return frame; - } + protected Screen() + { + Frame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas), style: null) + { + CanBeFocused = false + }; } /// - /// By default, creates a new frame for the screen and adds all elements to the gui update list. + /// By default, submits the screen's main GUIFrame and, + /// if requested upon construction, the social drawer, + /// to the GUI update list. /// public virtual void AddToGUIUpdateList() { @@ -68,9 +63,7 @@ namespace Barotrauma public virtual void Release() { - if (frame is null) { return; } - frame.RectTransform.Parent = null; - frame = null; + Frame.RectTransform.Parent = null; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs index d48c4bc39..a435e37a7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs @@ -7,7 +7,9 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Threading.Tasks; using System.Xml.Linq; +using Barotrauma.Steam; namespace Barotrauma { @@ -44,70 +46,6 @@ namespace Barotrauma Disabled } - //friends list - public sealed class FriendInfo - { - public string Name; - - public readonly AccountId Id; - - public enum Status - { - Offline, - NotPlaying, - PlayingAnotherGame, - PlayingBarotrauma - } - - public readonly Status CurrentStatus; - - public string ServerName; - - public Option ConnectCommand; - public Option Avatar; - - public bool IsInServer - => CurrentStatus == Status.PlayingBarotrauma && ConnectCommand.IsSome(); - - public bool IsPlayingBarotrauma - => CurrentStatus == Status.PlayingBarotrauma; - - public bool PlayingAnotherGame - => CurrentStatus == Status.PlayingAnotherGame; - - public bool IsOnline - => CurrentStatus != Status.Offline; - - public LocalizedString StatusText - => CurrentStatus switch - { - Status.Offline => "", - _ when ConnectCommand.IsSome() - => TextManager.GetWithVariable("FriendPlayingOnServer", "[servername]", ServerName), - _ => TextManager.Get($"Friend{CurrentStatus}") - }; - - public FriendInfo(string name, AccountId id, Status status) - { - Name = name; - Id = id; - CurrentStatus = status; - ConnectCommand = Option.None(); - Avatar = Option.None(); - } - } - - private GUILayoutGroup friendsButtonHolder; - - private GUIButton friendsDropdownButton; - private GUIListBox friendsDropdown; - - private readonly FriendProvider friendProvider = new SteamFriendProvider(); - - private List friendsList; - private GUIFrame friendPopup; - private double friendsListUpdateTime; - public enum TabEnum { All, @@ -115,7 +53,7 @@ namespace Barotrauma Recent } - public struct Tab + public readonly struct Tab { public readonly string Storage; public readonly GUIButton Button; @@ -127,7 +65,7 @@ namespace Barotrauma { Storage = storage; servers = new List(); - Button = new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), tabber.RectTransform), + Button = new GUIButton(new RectTransform(new Vector2(0.33f, 1.0f), tabber.RectTransform), TextManager.Get($"ServerListTab.{tabEnum}"), style: "GUITabButton") { OnClicked = (_,__) => @@ -187,8 +125,7 @@ namespace Barotrauma } } - private readonly ServerProvider serverProvider - = new CompositeServerProvider(new SteamDedicatedServerProvider(), new SteamP2PServerProvider()); + private ServerProvider serverProvider = null; public GUITextBox ClientNameBox { get; private set; } @@ -257,16 +194,16 @@ namespace Barotrauma private bool sortedAscending = true; private const float sidebarWidth = 0.2f; - public ServerListScreen() + public ServerListScreen() : base() { selectedServer = Option.None(); GameMain.Instance.ResolutionChanged += CreateUI; CreateUI(); } - private string GetDefaultUserName() + private static Task GetDefaultUserName() { - return friendProvider.GetUserName(); + return new CompositeFriendProvider(new SteamFriendProvider(), new EpicFriendProvider()).GetSelfUserName(); } private void AddTernaryFilter(RectTransform parent, float elementHeight, Identifier tag, Action valueSetter) @@ -331,13 +268,32 @@ namespace Barotrauma var topRow = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), paddedFrame.RectTransform)) { Stretch = true }; - var title = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.33f), topRow.RectTransform), TextManager.Get("JoinServer"), font: GUIStyle.LargeFont) + var titleContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.995f, 0.33f), topRow.RectTransform), isHorizontal: true) { Stretch = true }; + + var title = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), titleContainer.RectTransform), TextManager.Get("JoinServer"), font: GUIStyle.LargeFont) { Padding = Vector4.Zero, ForceUpperCase = ForceUpperCase.Yes, AutoScaleHorizontal = true }; + var friendsButton = new GUIButton( + new RectTransform(Vector2.One * 0.9f, titleContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), + style: "FriendsButton") + { + OnClicked = (_, _) => + { + if (SocialOverlay.Instance is { } socialOverlay) { socialOverlay.IsOpen = true; } + return false; + }, + ToolTip = TextManager.GetWithVariable("SocialOverlayShortcutHint", "[shortcut]", SocialOverlay.ShortcutBindText) + }; + new GUIFrame(new RectTransform(Vector2.One, friendsButton.RectTransform, Anchor.Center), + style: "FriendsButtonIcon") + { + CanBeFocused = false + }; + var infoHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.33f), topRow.RectTransform), isHorizontal: true, Anchor.BottomLeft) { RelativeSpacing = 0.01f, Stretch = false }; var clientNameHolder = new GUILayoutGroup(new RectTransform(new Vector2(sidebarWidth, 1.0f), infoHolder.RectTransform)) { RelativeSpacing = 0.05f }; @@ -352,7 +308,13 @@ namespace Barotrauma if (string.IsNullOrEmpty(ClientNameBox.Text)) { - ClientNameBox.Text = GetDefaultUserName(); + TaskPool.Add("GetDefaultUserName", + GetDefaultUserName(), + t => + { + if (!t.TryGetResult(out string name)) { return; } + if (ClientNameBox.Text.IsNullOrEmpty()) { ClientNameBox.Text = name; } + }); } ClientNameBox.OnTextChanged += (textbox, text) => { @@ -366,14 +328,6 @@ namespace Barotrauma tabs[TabEnum.Favorites] = new Tab(TabEnum.Favorites, this, tabButtonHolder, "Data/favoriteservers.xml"); tabs[TabEnum.Recent] = new Tab(TabEnum.Recent, this, tabButtonHolder, "Data/recentservers.xml"); - var friendsButtonFrame = new GUIFrame(new RectTransform(new Vector2(0.31f, 2.0f), tabButtonHolder.RectTransform, Anchor.BottomRight), style: "InnerFrame") - { - IgnoreLayoutGroups = true - }; - - friendsButtonHolder = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.9f), friendsButtonFrame.RectTransform, Anchor.Center), childAnchor: Anchor.TopLeft) { RelativeSpacing = 0.01f, IsHorizontal = true }; - friendsList = new List(); - //------------------------------------------------------------------------------------- // Bottom row //------------------------------------------------------------------------------------- @@ -740,7 +694,7 @@ namespace Barotrauma { if (selectedServer.TryUnwrap(out var serverInfo)) { - JoinServer(serverInfo.Endpoint, serverInfo.ServerName); + JoinServer(serverInfo.Endpoints, serverInfo.ServerName); } return true; }, @@ -778,7 +732,7 @@ namespace Barotrauma { GUIComponent existingElement = serverList.Content.FindChild(d => d.UserData is ServerInfo existingServerInfo && - existingServerInfo.Endpoint == serverInfo.Endpoint); + existingServerInfo.Endpoints.Any(serverInfo.Endpoints.Contains)); if (existingElement == null) { AddToServerList(serverInfo); @@ -791,7 +745,7 @@ namespace Barotrauma public void AddToRecentServers(ServerInfo info) { - if (info.Endpoint.Address.IsLocalHost) { return; } + if (info.Endpoints.First().Address.IsLocalHost) { return; } tabs[TabEnum.Recent].AddOrUpdate(info); tabs[TabEnum.Recent].Save(); } @@ -924,7 +878,32 @@ namespace Barotrauma public override void Select() { base.Select(); - + + if (EosInterface.IdQueries.IsLoggedIntoEosConnect) + { + if (SteamManager.IsInitialized) + { + serverProvider = new CompositeServerProvider( + new EosServerProvider(), + new SteamDedicatedServerProvider(), + new SteamP2PServerProvider()); + } + else + { + serverProvider = new EosServerProvider(); + } + } + else if (SteamManager.IsInitialized) + { + serverProvider = new CompositeServerProvider( + new SteamDedicatedServerProvider(), + new SteamP2PServerProvider()); + } + else + { + serverProvider = null; + } + Steamworks.SteamMatchmaking.ResetActions(); selectedTab = TabEnum.All; @@ -957,6 +936,7 @@ namespace Barotrauma public override void Deselect() { base.Deselect(); + serverProvider?.Cancel(); GameSettings.SaveCurrentConfig(); } @@ -964,21 +944,9 @@ namespace Barotrauma { base.Update(deltaTime); - UpdateFriendsList(); panelAnimator?.Update(); scanServersButton.Enabled = (DateTime.Now - lastRefreshTime) >= AllowedRefreshInterval; - - if (PlayerInput.PrimaryMouseButtonClicked()) - { - friendPopup = null; - if (friendsDropdown != null && friendsDropdownButton != null && - !friendsDropdown.Rect.Contains(PlayerInput.MousePosition) && - !friendsDropdownButton.Rect.Contains(PlayerInput.MousePosition)) - { - friendsDropdown.Visible = false; - } - } } public void FilterServers() @@ -1113,7 +1081,8 @@ namespace Barotrauma RelativeSpacing = 0.05f }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform), TextManager.Get("ServerEndpoint"), textAlignment: Alignment.Center); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform), + SteamManager.IsInitialized ? TextManager.Get("ServerEndpoint") : TextManager.Get("ServerIP"), textAlignment: Alignment.Center); var endpointBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform)); content.RectTransform.NonScaledSize = new Point(content.Rect.Width, (int)(content.RectTransform.Children.Sum(c => c.Rect.Height))); @@ -1126,11 +1095,18 @@ namespace Barotrauma { if (Endpoint.Parse(endpointBox.Text).TryUnwrap(out var endpoint)) { - JoinServer(endpoint, ""); + if (endpoint is SteamP2PEndpoint && !SteamManager.IsInitialized) + { + new GUIMessageBox(TextManager.Get("error"), TextManager.Get("CannotJoinSteamServer.SteamNotInitialized")); + } + else + { + JoinServer(endpoint.ToEnumerable().ToImmutableArray(), ""); + } } else if (LidgrenEndpoint.ParseFromWithHostNameCheck(endpointBox.Text, tryParseHostName: true).TryUnwrap(out var lidgrenEndpoint)) { - JoinServer(lidgrenEndpoint, ""); + JoinServer(((Endpoint)lidgrenEndpoint).ToEnumerable().ToImmutableArray(), ""); } else { @@ -1171,8 +1147,6 @@ namespace Barotrauma selectedTab = TabEnum.Favorites; FilterServers(); - #warning Interface with server providers to get up-to-date info on the given server - msgBox.Close(); return false; }; @@ -1187,222 +1161,6 @@ namespace Barotrauma }; } - private bool JoinFriend(GUIButton button, object userdata) - { - if (!(userdata is FriendInfo { IsInServer: true } info)) { return false; } - - GameMain.Instance.ConnectCommand = info.ConnectCommand; - return false; - } - - private bool OpenFriendPopup(GUIButton button, object userdata) - { - if (!(userdata is FriendInfo { IsInServer: true } info)) { return false; } - - if (info.IsInServer - && info.ConnectCommand.TryUnwrap(out var command) - && command.EndpointOrLobby.TryGet(out ConnectCommand.NameAndEndpoint nameAndEndpoint)) - { - const int framePadding = 5; - - friendPopup = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas)); - - var serverNameText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), friendPopup.RectTransform, Anchor.CenterLeft), nameAndEndpoint.ServerName ?? "[Unnamed]"); - serverNameText.RectTransform.AbsoluteOffset = new Point(framePadding, 0); - - var joinButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), friendPopup.RectTransform, Anchor.CenterRight), TextManager.Get("ServerListJoin")) - { - UserData = info - }; - joinButton.OnClicked = JoinFriend; - joinButton.RectTransform.AbsoluteOffset = new Point(framePadding, 0); - - Point joinButtonTextSize = joinButton.Font.MeasureString(joinButton.Text).ToPoint(); - int joinButtonHeight = joinButton.RectTransform.NonScaledSize.Y; - int totalAdditionalTextPadding = (joinButtonHeight - joinButtonTextSize.Y); - - // Make the final button sized so that the space between the text and the edges in the X direction is the same as the Y direction. - Point finalButtonSize = new Point(joinButtonTextSize.X + totalAdditionalTextPadding, joinButtonHeight); - - // Add padding to the server name to match the padding on the button text. - serverNameText.Padding = new Vector4(totalAdditionalTextPadding / 2); - - // Get the dimensions of the text we want to show, plus the extra padding we added. - Point serverNameSize = serverNameText.Font.MeasureString(serverNameText.Text).ToPoint() + new Point(totalAdditionalTextPadding, totalAdditionalTextPadding); - - // Now determine how large the parent frame has to be to exactly fit our two controls. - Point frameDims = new Point(serverNameSize.X + finalButtonSize.X + framePadding*2, Math.Max(serverNameSize.Y, finalButtonSize.Y) + framePadding * 2); - - var popupPos = PlayerInput.MousePosition.ToPoint(); - if(popupPos.X+frameDims.X > GUI.Canvas.NonScaledSize.X) - { - // Prevent the Join button from going off the end of the screen if the server name is long or we click a user towards the edge. - popupPos.X = GUI.Canvas.NonScaledSize.X - frameDims.X; - } - - // Apply the size and position changes. - friendPopup.RectTransform.NonScaledSize = frameDims; - friendPopup.RectTransform.RelativeOffset = Vector2.Zero; - friendPopup.RectTransform.AbsoluteOffset = popupPos; - - joinButton.RectTransform.NonScaledSize = finalButtonSize; - - friendPopup.RectTransform.RecalculateChildren(true); - friendPopup.RectTransform.SetPosition(Anchor.TopLeft); - } - - return false; - } - - public enum AvatarSize - { - Small, - Medium, - Large - } - - private void UpdateFriendsList() - { - if (friendsListUpdateTime > Timing.TotalTime) { return; } - friendsListUpdateTime = Timing.TotalTime + 5.0; - - float prevDropdownScroll = friendsDropdown?.ScrollBar.BarScrollValue ?? 0.0f; - - friendsDropdown ??= new GUIListBox(new RectTransform(Vector2.One, GUI.Canvas)) - { - OutlineColor = Color.Black, - Visible = false - }; - friendsDropdown.ClearChildren(); - - var avatarSize = friendsButtonHolder.RectTransform.Rect.Height switch - { - var h when h <= 24 => AvatarSize.Small, - var h when h <= 48 => AvatarSize.Medium, - _ => AvatarSize.Large - }; - - FriendInfo[] friends = friendProvider.RetrieveFriends(); - - foreach (var friend in friends) - { - int existingIndex = friendsList.FindIndex(f => f.Id == friend.Id); - if (existingIndex >= 0) - { - friend.Avatar = friend.Avatar.Fallback(friendsList[existingIndex].Avatar); - } - - if (friend.Avatar.IsNone()) - { - friendProvider.RetrieveAvatar(friend, avatarSize); - } - } - - friendsList.Clear(); friendsList.AddRange(friends.OrderByDescending(f => f.CurrentStatus)); - - friendsButtonHolder.ClearChildren(); - - if (friendsList.Count > 0) - { - friendsDropdownButton = new GUIButton(new RectTransform(Vector2.One, friendsButtonHolder.RectTransform, Anchor.BottomRight, Pivot.BottomRight, scaleBasis: ScaleBasis.BothHeight), "\u2022 \u2022 \u2022", style: "GUIButtonFriendsDropdown") - { - OnClicked = (button, udt) => - { - friendsDropdown.RectTransform.NonScaledSize = new Point(friendsButtonHolder.Rect.Height * 5 * 166 / 100, friendsButtonHolder.Rect.Height * 4 * 166 / 100); - friendsDropdown.RectTransform.AbsoluteOffset = new Point(friendsButtonHolder.Rect.X, friendsButtonHolder.Rect.Bottom); - friendsDropdown.RectTransform.RecalculateChildren(true); - friendsDropdown.Visible = !friendsDropdown.Visible; - return false; - } - }; - } - else - { - friendsDropdownButton = null; - friendsDropdown.Visible = false; - } - - for (int i = 0; i < friendsList.Count; i++) - { - var friend = friendsList[i]; - - if (i < 5) - { - string style = friend.IsPlayingBarotrauma - ? "GUIButtonFriendPlaying" - : "GUIButtonFriendNotPlaying"; - - var guiButton = new GUIButton(new RectTransform(Vector2.One, friendsButtonHolder.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: style) - { - UserData = friend, - OnClicked = OpenFriendPopup - }; - guiButton.ToolTip = friend.Name + "\n" + friend.StatusText; - - if (friend.Avatar.TryUnwrap(out Sprite sprite)) - { - new GUICustomComponent(new RectTransform(Vector2.One, guiButton.RectTransform, Anchor.Center), - onDraw: (sb, component) => - { - var destinationRect = component.Rect; - destinationRect.Inflate(-GUI.IntScale(4), -GUI.IntScale(4)); - sb.Draw(sprite.Texture, destinationRect, Color.White); - - if (!GUI.IsMouseOn(guiButton)) - { - return; - } - - sb.End(); - sb.Begin( - SpriteSortMode.Deferred, - blendState: BlendState.Additive, - samplerState: GUI.SamplerState, - rasterizerState: GameMain.ScissorTestEnable); - sb.Draw(sprite.Texture, destinationRect, Color.White * 0.5f); - sb.End(); - sb.Begin( - SpriteSortMode.Deferred, - samplerState: GUI.SamplerState, - rasterizerState: GameMain.ScissorTestEnable); - }) { CanBeFocused = false }; - } - } - - var friendFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.167f), friendsDropdown.Content.RectTransform), style: "GUIFrameFriendsDropdown"); - if (friend.Avatar.TryUnwrap(out var avatar)) - { - GUIImage guiImage = - new GUIImage( - new RectTransform(Vector2.One * 0.9f, friendFrame.RectTransform, Anchor.CenterLeft, - scaleBasis: ScaleBasis.BothHeight) { RelativeOffset = new Vector2(0.02f, 0.02f) }, - avatar, null, true); - } - - var textBlock = new GUITextBlock(new RectTransform(Vector2.One * 0.8f, friendFrame.RectTransform, Anchor.CenterLeft, scaleBasis: ScaleBasis.BothHeight) { RelativeOffset = new Vector2(1.0f / 7.7f, 0.0f) }, friend.Name + "\n" + friend.StatusText) - { - Font = GUIStyle.SmallFont - }; - if (friend.IsPlayingBarotrauma) { textBlock.TextColor = GUIStyle.Green; } - if (friend.PlayingAnotherGame) { textBlock.TextColor = GUIStyle.Blue; } - - if (friend.IsInServer) - { - var joinButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.6f), friendFrame.RectTransform, Anchor.CenterRight) { RelativeOffset = new Vector2(0.05f, 0.0f) }, TextManager.Get("ServerListJoin"), style: "GUIButtonJoinFriend") - { - UserData = friend, - OnClicked = JoinFriend - }; - } - } - - friendsDropdown.RectTransform.NonScaledSize = new Point(friendsButtonHolder.Rect.Height * 5 * 166 / 100, friendsButtonHolder.Rect.Height * 4 * 166 / 100); - friendsDropdown.RectTransform.AbsoluteOffset = new Point(friendsButtonHolder.Rect.X, friendsButtonHolder.Rect.Bottom); - friendsDropdown.RectTransform.RecalculateChildren(true); - - friendsDropdown.ScrollBar.BarScrollValue = prevDropdownScroll; - } - private void RemoveMsgFromServerList() { serverList.Content.Children @@ -1429,7 +1187,7 @@ namespace Barotrauma private void RefreshServers() { lastRefreshTime = DateTime.Now; - serverProvider.Cancel(); + serverProvider?.Cancel(); currentServerDataRecvCallbackObj = null; PingUtils.QueryPingData(); @@ -1462,7 +1220,7 @@ namespace Barotrauma } var (onServerDataReceived, onQueryCompleted) = MakeServerQueryCallbacks(); - serverProvider.RetrieveServers(onServerDataReceived, onQueryCompleted); + serverProvider?.RetrieveServers(onServerDataReceived, onQueryCompleted); } private GUIComponent FindFrameMatchingServerInfo(ServerInfo serverInfo) @@ -1474,7 +1232,7 @@ namespace Barotrauma #if DEBUG if (serverList.Content.Children.Count(matches) > 1) { - DebugConsole.ThrowError($"There are several entries in the server list for endpoint {serverInfo.Endpoint}"); + DebugConsole.ThrowError($"There are several entries in the server list for endpoints {string.Join(", ", serverInfo.Endpoints)}"); } #endif @@ -1482,7 +1240,7 @@ namespace Barotrauma } private object currentServerDataRecvCallbackObj = null; - private (Action OnServerDataReceived, Action OnQueryCompleted) MakeServerQueryCallbacks() + private (Action OnServerDataReceived, Action OnQueryCompleted) MakeServerQueryCallbacks() { var uniqueObject = new object(); currentServerDataRecvCallbackObj = uniqueObject; @@ -1497,10 +1255,21 @@ namespace Barotrauma } return ( - serverInfo => + (serverInfo, serverProvider) => { if (!shouldRunCallback()) { return; } + if (serverProvider is not EosServerProvider + && EosInterface.IdQueries.IsLoggedIntoEosConnect) + { + if (serverInfo.EosCrossplay) + { + // EosServerProvider should get us this server, + // don't add it again + return; + } + } + if (selectedTab == TabEnum.All) { AddToServerList(serverInfo); @@ -1724,7 +1493,7 @@ namespace Barotrauma private static void ReportServer(ServerInfo info, IEnumerable reasons) { if (!reasons.Any()) { return; } - GameAnalyticsManager.AddErrorEvent(GameAnalyticsManager.ErrorSeverity.Info, $"[Spam] Reported server: Name: \"{info.ServerName}\", Message: \"{info.ServerMessage}\", Endpoint: \"{info.Endpoint.StringRepresentation}\". Reason: \"{string.Join(", ", reasons)}\"."); + GameAnalyticsManager.AddErrorEvent(GameAnalyticsManager.ErrorSeverity.Info, $"[Spam] Reported server: Name: \"{info.ServerName}\", Message: \"{info.ServerMessage}\", Endpoint: \"{info.Endpoints.First().StringRepresentation}\". Reason: \"{string.Join(", ", reasons)}\"."); } private void UpdateServerInfoUI(ServerInfo serverInfo) @@ -1784,7 +1553,7 @@ namespace Barotrauma var serverName = new GUITextBlock(columnRT(ColumnLabel.ServerListName), #if DEBUG - $"[{serverInfo.Endpoint.GetType().Name}] " + + $"[{serverInfo.Endpoints.First().GetType().Name}] " + #endif serverInfo.ServerName, style: "GUIServerListTextBox") { CanBeFocused = false }; @@ -1819,6 +1588,13 @@ namespace Barotrauma serverPingText.Text = ping.ToString(); serverPingText.TextColor = GetPingTextColor(ping); } + else if ((serverInfo.Endpoints.Length == 1 && serverInfo.Endpoints.First() is EosP2PEndpoint) + || (!SteamManager.IsInitialized && serverInfo.Endpoints.Any(e => e is P2PEndpoint))) + { + serverPingText.Text = "-"; + serverPingText.ToolTip = TextManager.Get("EosPingUnavailable"); + serverPingText.TextAlignment = Alignment.Center; + } else { serverPingText.Text = "?"; @@ -1954,7 +1730,7 @@ namespace Barotrauma } } - public void JoinServer(Endpoint endpoint, string serverName) + public void JoinServer(ImmutableArray endpoints, string serverName) { if (string.IsNullOrWhiteSpace(ClientNameBox.Text)) { @@ -1967,20 +1743,38 @@ namespace Barotrauma MultiplayerPreferences.Instance.PlayerName = ClientNameBox.Text; GameSettings.SaveCurrentConfig(); -#if !DEBUG - try + if (MultiplayerPreferences.Instance.PlayerName.IsNullOrEmpty()) { -#endif - GameMain.Client = new GameClient(MultiplayerPreferences.Instance.PlayerName.FallbackNullOrEmpty(GetDefaultUserName()), endpoint, serverName, Option.None()); -#if !DEBUG + TaskPool.Add("GetDefaultUserName", + GetDefaultUserName(), + t => + { + if (!t.TryGetResult(out string name)) { return; } + startClient(name); + }); } - catch (Exception e) + else { - DebugConsole.ThrowError("Failed to start the client", e); + startClient(MultiplayerPreferences.Instance.PlayerName); } + + void startClient(string name) + { +#if !DEBUG + try + { #endif + GameMain.Client = new GameClient(name, endpoints, serverName, Option.None); +#if !DEBUG + } + catch (Exception e) + { + DebugConsole.ThrowError("Failed to start the client", e); + } +#endif + } } - + private static Color GetPingTextColor(int ping) { if (ping < 0) { return Color.DarkRed; } @@ -2000,8 +1794,6 @@ namespace Barotrauma public override void AddToGUIUpdateList() { menu.AddToGUIUpdateList(); - friendPopup?.AddToGUIUpdateList(); - friendsDropdown?.AddToGUIUpdateList(); } public void StoreServerFilters() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 768e1f4cc..cabd7e4fd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -1554,7 +1554,6 @@ namespace Barotrauma autoSaveLabel?.Parent?.RemoveChild(autoSaveLabel); autoSaveLabel = null; -#if USE_STEAM if (editorSelectedTime.TryUnwrap(out DateTime selectedTime)) { TimeSpan timeInEditor = DateTime.Now - selectedTime; @@ -1568,11 +1567,10 @@ namespace Barotrauma } else { - SteamAchievementManager.IncrementStat("hoursineditor".ToIdentifier(), (float)timeInEditor.TotalHours); + AchievementManager.IncrementStat(AchievementStat.HoursInEditor, (float)timeInEditor.TotalHours); editorSelectedTime = Option.None(); } } -#endif GUI.ForceMouseOn(null); @@ -4173,7 +4171,7 @@ namespace Barotrauma Rectangle newColorRect = new Rectangle(rect.Location, areaSize); Rectangle oldColorRect = new Rectangle(new Point(newColorRect.Left, newColorRect.Bottom), areaSize); - GUI.DrawRectangle(batch, newColorRect, ToolBox.HSVToRGB(colorPicker.SelectedHue, colorPicker.SelectedSaturation, colorPicker.SelectedValue), isFilled: true); + GUI.DrawRectangle(batch, newColorRect, ToolBoxCore.HSVToRGB(colorPicker.SelectedHue, colorPicker.SelectedSaturation, colorPicker.SelectedValue), isFilled: true); GUI.DrawRectangle(batch, oldColorRect, originalColor, isFilled: true); GUI.DrawRectangle(batch, rect, Color.Black, isFilled: false); }); @@ -4293,7 +4291,7 @@ namespace Barotrauma setValues = true; } - Color color = ToolBox.HSVToRGB(colorPicker.SelectedHue, colorPicker.SelectedSaturation, colorPicker.SelectedValue); + Color color = ToolBoxCore.HSVToRGB(colorPicker.SelectedHue, colorPicker.SelectedSaturation, colorPicker.SelectedValue); foreach (var (e, origColor, prop) in entities) { if (e is MapEntity { Removed: true }) { continue; } @@ -4327,7 +4325,7 @@ namespace Barotrauma void SetHex(Vector3 hsv) { - Color hexColor = ToolBox.HSVToRGB(hsv.X, hsv.Y, hsv.Z); + Color hexColor = ToolBoxCore.HSVToRGB(hsv.X, hsv.Y, hsv.Z); hexValueBox!.Text = ColorToHex(hexColor); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs index 24d40dd72..f8c7eacf5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; using System.Linq; +using Barotrauma.Eos; using Barotrauma.Extensions; using Barotrauma.Networking; using Barotrauma.Steam; @@ -14,7 +15,7 @@ using OpenAL; namespace Barotrauma { - class SettingsMenu + sealed class SettingsMenu { public static SettingsMenu? Instance { get; private set; } @@ -682,22 +683,22 @@ namespace Barotrauma private void CreateGameplayTab() { GUIFrame content = CreateNewContentFrame(Tab.Gameplay); - - GUILayoutGroup layout = CreateCenterLayout(content); + + var (left, right) = CreateSidebars(content); var languages = TextManager.AvailableLanguages .OrderBy(l => TextManager.GetTranslatedLanguageName(l).ToIdentifier()) .ToArray(); - Label(layout, TextManager.Get("Language"), GUIStyle.SubHeadingFont); - Dropdown(layout, v => TextManager.GetTranslatedLanguageName(v), null, languages, unsavedConfig.Language, v => unsavedConfig.Language = v); - Spacer(layout); - - Tickbox(layout, TextManager.Get("PauseOnFocusLost"), TextManager.Get("PauseOnFocusLostTooltip"), unsavedConfig.PauseOnFocusLost, v => unsavedConfig.PauseOnFocusLost = v); - Spacer(layout); - - Tickbox(layout, TextManager.Get("DisableInGameHints"), TextManager.Get("DisableInGameHintsTooltip"), unsavedConfig.DisableInGameHints, v => unsavedConfig.DisableInGameHints = v); + Label(left, TextManager.Get("Language"), GUIStyle.SubHeadingFont); + Dropdown(left, v => TextManager.GetTranslatedLanguageName(v), null, languages, unsavedConfig.Language, v => unsavedConfig.Language = v); + Spacer(left); + + Tickbox(left, TextManager.Get("PauseOnFocusLost"), TextManager.Get("PauseOnFocusLostTooltip"), unsavedConfig.PauseOnFocusLost, v => unsavedConfig.PauseOnFocusLost = v); + Spacer(left); + + Tickbox(left, TextManager.Get("DisableInGameHints"), TextManager.Get("DisableInGameHintsTooltip"), unsavedConfig.DisableInGameHints, v => unsavedConfig.DisableInGameHints = v); var resetInGameHintsButton = - new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), layout.RectTransform), + new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), left.RectTransform), TextManager.Get("ResetInGameHints"), style: "GUIButtonSmall") { OnClicked = (button, o) => @@ -715,36 +716,22 @@ namespace Barotrauma return false; } }; - Spacer(layout); + Spacer(left); - Label(layout, TextManager.Get("ShowEnemyHealthBars"), GUIStyle.SubHeadingFont); - DropdownEnum(layout, v => TextManager.Get($"ShowEnemyHealthBars.{v}"), null, unsavedConfig.ShowEnemyHealthBars, v => unsavedConfig.ShowEnemyHealthBars = v); - Spacer(layout); + Label(left, TextManager.Get("ShowEnemyHealthBars"), GUIStyle.SubHeadingFont); + DropdownEnum(left, v => TextManager.Get($"ShowEnemyHealthBars.{v}"), null, unsavedConfig.ShowEnemyHealthBars, v => unsavedConfig.ShowEnemyHealthBars = v); + Spacer(left); + + Label(left, TextManager.Get("HUDScale"), GUIStyle.SubHeadingFont); + Slider(left, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.HUDScale, v => unsavedConfig.Graphics.HUDScale = v); + Label(left, TextManager.Get("InventoryScale"), GUIStyle.SubHeadingFont); + Slider(left, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.InventoryScale, v => unsavedConfig.Graphics.InventoryScale = v); + Label(left, TextManager.Get("TextScale"), GUIStyle.SubHeadingFont); + Slider(left, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.TextScale, v => unsavedConfig.Graphics.TextScale = v); - Label(layout, TextManager.Get("HUDScale"), GUIStyle.SubHeadingFont); - Slider(layout, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.HUDScale, v => unsavedConfig.Graphics.HUDScale = v); - Label(layout, TextManager.Get("InventoryScale"), GUIStyle.SubHeadingFont); - Slider(layout, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.InventoryScale, v => unsavedConfig.Graphics.InventoryScale = v); - Label(layout, TextManager.Get("TextScale"), GUIStyle.SubHeadingFont); - Slider(layout, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.TextScale, v => unsavedConfig.Graphics.TextScale = v); - Spacer(layout); - var resetSpamListFilter = - new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), layout.RectTransform), - TextManager.Get("clearserverlistfilters"), style: "GUIButtonSmall") - { - OnClicked = static (_, _) => - { - GUI.AskForConfirmation( - header: TextManager.Get("clearserverlistfilters"), - body: TextManager.Get("clearserverlistfiltersconfirmation"), - onConfirm: SpamServerFilters.ClearLocalSpamFilter); - return true; - } - }; - Spacer(layout); #if !OSX - Spacer(layout); - var statisticsTickBox = new GUITickBox(NewItemRectT(layout), TextManager.Get("statisticsconsenttickbox")) + Spacer(right); + var statisticsTickBox = new GUITickBox(NewItemRectT(right), TextManager.Get("statisticsconsenttickbox")) { OnSelected = tickBox => { @@ -767,7 +754,7 @@ namespace Barotrauma void updateGATickBoxToolTip() => statisticsTickBox.ToolTip = TextManager.Get($"GameAnalyticsStatus.{GameAnalyticsManager.UserConsented}"); updateGATickBoxToolTip(); - + var cachedConsent = GameAnalyticsManager.Consent.Unknown; var statisticsTickBoxUpdater = new GUICustomComponent( new RectTransform(Vector2.Zero, statisticsTickBox.RectTransform), @@ -789,6 +776,37 @@ namespace Barotrauma statisticsTickBox.Enabled &= GameAnalyticsManager.UserConsented != GameAnalyticsManager.Consent.Error; }); #endif + //Steam version supports hosting/joining servers using EOS networking + if (SteamManager.IsInitialized) + { + bool shouldCrossplayBeEnabled = unsavedConfig.CrossplayChoice is Eos.EosSteamPrimaryLogin.CrossplayChoice.Enabled; + var crossplayTickBox = Tickbox(right, TextManager.Get("EosAllowCrossplay"), TextManager.Get("EosAllowCrossplayTooltip"), shouldCrossplayBeEnabled, v => + { + unsavedConfig.CrossplayChoice = v + ? Eos.EosSteamPrimaryLogin.CrossplayChoice.Enabled + : Eos.EosSteamPrimaryLogin.CrossplayChoice.Disabled; + }); + if (GameMain.NetworkMember != null) + { + crossplayTickBox.Enabled = false; + crossplayTickBox.ToolTip = TextManager.Get("CantAccessEOSSettingsInMP"); + } + } + + Spacer(right); + var resetSpamListFilter = + new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), right.RectTransform), + TextManager.Get("clearserverlistfilters"), style: "GUIButtonSmall") + { + OnClicked = static (_, _) => + { + GUI.AskForConfirmation( + header: TextManager.Get("clearserverlistfilters"), + body: TextManager.Get("clearserverlistfiltersconfirmation"), + onConfirm: SpamServerFilters.ClearLocalSpamFilter); + return true; + } + }; } private void CreateModsTab(out WorkshopMenu workshopMenu) @@ -832,9 +850,9 @@ namespace Barotrauma public void ApplyInstalledModChanges() { + EosSteamPrimaryLogin.HandleCrossplayChoiceChange(unsavedConfig.CrossplayChoice); GameSettings.SetCurrentConfig(unsavedConfig); - if (WorkshopMenu is MutableWorkshopMenu mutableWorkshopMenu && - mutableWorkshopMenu.CurrentTab == MutableWorkshopMenu.Tab.InstalledMods) + if (WorkshopMenu is MutableWorkshopMenu { CurrentTab: MutableWorkshopMenu.Tab.InstalledMods } mutableWorkshopMenu) { mutableWorkshopMenu.Apply(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Social/FriendInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Social/FriendInfo.cs new file mode 100644 index 000000000..eb17cecd3 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Social/FriendInfo.cs @@ -0,0 +1,73 @@ +#nullable enable +using System; +using Barotrauma.Networking; + +namespace Barotrauma; + +sealed class FriendInfo : IDisposable +{ + public readonly string Name; + public readonly AccountId Id; + + public readonly FriendStatus CurrentStatus; + public readonly string ServerName; + public readonly Option ConnectCommand; + + public readonly FriendProvider Provider; + public Option Avatar { get; set; } + + public bool IsInServer + => CurrentStatus == FriendStatus.PlayingBarotrauma && ConnectCommand.IsSome(); + + public bool IsOnline + => CurrentStatus != FriendStatus.Offline; + + public LocalizedString StatusText + => CurrentStatus switch + { + FriendStatus.Offline => "", + _ when ConnectCommand.IsSome() + => TextManager.GetWithVariable("FriendPlayingOnServer", "[servername]", ServerName), + _ => TextManager.Get($"Friend{CurrentStatus}") + }; + + public FriendInfo(string name, AccountId id, FriendStatus status, string serverName, Option connectCommand, FriendProvider provider) + { + Name = name; + Id = id; + CurrentStatus = status; + ServerName = serverName; + ConnectCommand = connectCommand; + Provider = provider; + Avatar = Option.None; + } + + public void RetrieveOrInheritAvatar(Option inheritableAvatar, int size) + { + if (Avatar.IsSome()) { return; } + + if (inheritableAvatar.IsSome()) + { + Avatar = inheritableAvatar; + return; + } + + TaskPool.Add( + "RetrieveAvatar", + Provider.RetrieveAvatar(this, size), + t => + { + if (!t.TryGetResult(out Option spr)) { return; } + Avatar = Avatar.Fallback(spr); + }); + } + + public void Dispose() + { + if (Avatar.TryUnwrap(out var avatar)) + { + avatar.Remove(); + } + Avatar = Option.None; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Social/FriendProviders/CompositeFriendProvider.cs b/Barotrauma/BarotraumaClient/ClientSource/Social/FriendProviders/CompositeFriendProvider.cs new file mode 100644 index 000000000..950881f06 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Social/FriendProviders/CompositeFriendProvider.cs @@ -0,0 +1,46 @@ +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Barotrauma.Extensions; +using Barotrauma.Networking; + +namespace Barotrauma; + +sealed class CompositeFriendProvider : FriendProvider +{ + private readonly ImmutableArray providers; + + public CompositeFriendProvider(params FriendProvider[] providers) + { + this.providers = providers.ToImmutableArray(); + } + + public override async Task> RetrieveFriend(AccountId id) + { + return (await Task.WhenAll(providers + .Select(p => p.RetrieveFriend(id)))) + .NotNone().FirstOrNone(); + } + + public override async Task> RetrieveFriends() + { + var friends = await Task.WhenAll(providers.Select(p => p.RetrieveFriends())); + return friends.SelectMany(a => a).ToImmutableArray(); + } + + public override async Task> RetrieveAvatar(FriendInfo friend, int avatarSize) + { + var subTasks = await Task.WhenAll(providers.Select(p => p.RetrieveAvatar(friend, avatarSize))); + return subTasks.FirstOrDefault(t => t.IsSome()); + } + + public override async Task GetSelfUserName() + { + foreach (var provider in providers) + { + string userName = await provider.GetSelfUserName(); + if (userName is { Length: > 0 }) { return userName; } + } + return ""; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Social/FriendProviders/EpicFriendProvider.cs b/Barotrauma/BarotraumaClient/ClientSource/Social/FriendProviders/EpicFriendProvider.cs new file mode 100644 index 000000000..a1402a974 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Social/FriendProviders/EpicFriendProvider.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Immutable; +using System.Globalization; +using System.Linq; +using System.Numerics; +using System.Threading.Tasks; +using Barotrauma.Extensions; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Vector2 = Microsoft.Xna.Framework.Vector2; + +namespace Barotrauma; + +sealed class EpicFriendProvider : FriendProvider +{ + private FriendInfo EgsFriendToFriendInfo(EosInterface.EgsFriend egsFriend) + { + return new FriendInfo( + name: egsFriend.DisplayName, + id: egsFriend.EpicAccountId, + status: egsFriend.Status, + serverName: egsFriend.ServerName, + connectCommand: ConnectCommand.Parse(egsFriend.ConnectCommand), + provider: this); + } + + public override async Task> RetrieveFriend(AccountId id) + { + if (id is not EpicAccountId friendEaid) { return Option.None; } + var selfEaidOption = Eos.EosAccount.SelfAccountIds.OfType().FirstOrNone(); + if (!selfEaidOption.TryUnwrap(out var selfEaid)) { return Option.None; } + + var friendResult = await EosInterface.Friends.GetFriend(selfEaid, friendEaid); + if (!friendResult.TryUnwrapSuccess(out var f)) { return Option.None; } + + return Option.Some(EgsFriendToFriendInfo(f)); + } + + public override async Task> RetrieveFriends() + { + var epicAccountIdOption = Eos.EosAccount.SelfAccountIds.OfType().FirstOrNone(); + if (!epicAccountIdOption.TryUnwrap(out var epicAccountId)) { return ImmutableArray.Empty; } + + var friendsResult = await EosInterface.Friends.GetFriends(epicAccountId); + if (!friendsResult.TryUnwrapSuccess(out var friends)) { return ImmutableArray.Empty; } + + return friends.Select(EgsFriendToFriendInfo).ToImmutableArray(); + } + + private static readonly ImmutableArray egsProfileColors = new[] + { + // Cyan + new Color(0xfff0a950), + + // Dark green + new Color(0xff2b9850), + + // Yellow-green + new Color(0xff2ba08e), + + // Purple + new Color(0xff951249), + + // Purple-red + new Color(0xff9a0c71), + + // Red + new Color(0xff3e29c6), + + // Orange + new Color(0xff3875ed), + + // Yellow-orange + new Color(0xff1ea5ed) + }.ToImmutableArray(); + + public override Task> RetrieveAvatar(FriendInfo friend, int avatarSize) + { + if (friend.Id is not EpicAccountId epicAccount) { return Task.FromResult>(Option.None); } + + // EGS doesn't have profile pictures yet. + // Instead, each player gets a color based on their account ID. + // This is an educated guess of how Epic picks that color, and is likely incorrect for IDs nearing the boundaries of ranges: + Color color = Color.Black; + if (ulong.TryParse(epicAccount.EosStringRepresentation[..16], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var mostSignificant64Bits) + && ulong.TryParse(epicAccount.EosStringRepresentation[16..], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var leastSignificant64Bits)) + { + BigInteger fullId = mostSignificant64Bits; + fullId <<= 64; + fullId |= leastSignificant64Bits; + + BigInteger idMaxValue = ulong.MaxValue; + idMaxValue <<= 64; + idMaxValue |= ulong.MaxValue; + + BigInteger middleRangeSize = idMaxValue / 7; + BigInteger firstRangeSize = middleRangeSize / 2; + if (fullId <= firstRangeSize) + { + color = egsProfileColors[0]; + } + else + { + color = egsProfileColors[(int)((fullId - firstRangeSize) / middleRangeSize) + 1]; + } + } + + char glyphChar = friend.Name.FallbackNullOrEmpty("?")[0]; + var font = GUIStyle.UnscaledSmallFont.GetFontForStr(glyphChar.ToString()); + + Texture2D tex = null; + if (font != null) + { + var (glyphData, glyphTexture) = font.GetGlyphDataAndTextureForChar(glyphChar); + var glyphSize = new Vector2(glyphData.TexCoords.Width, glyphData.TexCoords.Height); + int texSize = (int)Math.Max( + MathUtils.RoundUpToPowerOfTwo((uint)(font.LineHeight * 1.5f)), + MathUtils.RoundUpToPowerOfTwo((uint)(font.LineHeight * 1.5f))); + + if (glyphTexture is not null) + { + var glyphTextureData = new Color[(int)glyphSize.X * (int)glyphSize.Y]; + glyphTexture.GetData( + level: 0, + rect: glyphData.TexCoords, + data: glyphTextureData, + startIndex: 0, + elementCount: glyphTextureData.Length); + + var texData = Enumerable.Range(0, texSize * texSize).Select(_ => color).ToArray(); + var start = (new Vector2(texSize, texSize) / 2 - glyphSize / 2).ToPoint(); + var end = start + glyphSize.ToPoint(); + + for (int x = start.X; x < end.X; x++) + { + for (int y = start.Y; y < end.Y; y++) + { + texData[x + y * texSize] = + Color.Lerp( + color, + Color.White, + glyphTextureData[(x - start.X) + (y - start.Y) * (int)glyphSize.X].A / 255f); + } + } + + tex = new Texture2D(GameMain.GraphicsDeviceManager.GraphicsDevice, texSize, texSize); + tex.SetData(texData); + } + } + + if (tex is null) + { + tex = new Texture2D(GameMain.GraphicsDeviceManager.GraphicsDevice, 2, 2); + tex.SetData(new[] { color, color, color, color }); + } + + + var sprite = new Sprite(tex, null, null); + return Task.FromResult(Option.Some(sprite)); + } + + public override async Task GetSelfUserName() + { + var epicAccountIdOption = Eos.EosAccount.SelfAccountIds.OfType().FirstOrNone(); + if (!epicAccountIdOption.TryUnwrap(out var epicAccountId)) { return ""; } + + var selfInfoResult = await EosInterface.Friends.GetSelfUserInfo(epicAccountId); + if (!selfInfoResult.TryUnwrapSuccess(out var selfInfo)) { return ""; } + + return selfInfo.DisplayName; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Social/FriendProviders/FriendProvider.cs b/Barotrauma/BarotraumaClient/ClientSource/Social/FriendProviders/FriendProvider.cs new file mode 100644 index 000000000..e3a4f4559 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Social/FriendProviders/FriendProvider.cs @@ -0,0 +1,25 @@ +#nullable enable + +using System.Collections.Immutable; +using System.Threading.Tasks; +using Barotrauma.Networking; + +namespace Barotrauma +{ + abstract class FriendProvider + { + public async Task> RetrieveFriendWithAvatar(AccountId id, int size) + { + var friendOption = await RetrieveFriend(id); + if (!friendOption.TryUnwrap(out var friend)) { return Option.None; } + + friend.Avatar = await RetrieveAvatar(friend, size); + return Option.Some(friend); + } + + public abstract Task> RetrieveFriend(AccountId id); + public abstract Task> RetrieveFriends(); + public abstract Task> RetrieveAvatar(FriendInfo friend, int avatarSize); + public abstract Task GetSelfUserName(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Social/FriendProviders/SteamFriendProvider.cs b/Barotrauma/BarotraumaClient/ClientSource/Social/FriendProviders/SteamFriendProvider.cs new file mode 100644 index 000000000..fa94b7786 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Social/FriendProviders/SteamFriendProvider.cs @@ -0,0 +1,69 @@ +#nullable enable +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Barotrauma.Networking; +using Barotrauma.Steam; +using Microsoft.Xna.Framework.Graphics; + +namespace Barotrauma +{ + sealed class SteamFriendProvider : FriendProvider + { + private FriendInfo FromSteamFriend(Steamworks.Friend steamFriend) + => new FriendInfo( + name: steamFriend.Name ?? "", + id: new SteamId(steamFriend.Id), + status: steamFriend.State switch + { + Steamworks.FriendState.Offline => FriendStatus.Offline, + Steamworks.FriendState.Invisible => FriendStatus.Offline, + _ when steamFriend.IsPlayingThisGame => FriendStatus.PlayingBarotrauma, + _ when steamFriend.GameInfo is { GameID: > 0 } => FriendStatus.PlayingAnotherGame, + _ => FriendStatus.NotPlaying + }, + serverName: steamFriend.GetRichPresence("servername") ?? "", + connectCommand: steamFriend.GetRichPresence("connect") is { } connectCmd + ? ConnectCommand.Parse(ToolBox.SplitCommand(connectCmd)) + : Option.None, + this); + + public override Task> RetrieveFriend(AccountId id) + => Task.FromResult(id is SteamId steamId + ? Option.Some(FromSteamFriend(new Steamworks.Friend(steamId.Value))) + : Option.None); + + public override Task> RetrieveFriends() + => Task.FromResult(SteamManager.IsInitialized + ? Steamworks.SteamFriends.GetFriends().Select(FromSteamFriend).ToImmutableArray() + : ImmutableArray.Empty); + + public override async Task> RetrieveAvatar(FriendInfo friend, int avatarSize) + { + if (friend.Id is not SteamId steamId) { return Option.None; } + + Func> avatarFunc = avatarSize switch + { + <= 24 => Steamworks.SteamFriends.GetSmallAvatarAsync, + <= 48 => Steamworks.SteamFriends.GetMediumAvatarAsync, + _ => Steamworks.SteamFriends.GetLargeAvatarAsync + }; + + var img = await avatarFunc(steamId.Value).ToOptionTask(); + if (!img.TryUnwrap(out var avatarImage)) { return Option.None; } + + if (friend.Avatar.TryUnwrap(out var prevAvatar)) + { + prevAvatar.Remove(); + } + + var avatarTexture = new Texture2D(GameMain.Instance.GraphicsDevice, (int)avatarImage.Width, (int)avatarImage.Height); + avatarTexture.SetData(avatarImage.Data); + return Option.Some(new Sprite(texture: avatarTexture, sourceRectangle: null, newOffset: null)); + } + + public override Task GetSelfUserName() + => Task.FromResult(SteamManager.GetUsername()); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Social/SocialExtensions.cs b/Barotrauma/BarotraumaClient/ClientSource/Social/SocialExtensions.cs new file mode 100644 index 000000000..08973aca1 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Social/SocialExtensions.cs @@ -0,0 +1,34 @@ +using Barotrauma.Networking; +using Barotrauma.Steam; + +namespace Barotrauma; + +static class SocialExtensions +{ + public static LocalizedString ViewProfileLabel(this AccountId accountId) + => accountId switch + { + SteamId => TextManager.Get("ViewSteamProfile"), + EpicAccountId => TextManager.Get("ViewEpicProfile"), + _ => "View profile of unknown origin" + }; + + public static void OpenProfile(this AccountId accountId) + { + string url = accountId switch + { + SteamId steamId => $"https://steamcommunity.com/profiles/{steamId.Value}", + EpicAccountId epicAccountId => $"https://store.epicgames.com/u/{epicAccountId.EosStringRepresentation}", + _ => "" + }; + + if (SteamManager.IsInitialized) + { + SteamManager.OverlayCustomUrl(url); + } + else + { + GameMain.ShowOpenUriPrompt(url); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Social/SocialOverlay.cs b/Barotrauma/BarotraumaClient/ClientSource/Social/SocialOverlay.cs new file mode 100644 index 000000000..76fc18ba2 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Social/SocialOverlay.cs @@ -0,0 +1,1063 @@ +#nullable enable +using Barotrauma.Eos; +using Barotrauma.Extensions; +using Barotrauma.Networking; +using Barotrauma.Steam; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; + +namespace Barotrauma; + +sealed class SocialOverlay : IDisposable +{ + public static readonly LocalizedString ShortcutBindText = TextManager.Get("SocialOverlayShortcutBind"); + + public static SocialOverlay? Instance { get; private set; } + public static void Init() + { + Instance ??= new SocialOverlay(); + } + + private sealed class NotificationHandler + { + public record Notification( + DateTime ReceiveTime, + GUIComponent GuiElement); + private readonly List notifications = new(); + + private static readonly TimeSpan notificationDuration = TimeSpan.FromSeconds(8); + private static readonly TimeSpan notificationEasingTimeSpan = TimeSpan.FromSeconds(0.5); + public readonly GUIFrame NotificationContainer = + new GUIFrame(new RectTransform((0.4f, 0.15f), GUI.Canvas, Anchor.BottomRight, scaleBasis: ScaleBasis.BothHeight), style: null) + { + CanBeFocused = false + }; + + public void Update() + { + var now = DateTime.Now; + float cumulativeNotificationOffset = 0; + + for (int i = notifications.Count - 1; i >= 0; i--) + { + var notification = notifications[i]; + + var expiryTime = notification.ReceiveTime + notificationDuration; + if (now > expiryTime + || notification.GuiElement.Parent is null) + { + RemoveNotification(notification); + continue; + } + + TimeSpan diffToStart = now - notification.ReceiveTime; + TimeSpan diffToEnd = expiryTime - now; + + float offsetToAdd = 1f; + offsetToAdd = Math.Min( + offsetToAdd, + (float)diffToStart.TotalSeconds / (float)notificationEasingTimeSpan.TotalSeconds); + offsetToAdd = Math.Min( + offsetToAdd, + (float)diffToEnd.TotalSeconds / (float)notificationEasingTimeSpan.TotalSeconds); + + offsetToAdd = Math.Max(offsetToAdd, 0f); + + cumulativeNotificationOffset += offsetToAdd; + + notification.GuiElement.RectTransform.RelativeOffset = (0, cumulativeNotificationOffset - 1f); + } + } + + public void AddToGuiUpdateList() + { + NotificationContainer.AddToGUIUpdateList(); + } + + public void AddNotification(Notification notification) + { + notifications.Add(notification); + } + + public void RemoveNotification(Notification notification) + { + notifications.Remove(notification); + NotificationContainer.RemoveChild(notification.GuiElement); + } + } + + private sealed class InviteHandler : IDisposable + { + private readonly record struct Invite( + FriendInfo Sender, + DateTime ReceiveTime, + Option NotificationOption); + + private readonly SocialOverlay socialOverlay; + private readonly FriendProvider friendProvider; + private readonly NotificationHandler notificationHandler; + + private readonly List invites = new List(); + private static readonly TimeSpan inviteDuration = TimeSpan.FromMinutes(5); + private readonly Identifier inviteReceivedEventIdentifier; + + public InviteHandler( + SocialOverlay inSocialOverlay, + FriendProvider inFriendProvider, + NotificationHandler inNotificationHandler) + { + socialOverlay = inSocialOverlay; + friendProvider = inFriendProvider; + notificationHandler = inNotificationHandler; + + inviteReceivedEventIdentifier = GetHashCode().ToIdentifier(); + EosInterface.Presence.OnInviteReceived.Register( + identifier: inviteReceivedEventIdentifier, + OnEosInviteReceived); + Steamworks.SteamFriends.OnChatMessage += OnSteamChatMsgReceived; + } + + private void OnSteamChatMsgReceived(Steamworks.Friend steamFriend, string msgType, string msgContent) + { + if (!string.Equals(msgType, "InviteGame")) { return; } + + var friendId = new SteamId(steamFriend.Id); + TaskPool.Add( + $"ReceivedInviteFrom{friendId}", + friendProvider.RetrieveFriend(friendId), + t => + { + if (!t.TryGetResult(out Option friendInfoOption)) { return; } + if (!friendInfoOption.TryUnwrap(out var friendInfo)) { return; } + RegisterInvite(friendInfo, showNotification: false); + }); + } + + private void OnEosInviteReceived(EosInterface.Presence.ReceiveInviteInfo info) + { + TaskPool.Add( + $"ReceivedInviteFrom{info.SenderId}", + friendProvider.RetrieveFriendWithAvatar(info.SenderId, notificationHandler.NotificationContainer.Rect.Height), + t => + { + if (!t.TryGetResult(out Option friendInfoOption)) { return; } + if (!friendInfoOption.TryUnwrap(out var friendInfo)) { return; } + RegisterInvite(friendInfo, showNotification: true); + }); + } + + public bool HasInviteFrom(AccountId sender) + => invites.Any(invite => invite.Sender.Id == sender); + + public void ClearInvitesFrom(AccountId sender) + { + foreach (var invite in invites) + { + if (invite.Sender.Id == sender && invite.NotificationOption.TryUnwrap(out var notification)) + { + notificationHandler.RemoveNotification(notification); + } + } + invites.RemoveAll(invite => invite.Sender.Id == sender); + + if (sender is not EpicAccountId friendEpicId) { return; } + + var selfEpicIds = EosInterface.IdQueries.GetLoggedInEpicIds(); + if (selfEpicIds.Length == 0) { return; } + + var selfEpicId = selfEpicIds[0]; + EosInterface.Presence.DeclineInvite(selfEpicId, friendEpicId); + } + + public void Update() + { + var now = DateTime.Now; + + for (int i = invites.Count - 1; i >= 0; i--) + { + var invite = invites[i]; + + var expiryTime = invite.ReceiveTime + inviteDuration; + if (now > expiryTime) + { + if (invite.NotificationOption.TryUnwrap(out var notification)) + { + notificationHandler.RemoveNotification(notification); + } + invites.RemoveAt(i); + } + } + } + + private void RegisterInvite(FriendInfo senderInfo, bool showNotification) + { + var now = DateTime.Now; + + var invite = new Invite( + Sender: senderInfo, + ReceiveTime: now, + NotificationOption: Option.None); + + if (showNotification) + { + var baseButton = new GUIButton( + new RectTransform(Vector2.One, notificationHandler.NotificationContainer.RectTransform, Anchor.BottomRight) + { + RelativeOffset = (0, -1) + }, style: "SocialOverlayPopup"); + baseButton.Frame.OutlineThickness = 1f; + + var topLayout = new GUILayoutGroup(new RectTransform(Vector2.One, baseButton.RectTransform), isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.05f + }; + + var avatarContainer = new GUIFrame(new RectTransform(Vector2.One, topLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: null); + + var avatarComponent = new GUICustomComponent( + new RectTransform( + Vector2.One * 0.8f, + avatarContainer.RectTransform, + Anchor.Center, + scaleBasis: ScaleBasis.BothHeight), + onDraw: (sb, component) => + { + if (!senderInfo.Avatar.TryUnwrap(out var avatar)) { return; } + + var rect = component.Rect; + sb.Draw(avatar.Texture, rect, avatar.Texture.Bounds, Color.White); + }); + + var textLayout = new GUILayoutGroup(new RectTransform(Vector2.One, topLayout.RectTransform)) + { + Stretch = true + }; + + void addPadding() + => new GUIFrame(new RectTransform((1.0f, 0.2f), textLayout.RectTransform), style: null); + + void addText(LocalizedString text, GUIFont font) + => new GUITextBlock(new RectTransform((1.0f, 0.2f), textLayout.RectTransform), text, font: font); + + addPadding(); + addText(senderInfo.Name, GUIStyle.SubHeadingFont); + addText(TextManager.Get("InvitedYou"), GUIStyle.Font); + addPadding(); + addText(TextManager.GetWithVariable("ClickHereOrPressSocialOverlayShortcut", "[shortcut]", ShortcutBindText), GUIStyle.SmallFont); + addPadding(); + + var notification = new NotificationHandler.Notification( + ReceiveTime: now, + GuiElement: baseButton); + baseButton.OnClicked = (_, _) => + { + socialOverlay.IsOpen = true; + notificationHandler.RemoveNotification(notification); + return false; + }; + baseButton.OnSecondaryClicked = (_, _) => + { + notificationHandler.RemoveNotification(notification); + return false; + }; + + notificationHandler.AddNotification(notification); + + invite = invite with { NotificationOption = Option.Some(notification) }; + } + + invites.Add(invite); + } + + public void Dispose() + { + EosInterface.Presence.OnInviteReceived.Deregister(inviteReceivedEventIdentifier); + Steamworks.SteamFriends.OnChatMessage -= OnSteamChatMsgReceived; + } + } + + private readonly NotificationHandler notificationHandler; + private readonly InviteHandler inviteHandler; + private readonly GUIFrame background; + private readonly GUIButton linkHint; + private readonly GUILayoutGroup contentLayout; + + private readonly GUIFrame selectedFriendInfoFrame; + + private const float WidthToHeightRatio = 7f; + + private readonly TimeSpan refreshInterval = TimeSpan.FromSeconds(30); + private DateTime lastRefreshTime; + + public bool IsOpen; + + private static RectTransform CreateRowRectT(GUIComponent parent, float heightScale = 1f) + => new RectTransform((1.0f, heightScale / WidthToHeightRatio), parent.RectTransform, scaleBasis: ScaleBasis.BothWidth); + + private static GUILayoutGroup CreateRowLayout(GUIComponent parent, float heightScale = 1f) + { + var rowLayout = new GUILayoutGroup(CreateRowRectT(parent, heightScale), isHorizontal: true) + { + Stretch = true + }; + + new GUICustomComponent(new RectTransform(Vector2.Zero, rowLayout.RectTransform), + onUpdate: (f, component) => + { + rowLayout.RectTransform.NonScaledSize = calculateSize(); + }); + + return rowLayout; + + Point calculateSize() => new Point(parent.Rect.Width, (int)((parent.Rect.Width * heightScale) / WidthToHeightRatio)); + } + + private readonly struct PlayerRow + { + public readonly GUIFrame AvatarContainer; + public readonly GUIFrame InfoContainer; + public readonly FriendInfo FriendInfo; + + internal PlayerRow(FriendInfo friendInfo, GUILayoutGroup containerLayout, bool invitedYou, IEnumerable? metadataText = null) + { + FriendInfo = friendInfo; + AvatarContainer = new GUIFrame(new RectTransform(Vector2.One, containerLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: null); + InfoContainer = new GUIFrame(new RectTransform(Vector2.One, containerLayout.RectTransform, scaleBasis: ScaleBasis.Normal), style: null); + + friendInfo.RetrieveOrInheritAvatar(Option.None, AvatarContainer.Rect.Height); + + var avatarBackground = new GUIFrame(new RectTransform(Vector2.One * 0.9f, AvatarContainer.RectTransform, Anchor.Center), + style: invitedYou + ? "FriendInvitedYou" + : $"Friend{friendInfo.CurrentStatus}"); + + var textLayout = new GUILayoutGroup(new RectTransform(Vector2.One, InfoContainer.RectTransform)) { Stretch = true }; + var textBlocks = new List(); + + addTextLayoutPadding(); + addTextBlock(friendInfo.Name, font: GUIStyle.SubHeadingFont); + metadataText ??= new[] { friendInfo.StatusText }; + foreach (var line in metadataText) + { + addTextBlock(line, font: GUIStyle.Font); + } + addTextLayoutPadding(); + + new GUICustomComponent(new RectTransform(Vector2.One, avatarBackground.RectTransform), + onUpdate: updateTextAlignments, + onDraw: drawAvatar); + + if (invitedYou) + { + var inviteIcon = new GUIImage(new RectTransform(new Vector2(0.5f), avatarBackground.RectTransform, Anchor.TopRight, Pivot.Center) + { RelativeOffset = Vector2.One * 0.15f }, style: "InviteNotification") + { + ToolTip = TextManager.Get("InviteNotification") + }; + inviteIcon.OnAddedToGUIUpdateList += (GUIComponent component) => + { + if (component.FlashTimer <= 0.0f) + { + component.Flash(GUIStyle.Green, useCircularFlash: true); + component.Pulsate(Vector2.One, Vector2.One * 1.5f, 0.5f); + } + }; + } + + void addTextLayoutPadding() + => new GUIFrame(new RectTransform(Vector2.One, textLayout.RectTransform), style: null); + + void addTextBlock(LocalizedString text, GUIFont font) + => textBlocks.Add(new GUITextBlock(new RectTransform(Vector2.One, textLayout.RectTransform), text, + textColor: Color.White, font: font, textAlignment: Alignment.CenterLeft) + { + ForceUpperCase = ForceUpperCase.No, + TextColor = avatarBackground.Color, + HoverTextColor = avatarBackground.HoverColor, + SelectedTextColor = avatarBackground.SelectedColor, + PressedColor = avatarBackground.PressedColor, + }); + + void updateTextAlignments(float deltaTime, GUICustomComponent component) + { + foreach (var textBlock in textBlocks) + { + int height = (int)textBlock.Font.LineHeight + GUI.IntScale(2); + textBlock.RectTransform.NonScaledSize = + (textBlock.RectTransform.NonScaledSize.X, height); + } + textLayout.NeedsToRecalculate = true; + } + + void drawAvatar(SpriteBatch sb, GUICustomComponent component) + { + if (!friendInfo.Avatar.TryUnwrap(out var avatar)) { return; } + Rectangle rect = component.Rect; + rect.Inflate(-GUI.IntScale(4f), -GUI.IntScale(4f)); + sb.Draw(avatar.Texture, rect, Color.White); + } + } + } + + private readonly FriendProvider friendProvider; + + private readonly GUILayoutGroup selfPlayerRowLayout; + + private readonly GUIButton? eosConfigButton; + private readonly GUILayoutGroup? eosStatusTextContainer; + private EosInterface.Core.Status eosLastKnownStatus; + + private readonly GUIListBox friendPlayerListBox; + private readonly List friendPlayerRows = new List(); + + private void RecreateSelfPlayerRow() + { + if (SteamManager.GetSteamId().TryUnwrap(out var steamId)) + { + selfPlayerRowLayout.ClearChildren(); + _ = new PlayerRow( + new FriendInfo( + name: SteamManager.GetUsername(), + id: steamId, + status: FriendStatus.PlayingBarotrauma, + serverName: "", + connectCommand: Option.None, + provider: friendProvider), + selfPlayerRowLayout, + invitedYou: false); + } + else if (EosInterface.IdQueries.IsLoggedIntoEosConnect) + { + static async Task> GetEpicAccountInfo() + { + if (!EosAccount.SelfAccountIds.OfType().FirstOrNone().TryUnwrap(out var epicAccountId)) + { + return Option.None; + } + + var selfUserInfoResult = await EosInterface.Friends.GetSelfUserInfo(epicAccountId); + + if (!selfUserInfoResult.TryUnwrapSuccess(out var selfUserInfo)) + { + return Option.None; + } + + return Option.Some(selfUserInfo); + } + + TaskPool.Add( + "GetEpicAccountIdForSelfPlayerRow", + GetEpicAccountInfo(), + t => + { + if (!t.TryGetResult(out Option userInfoOption) + || !userInfoOption.TryUnwrap(out var userInfo)) + { + return; + } + + selfPlayerRowLayout.ClearChildren(); + _ = new PlayerRow( + new FriendInfo( + name: userInfo.DisplayName, + id: userInfo.EpicAccountId, + status: FriendStatus.PlayingBarotrauma, + serverName: "", + connectCommand: Option.None, + provider: friendProvider), + selfPlayerRowLayout, + invitedYou: false); + }); + } + } + + private SocialOverlay() + { + background = + new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, GUI.Canvas, Anchor.Center), style: "SocialOverlayBackground"); + var rightSideLayout = + new GUILayoutGroup( + new RectTransform((0.9f, 1.0f), background.RectTransform, Anchor.CenterRight, + scaleBasis: ScaleBasis.BothHeight), isHorizontal: true, childAnchor: Anchor.BottomLeft); + + linkHint = new GUIButton(new RectTransform((0.5f, 0.9f / WidthToHeightRatio), rightSideLayout.RectTransform, Anchor.BottomRight, scaleBasis: ScaleBasis.BothWidth), style: "FriendsButton") + { + OnClicked = (btn, _) => + { + eosConfigButton?.Flash(GUIStyle.Green); + EosSteamPrimaryLogin.IsNewEosPlayer = false; + btn.Visible = false; + return false; + }, + Visible = false + }; + _ = new GUITextBlock(new RectTransform(Vector2.One * 0.95f, linkHint.RectTransform, Anchor.Center), + text: TextManager.Get("EosSettings.RecommendLinkingToEpicAccount"), + wrap: true, + style: "FriendsButton"); + + var content = new GUIFrame( + new RectTransform((0.5f, 1.0f), rightSideLayout.RectTransform), + style: "SocialOverlayFriendsList"); + + _ = new GUIButton( + new RectTransform(Vector2.One * 0.08f, content.RectTransform, Anchor.TopLeft, Pivot.TopRight, + scaleBasis: ScaleBasis.BothWidth) + { + RelativeOffset = (-0.03f, 0.015f) + }, + style: "SocialOverlayCloseButton") + { + OnClicked = (_, _) => + { + IsOpen = false; + return false; + } + }; + + friendProvider = new CompositeFriendProvider(new SteamFriendProvider(), new EpicFriendProvider()); + + notificationHandler = new NotificationHandler(); + inviteHandler = new InviteHandler( + inSocialOverlay: this, + inFriendProvider: friendProvider, + inNotificationHandler: notificationHandler); + + selectedFriendInfoFrame = new GUIFrame(new RectTransform((0.25f, 0.28f), background.RectTransform, + Anchor.TopRight, scaleBasis: ScaleBasis.BothHeight), style: "SocialOverlayPopup") + { + OutlineThickness = 1f, + Visible = false + }; + + contentLayout = new GUILayoutGroup(new RectTransform(Vector2.One, content.RectTransform)) { Stretch = true }; + + selfPlayerRowLayout = CreateRowLayout(contentLayout); + RecreateSelfPlayerRow(); + + friendPlayerListBox = + new GUIListBox(new RectTransform(Vector2.One, contentLayout.RectTransform), style: null) + { + OnSelected = (component, userData) => + { + if (userData is not FriendInfo friendInfo) { return false; } + selectedFriendInfoFrame.Visible = true; + selectedFriendInfoFrame.RectTransform.AbsoluteOffset = ( + X: background.Rect.Right - component.Rect.X, + Y: Math.Clamp( + value: component.Rect.Center.Y - selectedFriendInfoFrame.Rect.Height / 2, + min: 0, + max: background.Rect.Bottom - selectedFriendInfoFrame.Rect.Height)); + PopulateSelectedFriendInfoFrame(friendInfo); + return true; + } + }; + friendPlayerListBox.ScrollBar.OnMoved += (_, _) => { friendPlayerListBox.Deselect(); return true; }; + + if (SteamManager.IsInitialized) + { + var eosConfigRowLayout = CreateRowLayout(contentLayout, heightScale: 1.5f); + eosConfigRowLayout.ChildAnchor = Anchor.CenterLeft; + + eosConfigButton = new GUIButton( + new RectTransform(Vector2.One * 0.8f, eosConfigRowLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), + style: null) + { + Enabled = GameMain.NetworkMember == null, + OnClicked = (_, _) => { ShowEosSettingsMenu(); return true; } + }; + new GUIFrame(new RectTransform(Vector2.One * 0.5f, eosConfigButton.RectTransform, Anchor.Center), style: "GUIButtonSettings") + { + CanBeFocused = false + }; + + eosStatusTextContainer = new GUILayoutGroup(new RectTransform(Vector2.One, eosConfigRowLayout.RectTransform)); + RefreshEosStatusText(); + } + + RefreshFriendList(); + } + + public void DisplayBindHintToPlayer() + { + if (IsOpen) { return; } + + var baseButton = new GUIButton( + new RectTransform(Vector2.One, notificationHandler.NotificationContainer.RectTransform, Anchor.BottomRight) + { + RelativeOffset = (0, -1) + }, style: "SocialOverlayPopup"); + baseButton.Frame.OutlineThickness = 1f; + + var notification = new NotificationHandler.Notification( + ReceiveTime: DateTime.Now, + GuiElement: baseButton); + baseButton.OnClicked = (_, _) => + { + IsOpen = true; + notificationHandler.RemoveNotification(notification); + return false; + }; + baseButton.OnSecondaryClicked = (_, _) => + { + notificationHandler.RemoveNotification(notification); + return false; + }; + + _ = new GUITextBlock( + new RectTransform(Vector2.One * 0.98f, baseButton.RectTransform, Anchor.Center), + text: TextManager.GetWithVariable("SocialOverlayShortcutHint", "[shortcut]", ShortcutBindText), + textAlignment: Alignment.Center, + wrap: true) + { + CanBeFocused = false + }; + + notificationHandler.AddNotification(notification); + } + + private void ShowEosSettingsMenu() + { + bool hasEpicAccount = EosAccount.SelfAccountIds.OfType().Any(); + string manageAccountsText = hasEpicAccount + ? "EosSettings.ManageConnectedAccounts" + : "EosSettings.LinkToEpicAccount"; + + bool eosEnabled = EosInterface.Core.IsInitialized; + string enableButtonText = eosEnabled ? "EosSettings.DisableEos" : "EosSettings.EnableEos"; + + var msgBox = new GUIMessageBox(TextManager.Get("EosSettings"), string.Empty, + new LocalizedString[] + { + TextManager.Get(manageAccountsText), + TextManager.Get(enableButtonText), + TextManager.Get("EosSettings.RequestDeletion") + }, minSize: new Point(GUI.IntScale(550), 0)) + { + DrawOnTop = true + }; + msgBox.Buttons[0].Enabled = eosEnabled; + msgBox.Buttons[0].ToolTip = TextManager.Get($"{manageAccountsText}.Tooltip"); + msgBox.Buttons[1].ToolTip = TextManager.Get($"{enableButtonText}.Tooltip"); + msgBox.Buttons[2].ToolTip = TextManager.Get("EosSettings.RequestDeletion.Tooltip"); + + var closeButton = new GUIButton(new RectTransform(new Point(GUI.IntScale(35)), msgBox.InnerFrame.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point(GUI.IntScale(8)) }, + style: "SocialOverlayCloseButton") + { + OnClicked = closeMsgBox(msgBox) + }; + + msgBox.Buttons[0].OnClicked += (_, _) => + { + if (!hasEpicAccount) + { + //attempt to create an epic account and link it with the Steam account + var loadingBox = GUIMessageBox.CreateLoadingBox( + text: TextManager.Get("EosLinkSteamToEpicLoadingText"), + new[] { (TextManager.Get("Cancel"), new Action(msgBox => msgBox.Close())) }, + relativeSize: (0.35f, 0.25f)); + loadingBox.DrawOnTop = true; + TaskPool.Add( + $"LoginToEpicAccountAsSecondary", + EosEpicSecondaryLogin.LoginToLinkedEpicAccount(), + t => + { + if (t.TryGetResult(out Result? result)) + { + LocalizedString taskResultMsg; + if (result.IsSuccess) + { + taskResultMsg = TextManager.Get("EosLinkSuccess"); + } + else if (result.TryUnwrapFailure(out var failure)) + { + taskResultMsg = TextManager.GetWithVariable("EosLinkError", "[error]", failure.ToString()); + } + else + { + taskResultMsg = TextManager.GetWithVariable("EosLinkError", "[error]", result.ToString()); + } + + var msgBox = new GUIMessageBox( + TextManager.Get("EosSettings.LinkToEpicAccount"), + taskResultMsg, + new[] + { + TextManager.Get("OK"), + }) + { + DrawOnTop = true + }; + msgBox.Buttons[0].OnClicked = closeMsgBox(msgBox); + } + loadingBox.Close(); + }); + msgBox.Close(); + } + else + { + //if the user has an epic account, we can just go and link it in the browser + const string url = "https://www.epicgames.com/account/connections"; + var prompt = GameMain.ShowOpenUriPrompt(url); + prompt.DrawOnTop = true; + msgBox.Close(); + } + return true; + }; + msgBox.Buttons[1].OnClicked += (btn, obj) => + { + var crossplayChoice = eosEnabled + ? EosSteamPrimaryLogin.CrossplayChoice.Disabled + : EosSteamPrimaryLogin.CrossplayChoice.Enabled; + EosSteamPrimaryLogin.HandleCrossplayChoiceChange(crossplayChoice); + GameSettings.SetCurrentConfig(GameSettings.CurrentConfig with { CrossplayChoice = crossplayChoice }); + GameSettings.SaveCurrentConfig(); + closeMsgBox(msgBox)(btn, obj); + return true; + }; + msgBox.Buttons[2].OnClicked += (btn, obj) => + { + const string emailAddress = "contact@barotraumagame.com"; + const string subject = "Requesting account information deletion"; + string bodyText = "I would like to delete all of my account information stored by Epic Games."; + + bool epicAccountIdAvailable = EosAccount.SelfAccountIds.OfType().Any(); + bool steamIdAvailable = SteamManager.GetSteamId().TryUnwrap(out SteamId? steamId); + if (!steamIdAvailable && !epicAccountIdAvailable) + { + new GUIMessageBox(TextManager.Get("Error"), TextManager.GetWithVariable( + "EosSettings.RequestDeletion.NoAccountId", + "[emailAddress]", + emailAddress)); + return false; + } + + if (epicAccountIdAvailable) + { + bodyText += $"\n\nMy Epic Account ID(s): {string.Join(", ", EosAccount.SelfAccountIds.OfType().Select(id => id.StringRepresentation))}"; + } + if (steamIdAvailable) + { + bodyText += $"\n\nMy Steam ID: {steamId!.StringRepresentation}"; + } + + string uri = + $"mailto:{emailAddress}?" + + $"subject={Uri.EscapeDataString(subject)}" + + $"&body={Uri.EscapeDataString(bodyText)}"; + var prompt = GameMain.ShowOpenUriPrompt(uri, + TextManager.GetWithVariables("OpenLinkInEmailClient", + ("[recipient]", emailAddress), + ("[message]", bodyText))); + + if (prompt != null) + { + prompt.DrawOnTop = true; + } + + closeMsgBox(msgBox)(btn, obj); + return true; + }; + return; + + GUIButton.OnClickedHandler closeMsgBox(GUIMessageBox msgBox) + { + return (button, obj) => + { + RefreshEosStatusText(); + return msgBox.Close(button, obj); + }; + } + } + + private void PopulateSelectedFriendInfoFrame(FriendInfo friendInfo) + { + selectedFriendInfoFrame.ClearChildren(); + var layout = + new GUILayoutGroup(new RectTransform(Vector2.One * 0.9f, selectedFriendInfoFrame.RectTransform, + Anchor.Center)) + { + Stretch = true, + RelativeSpacing = 0.02f + }; + + addPadding(); + new GUITextBlock( + new RectTransform((1.0f, 0.08f), layout.RectTransform), + text: friendInfo.Name, + font: GUIStyle.SubHeadingFont, + textAlignment: Alignment.Center) + { + ForceUpperCase = ForceUpperCase.No + }; + new GUITextBlock( + new RectTransform((1.0f, 0.08f), layout.RectTransform), + text: friendInfo.StatusText, + font: GUIStyle.Font, + textAlignment: Alignment.TopCenter) + { + ForceUpperCase = ForceUpperCase.No + }; + addPadding(); + var viewProfileButton = addButton(friendInfo.Id.ViewProfileLabel()); + viewProfileButton.OnClicked = (_, _) => + { + friendInfo.Id.OpenProfile(); + return false; + }; + if (friendInfo.IsInServer && + /* don't allow joining other servers when hosting */ + GameMain.Client is not { IsServerOwner: true } && + /* can't join if already joined */ + friendInfo.ConnectCommand.TryUnwrap(out var command) && !command.IsClientConnectedToEndpoint()) + { + var joinButton = addButton(TextManager.Get("ServerListJoin")); + joinButton.OnClicked = (_, _) => + { + GameMain.Instance.ConnectCommand = friendInfo.ConnectCommand; + selectedFriendInfoFrame.Visible = false; + IsOpen = false; + return false; + }; + } + if (inviteHandler.HasInviteFrom(friendInfo.Id)) + { + var declineButton = addButton(TextManager.Get("DeclineInvite")); + declineButton.OnClicked = (_, _) => + { + inviteHandler.ClearInvitesFrom(friendInfo.Id); + selectedFriendInfoFrame.Visible = false; + return false; + }; + } + if (GameMain.Client is not null) + { + var inviteButton = addButton(TextManager.Get("InviteFriend")); + inviteButton.OnClicked = (_, _) => + { + selectedFriendInfoFrame.Visible = false; + var connectCommandOption = (GameMain.Client?.ClientPeer.ServerEndpoint) switch + { + LidgrenEndpoint lidgrenEndpoint => Option.Some(new ConnectCommand(GameMain.Client.Name, lidgrenEndpoint)), + P2PEndpoint or PipeEndpoint => Option.Some(new ConnectCommand(GameMain.Client.Name, GameMain.Client.ClientPeer.AllServerEndpoints.OfType().ToImmutableArray())), + _ => Option.None + }; + if (!connectCommandOption.TryUnwrap(out var connectCommand)) + { + DebugConsole.AddWarning($"Could not create an invite for the endpoint {GameMain.Client?.ClientPeer.ServerEndpoint}."); + return false; + } + + if (friendInfo.Id is SteamId friendSteamId && SteamManager.IsInitialized) + { + var steamFriend = new Steamworks.Friend(friendSteamId.Value); + steamFriend.InviteToGame(connectCommand.ToString()); + } + else if (friendInfo.Id is EpicAccountId friendEpicId && EosInterface.Core.IsInitialized) + { + async Task sendEpicInvite() + { + var selfEpicIds = EosInterface.IdQueries.GetLoggedInEpicIds(); + if (selfEpicIds.Length == 0) { return; } + + var selfEpicId = selfEpicIds[0]; + await EosInterface.Presence.SendInvite(selfEpicId, friendEpicId); + } + + TaskPool.Add( + $"Invite{friendEpicId}", + sendEpicInvite(), + _ => { }); + } + return false; + }; + } + addPadding(); + + void addPadding() + => new GUIFrame(new RectTransform((1.0f, 0.05f), layout.RectTransform), style: null); + + GUIButton addButton(LocalizedString label) + => new GUIButton(new RectTransform((1.0f, 0.08f), layout.RectTransform), label, style: "SocialOverlayButton"); + } + + private void RefreshEosStatusText() + { + if (eosStatusTextContainer is null) { return; } + + eosStatusTextContainer.ClearChildren(); + bool linkedToEpicAccount = EosAccount.SelfAccountIds.OfType().Any(); + _ = new GUITextBlock(new RectTransform(Vector2.One, eosStatusTextContainer.RectTransform), + textAlignment: Alignment.CenterLeft, + wrap: true, + text: TextManager.Get($"EosStatus.{EosInterface.Core.CurrentStatus}") + + "\n" + + TextManager.Get(linkedToEpicAccount + ? "EosSettings.LinkedToAccount" + : "EosSettings.NotLinkedToAccount")); + + linkHint.Visible = !linkedToEpicAccount && EosSteamPrimaryLogin.IsNewEosPlayer; + } + + public void RefreshFriendList() + { + EosAccount.RefreshSelfAccountIds(onRefreshComplete: () => + { + RefreshEosStatusText(); + lastRefreshTime = DateTime.Now; + + if (EosInterface.Core.CurrentStatus != EosInterface.Core.Status.Online + && !SteamManager.IsInitialized) + { + friendPlayerListBox.ClearChildren(); + var offlineLabel = insertLabel(TextManager.Get("SocialOverlayOffline"), heightScale: 4.0f); + offlineLabel.Wrap = true; + + return; + } + + TaskPool.Add( + "RefreshFriendList", + friendProvider.RetrieveFriends(), + t => + { + if (!t.TryGetResult(out ImmutableArray friends)) + { + return; + } + + friendPlayerListBox.ClearChildren(); + friendPlayerRows.ForEach(f => f.FriendInfo.Dispose()); + friendPlayerRows.Clear(); + + var friendsOrdered = friends + .OrderByDescending(f => f.CurrentStatus) + .ThenByDescending(f => inviteHandler.HasInviteFrom(f.Id)) + .ThenBy(f => f.Name) + .ToImmutableArray(); + bool prevWasOnline = true; + if (friendsOrdered.Length > 0 && friendsOrdered[0].IsOnline) + { + insertLabel(TextManager.Get("Label.OnlineLabel")); + } + + for (int friendIndex = 0; friendIndex < friendsOrdered.Length; friendIndex++) + { + var friend = friendsOrdered[friendIndex]; + if (prevWasOnline && !friend.IsOnline) + { + if (friendIndex > 0) + { + insertLabel(""); + } + + insertLabel(TextManager.Get("Label.OfflineLabel")); + } + + var friendFrame = new GUIFrame(CreateRowRectT(friendPlayerListBox.Content), + style: "ListBoxElement") + { + UserData = friend + }; + GUILayoutGroup newRowLayout = CreateRowLayout(friendFrame); + newRowLayout.RectTransform.RelativeSize = Vector2.One; + newRowLayout.RectTransform.ScaleBasis = ScaleBasis.Normal; + var newRow = new PlayerRow(friend, newRowLayout, + invitedYou: inviteHandler.HasInviteFrom(friend.Id)); + friendPlayerRows.Add(newRow); + + prevWasOnline = friend.IsOnline; + } + + contentLayout.Recalculate(); + friendPlayerListBox.UpdateScrollBarSize(); + }); + }); + + GUITextBlock insertLabel(LocalizedString text, float heightScale = 0.5f) + { + var labelContainer = new GUIFrame(CreateRowRectT(friendPlayerListBox.Content), style: null) + { + CanBeFocused = false + }; + Vector2 oldRelativeSize = labelContainer.RectTransform.RelativeSize; + labelContainer.RectTransform.RelativeSize + = (oldRelativeSize.X, oldRelativeSize.Y * heightScale); + return new GUITextBlock(new RectTransform(Vector2.One, labelContainer.RectTransform), + text: text, + font: GUIStyle.SubHeadingFont); + } + } + + public void AddToGuiUpdateList() + { + if (IsOpen) + { + background.AddToGUIUpdateList(); + } + notificationHandler.AddToGuiUpdateList(); + } + + public void Update() + { + inviteHandler.Update(); + notificationHandler.Update(); + + if (!IsOpen) { return; } + + if (selectedFriendInfoFrame.Visible) + { + if (PlayerInput.PrimaryMouseButtonClicked() + && selectedFriendInfoFrame.Visible + && !GUI.IsMouseOn(friendPlayerListBox) + && !GUI.IsMouseOn(selectedFriendInfoFrame)) + { + friendPlayerListBox.Deselect(); + } + + if (GUI.IsMouseOn(friendPlayerListBox) + && PlayerInput.ScrollWheelSpeed != 0) + { + friendPlayerListBox.Deselect(); + } + + if (!friendPlayerListBox.Selected) + { + selectedFriendInfoFrame.Visible = false; + } + } + + if (eosConfigButton != null) + { + bool eosConfigAccessible = GameMain.NetworkMember == null; + if (eosConfigAccessible != eosConfigButton.Enabled) + { + eosConfigButton.Enabled = eosConfigAccessible; + eosConfigButton.Children.ForEach(c => c.Enabled = eosConfigAccessible); + eosConfigButton.ToolTip = eosConfigAccessible ? string.Empty : TextManager.Get("CantAccessEOSSettingsInMP"); + } + } + + var currentEosStatus = EosInterface.Core.CurrentStatus; + if (currentEosStatus != eosLastKnownStatus) + { + eosLastKnownStatus = currentEosStatus; + RefreshEosStatusText(); + } + + if (DateTime.Now < lastRefreshTime + refreshInterval) { return; } + + RefreshFriendList(); + } + + public void Dispose() + { + inviteHandler.Dispose(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/SpamServerFilter.cs b/Barotrauma/BarotraumaClient/ClientSource/SpamServerFilter.cs index 07da313e3..c3a929ec9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/SpamServerFilter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/SpamServerFilter.cs @@ -61,7 +61,7 @@ namespace Barotrauma SpamServerFilterType.MessageEquals => CompareEquals(desc, value), SpamServerFilterType.MessageContains => CompareContains(desc, value), - SpamServerFilterType.Endpoint => info.Endpoint.StringRepresentation.Equals(value, StringComparison.OrdinalIgnoreCase), + SpamServerFilterType.Endpoint => info.Endpoints.First().StringRepresentation.Equals(value, StringComparison.OrdinalIgnoreCase), SpamServerFilterType.PlayerCountLarger => info.PlayerCount > parsedInt, SpamServerFilterType.PlayerCountExact => info.PlayerCount == parsedInt, @@ -299,7 +299,7 @@ These will hide all servers that have a discord.gg link in their name or descrip { try { - if (!t.TryGetResult(out IRestResponse remoteContentResponse)) { throw new Exception("Task did not return a valid result"); } + if (!t.TryGetResult(out IRestResponse? remoteContentResponse)) { throw new Exception("Task did not return a valid result"); } if (remoteContentResponse.StatusCode != HttpStatusCode.OK) { DebugConsole.AddWarning( diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/AuthTicket.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/AuthTicket.cs deleted file mode 100644 index 82fa24ac1..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/AuthTicket.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace Barotrauma.Steam -{ - static partial class SteamManager - { - public static Steamworks.BeginAuthResult StartAuthSession(byte[] authTicketData, ulong clientSteamID) - { - if (!IsInitialized || !Steamworks.SteamClient.IsValid) return Steamworks.BeginAuthResult.ServerNotConnectedToSteam; - - DebugConsole.Log("SteamManager authenticating Steam client " + clientSteamID); - Steamworks.BeginAuthResult startResult = Steamworks.SteamUser.BeginAuthSession(authTicketData, clientSteamID); - if (startResult != Steamworks.BeginAuthResult.OK) - { - DebugConsole.Log("Authentication failed: failed to start auth session (" + startResult.ToString() + ")"); - } - - return startResult; - } - - public static void StopAuthSession(ulong clientSteamID) - { - if (!IsInitialized || !Steamworks.SteamClient.IsValid) return; - - DebugConsole.NewMessage("SteamManager ending auth session with Steam client " + clientSteamID); - Steamworks.SteamUser.EndAuthSession(clientSteamID); - } - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs index e42bf4dd3..5d8783855 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs @@ -31,8 +31,8 @@ namespace Barotrauma.Steam t => { msgBox.Close(); - if (!t.TryGetResult(out IReadOnlyList items)) { return; } - + if (!t.TryGetResult(out IReadOnlyList? items)) { return; } + InitiateDownloads(items); }); } @@ -48,7 +48,7 @@ namespace Barotrauma.Steam t => { msgBox.Close(); - if (!t.TryGetResult(out Steamworks.Ugc.Item?[] itemsNullable)) { return; } + if (!t.TryGetResult(out Steamworks.Ugc.Item?[]? itemsNullable)) { return; } var items = itemsNullable .Where(it => it.HasValue) @@ -74,7 +74,7 @@ namespace Barotrauma.Steam .NotNone() .OfType() .Select(async id => await SteamManager.Workshop.GetItem(id.Value)))) - .Where(p => p.HasValue).Select(p => p ?? default).ToArray(); + .NotNone().ToArray(); } public static void InitiateDownloads(IReadOnlyList itemsToDownload, Action? onComplete = null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs index 90c86e709..873e8a9ec 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs @@ -1,6 +1,6 @@ using Barotrauma.Networking; using System; -using System.Globalization; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -26,6 +26,7 @@ namespace Barotrauma.Steam public static void CreateLobby(ServerSettings serverSettings) { + if (!SteamManager.IsInitialized) { return; } if (lobbyState != LobbyState.NotConnected) { return; } lobbyState = LobbyState.Creating; TaskPool.Add("CreateLobbyAsync", Steamworks.SteamMatchmaking.CreateLobbyAsync(serverSettings.MaxPlayers + 10), @@ -88,45 +89,35 @@ namespace Barotrauma.Steam return; } - var contentPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent); + serverSettings.UpdateServerListInfo(SetServerListInfo); - currentLobby?.SetData("name", serverSettings.ServerName); - currentLobby?.SetData("playercount", (GameMain.Client?.ConnectedClients?.Count ?? 0).ToString()); - currentLobby?.SetData("maxplayernum", serverSettings.MaxPlayers.ToString()); - //currentLobby?.SetData("hostipaddress", lobbyIP); - string pingLocation = Steamworks.SteamNetworkingUtils.LocalPingLocation?.ToString(); - currentLobby?.SetData("pinglocation", pingLocation ?? ""); currentLobby?.SetData("lobbyowner", GetSteamId().TryUnwrap(out var steamId) ? steamId.StringRepresentation : throw new InvalidOperationException("Steamworks not initialized")); - currentLobby?.SetData("haspassword", serverSettings.HasPassword.ToString()); - currentLobby?.SetData("message", serverSettings.ServerMessageText); - currentLobby?.SetData("version", GameMain.Version.ToString()); - - currentLobby?.SetData("contentpackage", string.Join(",", contentPackages.Select(cp => cp.Name))); - currentLobby?.SetData("contentpackagehash", string.Join(",", contentPackages.Select(cp => cp.Hash.StringRepresentation))); - currentLobby?.SetData("contentpackageid", string.Join(",", contentPackages.Select(cp - => cp.UgcId.TryUnwrap(out var ugcId) ? ugcId.StringRepresentation : ""))); - currentLobby?.SetData("modeselectionmode", serverSettings.ModeSelectionMode.ToString()); - currentLobby?.SetData("subselectionmode", serverSettings.SubSelectionMode.ToString()); - currentLobby?.SetData("voicechatenabled", serverSettings.VoiceChatEnabled.ToString()); - currentLobby?.SetData("allowspectating", serverSettings.AllowSpectating.ToString()); - currentLobby?.SetData("allowrespawn", serverSettings.AllowRespawn.ToString()); - currentLobby?.SetData("karmaenabled", serverSettings.KarmaEnabled.ToString()); - currentLobby?.SetData("friendlyfireenabled", serverSettings.AllowFriendlyFire.ToString()); - currentLobby?.SetData("traitors", serverSettings.TraitorProbability.ToString(CultureInfo.InvariantCulture)); - currentLobby?.SetData("gamestarted", GameMain.Client.GameStarted.ToString()); - currentLobby?.SetData("playstyle", serverSettings.PlayStyle.ToString()); - currentLobby?.SetData("gamemode", GameMain.NetLobbyScreen?.SelectedMode?.Identifier.Value ?? ""); - currentLobby?.SetData("language", serverSettings.Language.ToString()); - if (GameMain.NetLobbyScreen?.SelectedSub != null) + if (EosInterface.IdQueries.GetLoggedInPuids() is { Length: > 0 } puids) { - currentLobby?.SetData("submarine", GameMain.NetLobbyScreen.SelectedSub.Name); + currentLobby?.SetData("EosEndpoint", puids[0].Value); } - + DebugConsole.Log("Lobby updated!"); } + + private static void SetServerListInfo(Identifier key, object value) + { + switch (value) + { + case IEnumerable contentPackages: + currentLobby?.SetData("contentpackage", contentPackages.Select(p => p.Name).JoinEscaped(',')); + currentLobby?.SetData("contentpackagehash", contentPackages.Select(p => p.Hash.StringRepresentation).JoinEscaped(',')); + currentLobby?.SetData("contentpackageid", contentPackages + .Select(p => p.UgcId.Select(ugcId => ugcId.StringRepresentation).Fallback("")) + .JoinEscaped(',')); + return; + } + + currentLobby?.SetData(key.Value.ToLowerInvariant(), value.ToString()); + } public static void LeaveLobby() { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs index 44506e13a..c2b404b07 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs @@ -42,6 +42,9 @@ namespace Barotrauma.Steam } Steamworks.SteamNetworkingUtils.OnDebugOutput += LogSteamworksNetworking; + + // Needed to detect invites for social overlay + Steamworks.SteamFriends.ListenForFriendsMessages = true; } catch (DllNotFoundException) { @@ -145,10 +148,5 @@ namespace Barotrauma.Steam Steamworks.SteamFriends.OpenWebOverlay(url); return true; } - - public static void OverlayProfile(SteamId steamId) - { - OverlayCustomUrl($"https://steamcommunity.com/profiles/{steamId.Value}"); - } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs index dd21a0e2a..f2d9e207e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs @@ -195,7 +195,7 @@ namespace Barotrauma.Steam modProject.Save(stagingFileListPath); } - public static async Task CreateLocalCopy(ContentPackage contentPackage) + public static async Task> CreateLocalCopy(ContentPackage contentPackage) { await Task.Yield(); @@ -234,7 +234,7 @@ namespace Barotrauma.Steam RefreshLocalMods(); - return ContentPackageManager.LocalPackages.FirstOrDefault(p => p.UgcId == contentPackage.UgcId); + return ContentPackageManager.LocalPackages.FirstOrNone(p => p.UgcId == contentPackage.UgcId); } private struct InstallWaiter diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/BBCode.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/BBCode.cs index 08c6ace39..36f8baaf6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/BBCode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/BBCode.cs @@ -196,9 +196,9 @@ namespace Barotrauma.Steam SteamManager.Workshop.GetItemAsap(workshopItem.Id.Value, withLongDescription: true), t => { - if (!t.TryGetResult(out Steamworks.Ugc.Item? workshopItemWithDescription)) { return; } + if (!t.TryGetResult(out Option workshopItemWithDescription)) { return; } - bbCode = workshopItemWithDescription?.Description ?? ""; + bbCode = workshopItemWithDescription.TryUnwrap(out var item) ? (item.Description ?? "") : ""; forceReset(); }); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs index 1089221b1..2feff3989 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs @@ -21,14 +21,14 @@ namespace Barotrauma.Steam private readonly Action onInstalledInfoButtonHit; private readonly GUITextBox modsListFilter; private readonly Dictionary modsListFilterTickboxes; - private readonly GUIButton bulkUpdateButton; + private readonly Option bulkUpdateButtonOption; private GUIComponent? draggedElement = null; private GUIListBox? draggedElementOrigin = null; private void UpdateSubscribedModInstalls() { - if (!SteamManager.IsInitialized) { return; } + if (!EnableWorkshopSupport) { return; } uint numSubscribedMods = SteamManager.GetNumSubscribedItems(); if (numSubscribedMods == memSubscribedModCount) { return; } @@ -171,7 +171,7 @@ namespace Barotrauma.Steam out Action onInstalledInfoButtonHit, out GUITextBox modsListFilter, out Dictionary modsListFilterTickboxes, - out GUIButton bulkUpdateButton) + out Option bulkUpdateButton) { GUIFrame content = CreateNewContentFrame(Tab.InstalledMods); @@ -233,18 +233,22 @@ namespace Barotrauma.Steam }, ToolTip = TextManager.Get("RefreshModLists") }; - bulkUpdateButton - = new GUIButton( - new RectTransform(Vector2.One, topRightButtons.RectTransform, scaleBasis: ScaleBasis.BothHeight), - text: "", style: "GUIUpdateButton") - { - OnClicked = (b, o) => + + bulkUpdateButton = EnableWorkshopSupport + ? Option.Some( + new GUIButton( + new RectTransform(Vector2.One, topRightButtons.RectTransform, scaleBasis: ScaleBasis.BothHeight), + text: "", style: "GUIUpdateButton") { - BulkDownloader.PrepareUpdates(); - return false; - }, - Enabled = false - }; + OnClicked = (b, + o) => + { + BulkDownloader.PrepareUpdates(); + return false; + }, + Enabled = false + }) + : Option.None; padTopRight(width: 0.1f); var (left, center, right) = CreateSidebars(mainLayout, centerWidth: 0.05f, leftWidth: 0.475f, rightWidth: 0.475f, height: 0.8f); @@ -405,10 +409,13 @@ namespace Barotrauma.Steam CanBeFocused = false }; } - - addFilterTickbox(Filter.ShowLocal, "WorkshopMenu.EditButton", selected: true); - addFilterTickbox(Filter.ShowWorkshop, "WorkshopMenu.DownloadedIcon", selected: true); - addFilterTickbox(Filter.ShowPublished, "WorkshopMenu.PublishedIcon", selected: true); + + if (EnableWorkshopSupport) + { + addFilterTickbox(Filter.ShowLocal, "WorkshopMenu.EditButton", selected: true); + addFilterTickbox(Filter.ShowWorkshop, "WorkshopMenu.DownloadedIcon", selected: true); + addFilterTickbox(Filter.ShowPublished, "WorkshopMenu.PublishedIcon", selected: true); + } addFilterTickbox(Filter.ShowOnlySubs, null, selected: false); addFilterTickbox(Filter.ShowOnlyItemAssemblies, null, selected: false); @@ -487,14 +494,23 @@ namespace Barotrauma.Steam var iconBtn = guiItem.GetChild()?.GetAllChildren().Last(); bool matches = false; - matches |= modsListFilterTickboxes[Filter.ShowLocal].Selected - && ContentPackageManager.LocalPackages.Contains(p); - matches |= modsListFilterTickboxes[Filter.ShowPublished].Selected - && (ContentPackageManager.WorkshopPackages.Contains(p) - && iconBtn?.Style?.Identifier == "WorkshopMenu.PublishedIcon"); - matches |= modsListFilterTickboxes[Filter.ShowWorkshop].Selected - && (ContentPackageManager.WorkshopPackages.Contains(p) - && iconBtn?.Style?.Identifier != "WorkshopMenu.PublishedIcon"); + + if (EnableWorkshopSupport) + { + matches |= modsListFilterTickboxes[Filter.ShowLocal].Selected + && ContentPackageManager.LocalPackages.Contains(p); + + matches |= modsListFilterTickboxes[Filter.ShowPublished].Selected + && (ContentPackageManager.WorkshopPackages.Contains(p) + && iconBtn?.Style?.Identifier == "WorkshopMenu.PublishedIcon"); + matches |= modsListFilterTickboxes[Filter.ShowWorkshop].Selected + && (ContentPackageManager.WorkshopPackages.Contains(p) + && iconBtn?.Style?.Identifier != "WorkshopMenu.PublishedIcon"); + } + else + { + matches = true; + } if (modsListFilterTickboxes[Filter.ShowOnlySubs].Selected && modsListFilterTickboxes[Filter.ShowOnlyItemAssemblies].Selected @@ -524,17 +540,20 @@ namespace Barotrauma.Steam TaskPool.Add($"PrepareToShow{mod.UgcId}Info", SteamManager.Workshop.GetItem(workshopId.Value), t => { - if (!t.TryGetResult(out Steamworks.Ugc.Item? item)) { return; } - if (item is null) { return; } - onInstalledInfoButtonHit(item.Value); + if (!t.TryGetResult(out Option itemOption)) { return; } + if (!itemOption.TryUnwrap(out var item)) { return; } + onInstalledInfoButtonHit(item); }); } public void PopulateInstalledModLists(bool forceRefreshEnabled = false, bool refreshDisabled = true) { ViewingItemDetails = false; - bulkUpdateButton.Enabled = false; - bulkUpdateButton.ToolTip = ""; + if (bulkUpdateButtonOption.TryUnwrap(out var bulkUpdateButton)) + { + bulkUpdateButton.Enabled = false; + bulkUpdateButton.ToolTip = ""; + } ContentPackageManager.UpdateContentPackageList(); var corePackages = ContentPackageManager.CorePackages.ToArray(); @@ -583,7 +602,7 @@ namespace Barotrauma.Steam return false; } }; - if (!SteamManager.IsInitialized) + if (!EnableWorkshopSupport) { infoButton.Enabled = false; } @@ -599,8 +618,11 @@ namespace Barotrauma.Steam infoButton.CanBeSelected = true; infoButton.ApplyStyle(GUIStyle.ComponentStyles["WorkshopMenu.InfoButtonUpdate"]); infoButton.ToolTip = TextManager.Get("ViewModDetailsUpdateAvailable"); - bulkUpdateButton.Enabled = true; - bulkUpdateButton.ToolTip = TextManager.Get("ModUpdatesAvailable"); + if (bulkUpdateButtonOption.TryUnwrap(out var bulkUpdateButton)) + { + bulkUpdateButton.Enabled = true; + bulkUpdateButton.ToolTip = TextManager.Get("ModUpdatesAvailable"); + } }); } } @@ -705,7 +727,7 @@ namespace Barotrauma.Steam TaskPool.AddIfNotFound($"UnsubFromSelected", Task.WhenAll(workshopIds.Select(SteamManager.Workshop.GetItem)), t => { - if (!t.TryGetResult(out Steamworks.Ugc.Item?[] items)) { return; } + if (!t.TryGetResult(out Steamworks.Ugc.Item?[]? items)) { return; } items.ForEach(it => { if (!(it is { } item)) { return; } @@ -761,7 +783,7 @@ namespace Barotrauma.Steam SteamManager.Workshop.GetPublishedItems(), t => { - if (!t.TryGetResult(out ISet items)) { return; } + if (!t.TryGetResult(out ISet? items)) { return; } var ids = items.Select(it => it.Id).ToHashSet(); foreach (var child in enabledRegularModsList.Content.Children diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs index 779101b67..91038dab5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs @@ -176,6 +176,8 @@ namespace Barotrauma.Steam private void AddUnpublishedMods(ISet workshopItems) { + if (!selfModsListOption.TryUnwrap(out var selfModsList)) { return; } + //Users that don't have a proper license cannot publish Workshop items //(see https://partner.steamgames.com/doc/features/workshop#15) void clearWithMessage(LocalizedString message) @@ -347,7 +349,7 @@ namespace Barotrauma.Steam workshopItem.Subscribe(); TaskPool.Add($"DownloadSubscribedItem{workshopItem.Id}", SteamManager.Workshop.ForceRedownload(workshopItem), - t => { }); + TaskPool.IgnoredCallback); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs index b69efd9b9..8c9a12c07 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs @@ -27,7 +27,7 @@ namespace Barotrauma.Steam } public Tab CurrentTab { get; private set; } - + private readonly GUILayoutGroup tabber; private readonly Dictionary tabContents; @@ -36,11 +36,13 @@ namespace Barotrauma.Steam private CancellationTokenSource taskCancelSrc = new CancellationTokenSource(); private readonly HashSet itemThumbnails = new HashSet(); - private readonly GUIListBox popularModsList; - private readonly GUIListBox selfModsList; + private readonly Option popularModsListOption; + private readonly Option selfModsListOption; private uint memSubscribedModCount = 0; + private static bool EnableWorkshopSupport => SteamManager.IsInitialized; + public MutableWorkshopMenu(GUIFrame parent) : base(parent) { var mainLayout @@ -50,25 +52,34 @@ namespace Barotrauma.Steam AbsoluteSpacing = GUI.IntScale(4) }; - tabber = new GUILayoutGroup(new RectTransform((1.0f, 0.05f), mainLayout.RectTransform), isHorizontal: true) + Vector2 tabberSize = EnableWorkshopSupport ? (1.0f, 0.05f) : Vector2.Zero; + + tabber = new GUILayoutGroup(new RectTransform(tabberSize, mainLayout.RectTransform), isHorizontal: true) { Stretch = true }; tabContents = new Dictionary(); - new GUIButton(new RectTransform((1.0f, 0.05f), mainLayout.RectTransform, Anchor.BottomLeft), - style: "GUIButtonSmall", text: TextManager.Get("FindModsButton")) + if (EnableWorkshopSupport) { - OnClicked = (button, o) => + new GUIButton(new RectTransform((1.0f, 0.05f), mainLayout.RectTransform, Anchor.BottomLeft), + style: "GUIButtonSmall", text: TextManager.Get("FindModsButton")) { - SteamManager.OverlayCustomUrl($"https://steamcommunity.com/app/{SteamManager.AppID}/workshop/"); - return false; - } - }; + OnClicked = (button, o) => + { + SteamManager.OverlayCustomUrl($"https://steamcommunity.com/app/{SteamManager.AppID}/workshop/"); + return false; + } + }; + } + else + { + tabber.Visible = false; + } contentFrame = new GUIFrame(new RectTransform((1.0f, 0.95f), mainLayout.RectTransform), style: null); new GUICustomComponent(new RectTransform(Vector2.Zero, mainLayout.RectTransform), onUpdate: (f, component) => UpdateSubscribedModInstalls()); - + CreateInstalledModsTab( out enabledCoreDropdown, out enabledRegularModsList, @@ -76,9 +87,21 @@ namespace Barotrauma.Steam out onInstalledInfoButtonHit, out modsListFilter, out modsListFilterTickboxes, - out bulkUpdateButton); - CreatePopularModsTab(out popularModsList); - CreatePublishTab(out selfModsList); + out bulkUpdateButtonOption); + + if (EnableWorkshopSupport) + { + CreatePopularModsTab(out GUIListBox popularModList); + CreatePublishTab(out GUIListBox selfModsList); + + popularModsListOption = Option.Some(popularModList); + selfModsListOption = Option.Some(selfModsList); + } + else + { + popularModsListOption = Option.None; + selfModsListOption = Option.None; + } SelectTab(Tab.InstalledMods); } @@ -105,10 +128,10 @@ namespace Barotrauma.Steam case Tab.InstalledMods: PopulateInstalledModLists(); break; - case Tab.PopularMods: + case Tab.PopularMods when popularModsListOption.TryUnwrap(out var popularModsList): PopulateItemList(popularModsList, SteamManager.Workshop.GetPopularItems(), includeSubscribeButton: true); break; - case Tab.Publish: + case Tab.Publish when selfModsListOption.TryUnwrap(out var selfModsList): PopulateItemList(selfModsList, SteamManager.Workshop.GetPublishedItems(), includeSubscribeButton: false, onFill: AddUnpublishedMods); break; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs index b7a66a8b8..fe3bc6a7b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs @@ -88,17 +88,21 @@ namespace Barotrauma.Steam private void DeselectPublishedItem() { - var deselectCarrier = selfModsList.Parent.FindChild(c => c.UserData is ActionCarrier { Id: var id } && id == "deselect"); - Action? deselectAction = deselectCarrier.UserData is ActionCarrier { Action: var action } - ? action - : null; - deselectAction?.Invoke(); + if (selfModsListOption.TryUnwrap(out var selfModsList)) + { + var deselectCarrier = selfModsList.Parent.FindChild(c => c.UserData is ActionCarrier { Id: var id } && id == "deselect"); + Action? deselectAction = deselectCarrier.UserData is ActionCarrier { Action: var action } + ? action + : null; + deselectAction?.Invoke(); + } + SelectTab(Tab.Publish); } - + private static bool PackageMatchesItem(ContentPackage p, Steamworks.Ugc.Item workshopItem) => p.TryExtractSteamWorkshopId(out var workshopId) && workshopId.Value == workshopItem.Id; - + private void PopulatePublishTab(ItemOrPackage itemOrPackage, GUIFrame parentFrame) { ContentPackageManager.LocalPackages.Refresh(); @@ -226,9 +230,12 @@ namespace Barotrauma.Steam SteamManager.Workshop.GetItemAsap(workshopItem.Id.Value, withLongDescription: true), t => { - if (!t.TryGetResult(out Steamworks.Ugc.Item? itemWithDescription)) { return; } + if (!t.TryGetResult(out Option itemWithDescriptionOption)) { return; } - descriptionTextBox.Text = itemWithDescription?.Description ?? descriptionTextBox.Text; + descriptionTextBox.Text = + itemWithDescriptionOption.TryUnwrap(out var itemWithDescription) + ? itemWithDescription.Description ?? descriptionTextBox.Text + : descriptionTextBox.Text; descriptionTextBox.Deselect(); }); } @@ -296,7 +303,7 @@ namespace Barotrauma.Steam var fileInfoLabel = Label(rightBottom, "", GUIStyle.Font, heightScale: 1.0f); fileInfoLabel.TextAlignment = Alignment.CenterRight; - TaskPool.Add($"FileInfoLabel{workshopItem.Id}", GetModDirInfo(localPackage.Dir, fileInfoLabel), t => { }); + TaskPool.AddWithResult($"FileInfoLabel{workshopItem.Id}", GetModDirInfo(localPackage.Dir, fileInfoLabel), t => { }); GUILayoutGroup buttonLayout = new GUILayoutGroup(NewItemRectT(rightBottom), isHorizontal: true, childAnchor: Anchor.CenterRight); @@ -351,7 +358,7 @@ namespace Barotrauma.Steam buttons: new[] { TextManager.Get("Yes"), TextManager.Get("No") }); confirmDeletion.Buttons[0].OnClicked = (yesBuffer, o1) => { - TaskPool.Add($"Delete{workshopItem.Id}", Steamworks.SteamUGC.DeleteFileAsync(workshopItem.Id), + TaskPool.AddWithResult($"Delete{workshopItem.Id}", Steamworks.SteamUGC.DeleteFileAsync(workshopItem.Id), t => { SteamManager.Workshop.Uninstall(workshopItem); @@ -452,7 +459,7 @@ namespace Barotrauma.Steam } bool localCopyMade = false; - TaskPool.Add($"Create local copy {workshopItem.Title}", + TaskPool.AddWithResult($"Create local copy {workshopItem.Title}", SteamManager.Workshop.CreateLocalCopy(workshopCopy), (t) => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/StoreIntegration/StoreIntegration.cs b/Barotrauma/BarotraumaClient/ClientSource/StoreIntegration/StoreIntegration.cs new file mode 100644 index 000000000..6a381ce47 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/StoreIntegration/StoreIntegration.cs @@ -0,0 +1,70 @@ +using System; +using System.Linq; +using System.Net.Http; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; +using Barotrauma.Steam; + +namespace Barotrauma; + +static class StoreIntegration +{ + public enum Store + { + None, + Steam, + Epic + } + + public static Store CurrentStore { get; private set; } = Store.None; + + public static void Init(ref string[] programArgs) + { +#if DEBUG + if (EosInterface.Login.ParseEgsExchangeCode(programArgs).IsNone()) + { + // If the dev tool is running on port 8730 with a credential of name localdev, + // we can ask it to give us an exchange code so we can test the launcher args parsing + try + { + var devAuthToolHttp = new HttpClient(); + devAuthToolHttp.BaseAddress = new UriBuilder(scheme: "http", host: "127.0.0.1", portNumber: 8730).Uri; + var response = devAuthToolHttp.Send(new HttpRequestMessage(HttpMethod.Get, "localdev/exchange_code")); + if (response.IsSuccessStatusCode) + { + string responseContent = response.Content.ReadAsStringAsync().Result; + var match = Regex.Match(input: responseContent, + @"\s*{\s*""exchange_code""\s*:\s*""([0-9a-fA-F]+)""\s*}\s*"); + if (match.Groups.Count > 1) + { + programArgs = programArgs.Concat(new[] + { + $"-AUTH_PASSWORD={match.Groups[1].Value}", + "-AUTH_TYPE=exchangecode" + }).ToArray(); + } + } + } + catch { /* do nothing */ } + } +#endif + if (EosInterface.Login.ParseEgsExchangeCode(programArgs).IsNone() && SteamManager.SteamworksLibExists) + { + // Didn't get EGS exchange code, assume we're on Steam + // and do not initialize EOS SDK until player consent is confirmed + SteamManager.Initialize(); + CurrentStore = Store.Steam; + } + else + { + // Got an EGS exchange code or Steamworks is not present in the files, + // assume we're on EGS and initialize EOS SDK immediately. + if (EosInterface.Core.Init(EosInterface.ApplicationCredentials.Client, enableOverlay: RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + .TryUnwrapFailure(out var initError)) + { + DebugConsole.ThrowError($"EOS failed to initialize: {initError}"); + } + CurrentStore = Store.Epic; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/LimitLString.cs b/Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/LimitLString.cs index 804c688ca..2e692bb81 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/LimitLString.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/LimitLString.cs @@ -19,12 +19,14 @@ namespace Barotrauma public override bool Loaded => nestedStr.Loaded; protected override bool MustRetrieveValue() { - return base.MustRetrieveValue() || cachedFont != font.Value || cachedFont.Size != font.Size; + return base.MustRetrieveValue() || cachedFont != font.Value || cachedFont?.Size != font.Size; } public override void RetrieveValue() { - cachedValue = ToolBox.LimitString(nestedStr.Value, font.Value, maxWidth); + cachedValue = font.Value != null + ? ToolBox.LimitString(nestedStr.Value, font.Value, maxWidth) + : nestedStr.Value; cachedFont = font.Value; UpdateLanguage(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/WrappedLString.cs b/Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/WrappedLString.cs index f2cf800d2..686ef3728 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/WrappedLString.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/WrappedLString.cs @@ -19,7 +19,10 @@ namespace Barotrauma public override bool Loaded => nestedStr.Loaded; public override void RetrieveValue() { - cachedValue = ToolBox.WrapText(nestedStr.Value, lineLength, font.GetFontForStr(nestedStr.Value), textScale); + cachedValue = + font.GetFontForStr(nestedStr.Value) is ScalableFont scalableFont + ? ToolBox.WrapText(nestedStr.Value, lineLength, scalableFont, textScale) + : nestedStr.Value; UpdateLanguage(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/ConnectCommand.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/ConnectCommand.cs deleted file mode 100644 index fb86070c6..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/ConnectCommand.cs +++ /dev/null @@ -1,32 +0,0 @@ -#nullable enable -using Barotrauma.Networking; - -namespace Barotrauma -{ - readonly struct ConnectCommand - { - public readonly struct NameAndEndpoint - { - public readonly string ServerName; - public readonly Endpoint Endpoint; - - public NameAndEndpoint(string serverName, Endpoint endpoint) - { - ServerName = serverName; - Endpoint = endpoint; - } - } - - public readonly Either EndpointOrLobby; - - public ConnectCommand(string serverName, Endpoint endpoint) - { - EndpointOrLobby = new NameAndEndpoint(serverName, endpoint); - } - - public ConnectCommand(ulong lobbyId) - { - EndpointOrLobby = lobbyId; - } - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs index e04dfc070..626db0eb9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +#nullable enable +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -451,28 +452,6 @@ namespace Barotrauma public static string WrapText(string text, float lineLength, ScalableFont font, float textScale = 1.0f) => font.WrapText(text, lineLength / textScale); - public static Option ParseConnectCommand(string[] args) - { - if (args == null || args.Length < 2) { return Option.None(); } - - if (args[0].Equals("-connect", StringComparison.OrdinalIgnoreCase)) - { - if (args.Length < 3) { return Option.None(); } - if (!(Endpoint.Parse(args[2]).TryUnwrap(out var endpoint))) { return Option.None(); } - return Option.Some( - new ConnectCommand( - serverName: args[1], - endpoint: endpoint)); - } - else if (args[0].Equals("+connect_lobby", StringComparison.OrdinalIgnoreCase)) - { - return UInt64.TryParse(args[1], out var lobbyId) - ? Option.Some(new ConnectCommand(lobbyId)) - : Option.None(); - } - return Option.None(); - } - public static bool VersionNewerIgnoreRevision(Version a, Version b) { if (b.Major > a.Major) { return true; } diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 3f7a3f89c..64c52d9e9 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.2.8.0 + 1.3.0.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma @@ -17,38 +17,38 @@ - DEBUG;TRACE;CLIENT;LINUX;USE_STEAM + DEBUG;TRACE;CLIENT;LINUX x64 ..\bin\$(Configuration)Linux\ - TRACE;DEBUG;CLIENT;LINUX;X64;USE_STEAM + TRACE;DEBUG;CLIENT;LINUX;X64 x64 ..\bin\$(Configuration)Linux\ - TRACE;CLIENT;LINUX;USE_STEAM + TRACE;CLIENT;LINUX x64 ..\bin\$(Configuration)Linux\ - TRACE;CLIENT;LINUX;USE_STEAM;UNSTABLE + TRACE;CLIENT;LINUX;UNSTABLE x64 ..\bin\$(Configuration)Linux\ true - TRACE;CLIENT;LINUX;X64;USE_STEAM + TRACE;CLIENT;LINUX;X64 x64 ..\bin\$(Configuration)Linux\ - TRACE;CLIENT;LINUX;X64;USE_STEAM;UNSTABLE + TRACE;CLIENT;LINUX;X64;UNSTABLE x64 ..\bin\$(Configuration)Linux\ true @@ -62,7 +62,6 @@ - @@ -136,6 +135,11 @@ + + + + + @@ -201,8 +205,10 @@ - linux-x64 + linux-x64 + Linux + \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index df3355c96..8effce6b9 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.2.8.0 + 1.3.0.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma @@ -17,26 +17,26 @@ - TRACE;CLIENT;OSX;USE_STEAM;DEBUG;NETCOREAPP;NETCOREAPP3_0 + TRACE;CLIENT;OSX;DEBUG;NETCOREAPP;NETCOREAPP3_0 x64 ..\bin\$(Configuration)Mac - TRACE;DEBUG;CLIENT;OSX;X64;USE_STEAM + TRACE;DEBUG;CLIENT;OSX;X64 x64 ..\bin\$(Configuration)Mac\ - TRACE;CLIENT;OSX;USE_STEAM;RELEASE;NETCOREAPP;NETCOREAPP3_0 + TRACE;CLIENT;OSX;RELEASE;NETCOREAPP;NETCOREAPP3_0 x64 ..\bin\$(Configuration)Mac - TRACE;CLIENT;OSX;USE_STEAM;RELEASE;NETCOREAPP;NETCOREAPP3_0;UNSTABLE + TRACE;CLIENT;OSX;RELEASE;NETCOREAPP;NETCOREAPP3_0;UNSTABLE x64 ..\bin\$(Configuration)Mac @@ -44,13 +44,13 @@ - TRACE;CLIENT;OSX;X64;USE_STEAM + TRACE;CLIENT;OSX;X64 x64 ..\bin\$(Configuration)Mac\ - TRACE;CLIENT;OSX;X64;USE_STEAM;UNSTABLE + TRACE;CLIENT;OSX;X64;UNSTABLE x64 ..\bin\$(Configuration)Mac\ true @@ -64,7 +64,6 @@ - @@ -140,6 +139,10 @@ PreserveNewest + + + + @@ -206,8 +209,10 @@ - osx-x64 + osx-x64 + MacOS + \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 4cd3790e1..3574cc843 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.2.8.0 + 1.3.0.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma @@ -18,13 +18,13 @@ - DEBUG;TRACE;CLIENT;WINDOWS;USE_STEAM + DEBUG;TRACE;CLIENT;WINDOWS x64 ..\bin\$(Configuration)Windows\ - TRACE;DEBUG;CLIENT;WINDOWS;X64;USE_STEAM + TRACE;DEBUG;CLIENT;WINDOWS;X64 x64 ..\bin\$(Configuration)Windows\ full @@ -33,20 +33,20 @@ - TRACE;CLIENT;WINDOWS;USE_STEAM + TRACE;CLIENT;WINDOWS x64 ..\bin\$(Configuration)Windows\ - TRACE;CLIENT;WINDOWS;USE_STEAM + TRACE;CLIENT;WINDOWS x64 ..\bin\$(Configuration)Windows\ true - TRACE;CLIENT;WINDOWS;X64;USE_STEAM + TRACE;CLIENT;WINDOWS;X64 x64 ..\bin\$(Configuration)Windows\ full @@ -54,7 +54,7 @@ - TRACE;CLIENT;WINDOWS;X64;USE_STEAM + TRACE;CLIENT;WINDOWS;X64 x64 ..\bin\$(Configuration)Windows\ full @@ -70,7 +70,6 @@ - @@ -168,6 +167,11 @@ + + + + + @@ -233,8 +237,10 @@ - win-x64 + win-x64 + Win64 + \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 4f1bb7e53..20678b562 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.2.8.0 + 1.3.0.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer @@ -17,38 +17,38 @@ - DEBUG;TRACE;SERVER;LINUX;USE_STEAM + DEBUG;TRACE;SERVER;LINUX x64 ..\bin\$(Configuration)Linux\ - TRACE;DEBUG;SERVER;LINUX;X64;USE_STEAM + TRACE;DEBUG;SERVER;LINUX;X64 x64 ..\bin\$(Configuration)Linux\ - TRACE;SERVER;LINUX;USE_STEAM + TRACE;SERVER;LINUX x64 ..\bin\$(Configuration)Linux\ - TRACE;SERVER;LINUX;USE_STEAM + TRACE;SERVER;LINUX x64 ..\bin\$(Configuration)Linux\ true - TRACE;SERVER;LINUX;X64;USE_STEAM + TRACE;SERVER;LINUX;X64 x64 ..\bin\$(Configuration)Linux\ - TRACE;SERVER;LINUX;X64;USE_STEAM + TRACE;SERVER;LINUX;X64 x64 ..\bin\$(Configuration)Linux\ true @@ -56,7 +56,6 @@ - @@ -80,6 +79,11 @@ + + + + + @@ -145,4 +149,10 @@ + + linux-x64 + Linux + + + diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 43e565bd8..4d80f2da0 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.2.8.0 + 1.3.0.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer @@ -17,7 +17,7 @@ - TRACE;SERVER;OSX;USE_STEAM;DEBUG;NETCOREAPP;NETCOREAPP3_0 + TRACE;SERVER;OSX;DEBUG;NETCOREAPP;NETCOREAPP3_0 x64 ..\bin\DebugMac true @@ -25,20 +25,20 @@ - TRACE;DEBUG;SERVER;OSX;X64;USE_STEAM + TRACE;DEBUG;SERVER;OSX;X64 x64 ..\bin\$(Configuration)Mac\ - TRACE;SERVER;OSX;USE_STEAM;RELEASE;NETCOREAPP;NETCOREAPP3_0 + TRACE;SERVER;OSX;RELEASE;NETCOREAPP;NETCOREAPP3_0 x64 ..\bin\ReleaseMac - TRACE;SERVER;OSX;USE_STEAM;RELEASE;NETCOREAPP;NETCOREAPP3_0;UNSTABLE + TRACE;SERVER;OSX;RELEASE;NETCOREAPP;NETCOREAPP3_0;UNSTABLE x64 ..\bin\ReleaseMac @@ -46,13 +46,13 @@ - TRACE;SERVER;OSX;X64;USE_STEAM + TRACE;SERVER;OSX;X64 x64 ..\bin\$(Configuration)Mac\ - TRACE;SERVER;OSX;X64;USE_STEAM;UNSTABLE + TRACE;SERVER;OSX;X64;UNSTABLE x64 ..\bin\$(Configuration)Mac\ true @@ -60,7 +60,6 @@ - @@ -86,7 +85,11 @@ - + + + + + @@ -151,4 +154,10 @@ + + osx-x64 + MacOS + + + diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs index fc368dace..4002d6ebe 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs @@ -72,7 +72,7 @@ namespace Barotrauma msg.WriteString(ragdollFileName); msg.WriteIdentifier(HumanPrefabIds.NpcIdentifier); msg.WriteIdentifier(MinReputationToHire.factionId); - if (MinReputationToHire.factionId != default) + if (!MinReputationToHire.factionId.IsEmpty) { msg.WriteSingle(MinReputationToHire.reputation); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index aebcc26a5..21758979c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -37,10 +37,8 @@ namespace Barotrauma NewMessage("Client \"" + client.Name + "\" attempted to use the command \"" + Names[0] + "\". Cheats must be enabled using \"enablecheats\" before the command can be used.", Color.Red); GameMain.Server.SendConsoleMessage("You need to enable cheats using the command \"enablecheats\" before you can use the command \"" + Names[0] + "\".", client, Color.Red); -#if USE_STEAM NewMessage("Enabling cheats will disable Steam achievements during this play session.", Color.Red); GameMain.Server.SendConsoleMessage("Enabling cheats will disable Steam achievements during this play session.", client, Color.Red); -#endif return; } @@ -1062,21 +1060,17 @@ namespace Barotrauma commands.Add(new Command("enablecheats", "enablecheats: Enables cheat commands and disables Steam achievements during this play session.", (string[] args) => { CheatsEnabled = true; - SteamAchievementManager.CheatsEnabled = true; + AchievementManager.CheatsEnabled = true; NewMessage("Enabled cheat commands.", Color.Red); -#if USE_STEAM NewMessage("Steam achievements have been disabled during this play session.", Color.Red); -#endif GameMain.Server?.UpdateCheatsEnabled(); })); AssignOnClientRequestExecute("enablecheats", (client, cursorPos, args) => { CheatsEnabled = true; - SteamAchievementManager.CheatsEnabled = true; + AchievementManager.CheatsEnabled = true; NewMessage("Cheat commands have been enabled by \"" + client.Name + "\".", Color.Red); -#if USE_STEAM NewMessage("Steam achievements have been disabled during this play session.", Color.Red); -#endif GameMain.Server?.UpdateCheatsEnabled(); }); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventLogAction.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventLogAction.cs index 86d0297f6..b6dac422e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventLogAction.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventLogAction.cs @@ -20,7 +20,7 @@ partial class EventLogAction : EventAction if (target is Character character) { var ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character); - if (ownerClient != null && eventLog != null) + if (ownerClient != null) { targetClients.Add(ownerClient); } @@ -38,7 +38,7 @@ partial class EventLogAction : EventAction } else { - if (eventLog != null && eventLog.TryAddEntry(ParentEvent.Prefab.Identifier, Id, displayText, GameMain.Server.ConnectedClients) && ShowInServerLog) + if (eventLog.TryAddEntry(ParentEvent.Prefab.Identifier, Id, displayText, GameMain.Server.ConnectedClients) && ShowInServerLog) { Log(targetClients: null); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index 1be65ac9c..c62784177 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -84,8 +84,21 @@ namespace Barotrauma Console.WriteLine("Loading game settings"); GameSettings.Init(); - Console.WriteLine("Initializing SteamManager"); - SteamManager.Initialize(); + //no owner key = dedicated server + if (!CommandLineArgs.Any(a => a.Trim().Equals("-ownerkey", StringComparison.OrdinalIgnoreCase))) + { + Console.WriteLine("Initializing SteamManager"); + SteamManager.Initialize(); + + if (!SteamManager.SteamworksLibExists) + { + Console.WriteLine("Initializing EosManager"); + if (EosInterface.Core.Init(EosInterface.ApplicationCredentials.Server, enableOverlay: false).TryUnwrapFailure(out var initError)) + { + Console.WriteLine($"EOS failed to initialize: {initError}"); + } + } + } //TODO: figure out how consent is supposed to work for servers //Console.WriteLine("Initializing GameAnalytics"); @@ -137,8 +150,8 @@ namespace Barotrauma bool enableUpnp = false; int maxPlayers = 10; - Option ownerKey = Option.None(); - Option steamId = Option.None(); + Option ownerKey = Option.None; + Option ownerEndpoint = Option.None; XDocument doc = XMLExtensions.TryLoadXml(ServerSettings.SettingsFile); if (doc?.Root == null) @@ -207,8 +220,8 @@ namespace Barotrauma } i++; break; - case "-steamid": - steamId = SteamId.Parse(CommandLineArgs[i + 1]); + case "-endpoint": + ownerEndpoint = P2PEndpoint.Parse(CommandLineArgs[i + 1]); i++; break; case "-pipes": @@ -227,7 +240,7 @@ namespace Barotrauma enableUpnp, maxPlayers, ownerKey, - steamId); + ownerEndpoint); Server.StartServer(); for (int i = 0; i < CommandLineArgs.Length; i++) @@ -323,6 +336,7 @@ namespace Barotrauma Server.Update((float)Timing.Step); if (Server == null) { break; } SteamManager.Update((float)Timing.Step); + EosInterface.Core.Update(); TaskPool.Update(); CoroutineManager.Update(paused: false, (float)Timing.Step); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs index 8742d17eb..5ffb59bc7 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs @@ -174,6 +174,22 @@ namespace Barotrauma.Networking return bannedPlayer != null; } + public bool IsBanned(AccountInfo accountInfo, out string reason) + { + if (accountInfo.AccountId.TryUnwrap(out var accountId) && IsBanned(accountId, out reason)) + { + return true; + } + + foreach (var otherId in accountInfo.OtherMatchingIds) + { + if (IsBanned(otherId, out reason)) { return true; } + } + + reason = ""; + return false; + } + public void BanPlayer(string name, Endpoint endpoint, string reason, TimeSpan? duration) => BanPlayer(name, endpoint.Address, reason, duration); @@ -305,7 +321,7 @@ namespace Barotrauma.Networking else { outMsg.WriteBoolean(false); outMsg.WritePadBits(); - outMsg.WriteString(((SteamId)bannedPlayer.AddressOrAccountId).StringRepresentation); + outMsg.WriteString(((AccountId)bannedPlayer.AddressOrAccountId).StringRepresentation); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index cede18f2d..af254550f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -52,7 +52,7 @@ namespace Barotrauma.Networking private DateTime refreshMasterTimer; private readonly TimeSpan refreshMasterInterval = new TimeSpan(0, 0, 60); - private bool registeredToMaster; + private bool registeredToSteamMaster; private DateTime roundStartTime; @@ -121,7 +121,7 @@ namespace Barotrauma.Networking public NetworkConnection OwnerConnection { get; private set; } private readonly Option ownerKey; - private readonly Option ownerSteamId; + private readonly Option ownerEndpoint; public GameServer( string name, @@ -132,7 +132,7 @@ namespace Barotrauma.Networking bool attemptUPnP, int maxPlayers, Option ownerKey, - Option ownerSteamId) + Option ownerEndpoint) { if (name.Length > NetConfig.ServerNameMaxLength) { @@ -150,7 +150,7 @@ namespace Barotrauma.Networking this.ownerKey = ownerKey; - this.ownerSteamId = ownerSteamId; + this.ownerEndpoint = ownerEndpoint; entityEventManager = new ServerEntityEventManager(this); } @@ -165,16 +165,18 @@ namespace Barotrauma.Networking OnInitializationComplete, GameMain.Instance.CloseServer, OnOwnerDetermined); - - if (ownerSteamId.TryUnwrap(out var steamId)) + + if (ownerEndpoint.TryUnwrap(out var endpoint)) { - Log("Using SteamP2P networking.", ServerLog.MessageType.ServerMessage); - serverPeer = new SteamP2PServerPeer(steamId, ownerKey.Fallback(0), ServerSettings, callbacks); + Log("Using P2P networking.", ServerLog.MessageType.ServerMessage); + serverPeer = new P2PServerPeer(endpoint, ownerKey.Fallback(0), ServerSettings, callbacks); } else { - Log("Using Lidgren networking. Manual port forwarding may be required. If players cannot connect to the server, you may want to use the in-game hosting menu (which uses SteamP2P networking and does not require port forwarding).", ServerLog.MessageType.ServerMessage); + Log("Using Lidgren networking. Manual port forwarding may be required. If players cannot connect to the server, you may want to use the in-game hosting menu (which uses Steamworks and EOS networking and does not require port forwarding).", ServerLog.MessageType.ServerMessage); serverPeer = new LidgrenServerPeer(ownerKey, ServerSettings, callbacks); + registeredToSteamMaster = SteamManager.CreateServer(this, ServerSettings.IsPublic); + Eos.EosSessionManager.UpdateOwnedSession(Option.None, ServerSettings); } FileSender = new FileSender(serverPeer, MsgConstants.MTU); @@ -187,13 +189,6 @@ namespace Barotrauma.Networking VoipServer = new VoipServer(serverPeer); - if (serverPeer is LidgrenServerPeer) - { -#if USE_STEAM - registeredToMaster = SteamManager.CreateServer(this, ServerSettings.IsPublic); -#endif - } - Log("Server started", ServerLog.MessageType.ServerMessage); GameMain.NetLobbyScreen.Select(); @@ -648,20 +643,23 @@ namespace Barotrauma.Networking updateTimer = DateTime.Now + UpdateInterval; } - if (registeredToMaster && (DateTime.Now > refreshMasterTimer || ServerSettings.ServerDetailsChanged)) + if (DateTime.Now > refreshMasterTimer || ServerSettings.ServerDetailsChanged) { - if (GameSettings.CurrentConfig.UseSteamMatchmaking) + if (registeredToSteamMaster) { bool refreshSuccessful = SteamManager.RefreshServerDetails(this); if (GameSettings.CurrentConfig.VerboseLogging) { Log(refreshSuccessful ? - "Refreshed server info on the server list." : - "Refreshing server info on the server list failed.", ServerLog.MessageType.ServerMessage); + "Refreshed server info on the Steam server list." : + "Refreshing server info on the Steam server list failed.", ServerLog.MessageType.ServerMessage); } } - refreshMasterTimer = DateTime.Now + refreshMasterInterval; + + Eos.EosSessionManager.UpdateOwnedSession(Option.None, ServerSettings); + ServerSettings.ServerDetailsChanged = false; + refreshMasterTimer = DateTime.Now + refreshMasterInterval; } } @@ -3031,8 +3029,6 @@ namespace Barotrauma.Networking client.WaitForNextRoundRespawn = null; client.InGame = false; - if (client.AccountId.TryUnwrap(out var steamId)) { SteamManager.StopAuthSession(steamId); } - var previousPlayer = previousPlayers.Find(p => p.MatchesClient(client)); if (previousPlayer == null) { @@ -3356,7 +3352,7 @@ namespace Barotrauma.Networking msg.WriteByte((byte)ServerPacketHeader.FILE_TRANSFER); msg.WriteByte((byte)FileTransferMessageType.Cancel); msg.WriteByte((byte)transfer.ID); - serverPeer.Send(msg, transfer.Connection, DeliveryMethod.ReliableOrdered); + serverPeer.Send(msg, transfer.Connection, DeliveryMethod.Reliable); } public void UpdateVoteStatus(bool checkActiveVote = true) @@ -3542,13 +3538,13 @@ namespace Barotrauma.Networking } } - public void IncrementStat(Character character, Identifier achievementIdentifier, int amount) + public void IncrementStat(Character character, AchievementStat stat, int amount) { foreach (Client client in connectedClients) { if (client.Character == character) { - IncrementStat(client, achievementIdentifier, amount); + IncrementStat(client, stat, amount); return; } } @@ -3562,19 +3558,17 @@ namespace Barotrauma.Networking IWriteMessage msg = new WriteOnlyMessage(); msg.WriteByte((byte)ServerPacketHeader.ACHIEVEMENT); msg.WriteIdentifier(achievementIdentifier); - msg.WriteInt32(0); serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } - public void IncrementStat(Client client, Identifier achievementIdentifier, int amount) + public void IncrementStat(Client client, AchievementStat stat, int amount) { - if (client.GivenAchievements.Contains(achievementIdentifier)) { return; } - IWriteMessage msg = new WriteOnlyMessage(); - msg.WriteByte((byte)ServerPacketHeader.ACHIEVEMENT); - msg.WriteIdentifier(achievementIdentifier); - msg.WriteInt32(amount); + msg.WriteByte((byte)ServerPacketHeader.ACHIEVEMENT_STAT); + + INetSerializableStruct incrementedStat = new NetIncrementedStat(stat, amount); + incrementedStat.Write(msg); serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } @@ -3582,7 +3576,7 @@ namespace Barotrauma.Networking public void SendTraitorMessage(WriteOnlyMessage msg, Client client) { if (client == null) { return; }; - serverPeer.Send(msg, client.Connection, DeliveryMethod.ReliableOrdered); + serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } public void UpdateCheatsEnabled() diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs index eaaf5e1dc..1f5c680b0 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs @@ -1,24 +1,26 @@ #nullable enable using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Net; using System.Linq; +using Barotrauma.Extensions; using Barotrauma.Steam; using Lidgren.Network; namespace Barotrauma.Networking { - internal sealed class LidgrenServerPeer : ServerPeer + internal sealed class LidgrenServerPeer : ServerPeer { private readonly NetPeerConfiguration netPeerConfiguration; + private ImmutableDictionary? authenticators; private NetServer? netServer; private readonly List incomingLidgrenMessages; - public LidgrenServerPeer(Option ownKey, ServerSettings settings, Callbacks callbacks) : base(callbacks) + public LidgrenServerPeer(Option ownKey, ServerSettings settings, Callbacks callbacks) : base(callbacks, settings) { - serverSettings = settings; - + authenticators = null; netServer = null; netPeerConfiguration = new NetPeerConfiguration("barotrauma") @@ -41,9 +43,6 @@ namespace Barotrauma.Networking netPeerConfiguration.EnableMessageType(NetIncomingMessageType.ConnectionApproval); - connectedClients = new List(); - pendingClients = new List(); - incomingLidgrenMessages = new List(); ownerKey = ownKey; @@ -53,6 +52,8 @@ namespace Barotrauma.Networking { if (netServer != null) { return; } + authenticators = Authenticator.GetAuthenticatorsForHost(Option.None); + incomingLidgrenMessages.Clear(); netServer = new NetServer(netPeerConfiguration); @@ -80,7 +81,7 @@ namespace Barotrauma.Networking for (int i = connectedClients.Count - 1; i >= 0; i--) { - Disconnect(connectedClients[i], PeerDisconnectPacket.WithReason(DisconnectReason.ServerShutdown)); + Disconnect(connectedClients[i].Connection, PeerDisconnectPacket.WithReason(DisconnectReason.ServerShutdown)); } netServer.Shutdown(PeerDisconnectPacket.WithReason(DisconnectReason.ServerShutdown).ToLidgrenStringRepresentation()); @@ -90,8 +91,6 @@ namespace Barotrauma.Networking netServer = null; - Steamworks.SteamServer.OnValidateAuthTicketResponse -= OnAuthChange; - callbacks.OnShutdown.Invoke(); } @@ -165,9 +164,10 @@ namespace Barotrauma.Networking ToolBox.ThrowIfNull(netPeerConfiguration); netServer.UPnP.ForwardPort(netPeerConfiguration.Port, "barotrauma"); -#if USE_STEAM - netServer.UPnP.ForwardPort(serverSettings.QueryPort, "barotrauma"); -#endif + if (SteamManager.IsInitialized) + { + netServer.UPnP.ForwardPort(serverSettings.QueryPort, "barotrauma"); + } } private bool DiscoveringUPnP() @@ -199,7 +199,7 @@ namespace Barotrauma.Networking return; } - PendingClient? pendingClient = pendingClients.Find(c => c.Connection is LidgrenConnection l && l.NetConnection == inc.SenderConnection); + PendingClient? pendingClient = pendingClients.Find(c => c.Connection.NetConnection == inc.SenderConnection); if (pendingClient is null) { @@ -214,7 +214,7 @@ namespace Barotrauma.Networking { if (netServer == null) { return; } - PendingClient? pendingClient = pendingClients.Find(c => c.Connection is LidgrenConnection l && l.NetConnection == lidgrenMsg.SenderConnection); + PendingClient? pendingClient = pendingClients.Find(c => c.Connection.NetConnection == lidgrenMsg.SenderConnection); IReadMessage inc = lidgrenMsg.ToReadMessage(); @@ -226,7 +226,7 @@ namespace Barotrauma.Networking } else if (!packetHeader.IsConnectionInitializationStep()) { - if (connectedClients.Find(c => c is LidgrenConnection l && l.NetConnection == lidgrenMsg.SenderConnection) is not LidgrenConnection conn) + if (connectedClients.Find(c => c.Connection.NetConnection == lidgrenMsg.SenderConnection) is not { Connection: LidgrenConnection conn }) { if (pendingClient != null) { @@ -263,7 +263,7 @@ namespace Barotrauma.Networking switch (inc.SenderConnection.Status) { case NetConnectionStatus.Disconnected: - LidgrenConnection? conn = connectedClients.Cast().FirstOrDefault(c => c.NetConnection == inc.SenderConnection); + LidgrenConnection? conn = connectedClients.Select(c => c.Connection).FirstOrDefault(c => c.NetConnection == inc.SenderConnection); if (conn != null) { if (conn == OwnerConnection) @@ -290,12 +290,7 @@ namespace Barotrauma.Networking } } - public override void InitializeSteamServerCallbacks() - { - Steamworks.SteamServer.OnValidateAuthTicketResponse += OnAuthChange; - } - - private void OnAuthChange(Steamworks.SteamId steamId, Steamworks.SteamId ownerId, Steamworks.AuthResponse status) + private void OnSteamAuthChange(Steamworks.SteamId steamId, Steamworks.SteamId ownerId, Steamworks.AuthResponse status) { if (netServer == null) { return; } @@ -307,8 +302,8 @@ namespace Barotrauma.Networking if (status == Steamworks.AuthResponse.OK) { return; } if (connectedClients.Find(c - => c.AccountInfo.AccountId.TryUnwrap(out var id) && id.Value == steamId) - is LidgrenConnection connection) + => c.Connection.AccountInfo.AccountId.TryUnwrap(out var id) && id.Value == steamId) + is { Connection: LidgrenConnection connection }) { Disconnect(connection, PeerDisconnectPacket.SteamAuthError(status)); } @@ -341,9 +336,15 @@ namespace Barotrauma.Networking { if (netServer == null) { return; } - if (!connectedClients.Contains(conn)) + if (conn is not LidgrenConnection lidgrenConnection) { - DebugConsole.ThrowError($"Tried to send message to unauthenticated connection: {conn.Endpoint.StringRepresentation}"); + DebugConsole.ThrowError($"Tried to send message to connection of incorrect type: expected {nameof(LidgrenConnection)}, got {conn.GetType().Name}"); + return; + } + + if (!connectedClients.Any(cc => cc.Connection == lidgrenConnection)) + { + DebugConsole.ThrowError($"Tried to send message to unauthenticated connection: {lidgrenConnection.Endpoint.StringRepresentation}"); return; } @@ -367,7 +368,7 @@ namespace Barotrauma.Networking { Buffer = bufAux }; - SendMsgInternal(conn, headers, body); + SendMsgInternal(lidgrenConnection, headers, body); } public override void Disconnect(NetworkConnection conn, PeerDisconnectPacket peerDisconnectPacket) @@ -376,18 +377,21 @@ namespace Barotrauma.Networking if (conn is not LidgrenConnection lidgrenConn) { return; } - if (connectedClients.Contains(lidgrenConn)) + if (connectedClients.FindIndex(cc => cc.Connection == lidgrenConn) is >= 0 and var ccIndex) { lidgrenConn.Status = NetworkConnectionStatus.Disconnected; - connectedClients.Remove(lidgrenConn); + connectedClients.RemoveAt(ccIndex); callbacks.OnDisconnect.Invoke(conn, peerDisconnectPacket); - if (conn.AccountInfo.AccountId.TryUnwrap(out var steamId)) { SteamManager.StopAuthSession(steamId); } + if (conn.AccountInfo.AccountId.TryUnwrap(out var accountId)) + { + authenticators?.Values.ForEach(authenticator => authenticator.EndAuthSession(accountId)); + } } lidgrenConn.NetConnection.Disconnect(peerDisconnectPacket.ToLidgrenStringRepresentation()); } - protected override void SendMsgInternal(NetworkConnection conn, PeerPacketHeaders headers, INetSerializableStruct? body) + protected override void SendMsgInternal(LidgrenConnection conn, PeerPacketHeaders headers, INetSerializableStruct? body) { IWriteMessage msgToSend = new WriteOnlyMessage(); msgToSend.WriteNetSerializableStruct(headers); @@ -402,75 +406,74 @@ namespace Barotrauma.Networking protected override void CheckOwnership(PendingClient pendingClient) { - if (OwnerConnection == null - && pendingClient.Connection is LidgrenConnection l - && IPAddress.IsLoopback(l.NetConnection.RemoteEndPoint.Address) - && ownerKey.IsSome() && pendingClient.OwnerKey == ownerKey) + if (OwnerConnection != null + || pendingClient.Connection is not LidgrenConnection l + || !IPAddress.IsLoopback(l.NetConnection.RemoteEndPoint.Address) + || !ownerKey.IsSome() || pendingClient.OwnerKey != ownerKey) { - ownerKey = Option.None(); - OwnerConnection = pendingClient.Connection; - callbacks.OnOwnerDetermined.Invoke(OwnerConnection); + return; } + + ownerKey = Option.None; + OwnerConnection = pendingClient.Connection; + callbacks.OnOwnerDetermined.Invoke(OwnerConnection); } - protected override void ProcessAuthTicket(ClientSteamTicketAndVersionPacket packet, PendingClient pendingClient) + private enum AuthResult { - if (pendingClient.AccountInfo.AccountId.IsNone()) + Success, + Failure + } + + protected override void ProcessAuthTicket(ClientAuthTicketAndVersionPacket packet, PendingClient pendingClient) + { + if (pendingClient.AccountInfo.AccountId.IsSome()) + { + if (pendingClient.AccountInfo.AccountId != packet.AccountId) + { + RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.AuthenticationFailed)); + } + return; + } + + void acceptClient(AccountInfo accountInfo) + { + pendingClient.Connection.SetAccountInfo(accountInfo); + pendingClient.Name = packet.Name; + pendingClient.OwnerKey = packet.OwnerKey; + pendingClient.InitializationStep = serverSettings.HasPassword ? ConnectionInitialization.Password : ConnectionInitialization.ContentPackageOrder; + } + + void rejectClient() + { + RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.AuthenticationFailed)); + } + + if (authenticators is null + || !packet.AuthTicket.TryUnwrap(out var authTicket) + || !authenticators.TryGetValue(authTicket.Kind, out var authenticator)) { - bool requireSteamAuth = GameSettings.CurrentConfig.RequireSteamAuthentication; #if DEBUG - requireSteamAuth = false; + DebugConsole.NewMessage($"Debug server accepts unauthenticated connections", Microsoft.Xna.Framework.Color.Yellow); + acceptClient(new AccountInfo(packet.AccountId)); +#else + rejectClient(); #endif - bool hasSteamAuth = packet.SteamAuthTicket.TryUnwrap(out var ticket); - - //steam auth cannot be done (SteamManager not initialized or no ticket given), - //but it's not required either -> let the client join without auth - if ((!SteamManager.IsInitialized || !hasSteamAuth) && !requireSteamAuth) - { - pendingClient.Name = packet.Name; - pendingClient.OwnerKey = packet.OwnerKey; - pendingClient.InitializationStep = serverSettings.HasPassword ? ConnectionInitialization.Password : ConnectionInitialization.ContentPackageOrder; - } - else - { - if (!packet.SteamId.TryUnwrap(out var id) || id is not SteamId steamId) - { - if (requireSteamAuth) - { - RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.SteamAuthenticationFailed)); - return; - } - } - else - { - Steamworks.BeginAuthResult authSessionStartState = SteamManager.StartAuthSession(ticket, steamId); - if (authSessionStartState != Steamworks.BeginAuthResult.OK) - { - if (requireSteamAuth) - { - RemovePendingClient(pendingClient, PeerDisconnectPacket.SteamAuthError(authSessionStartState)); - } - else - { - packet.SteamId = Option.None(); - pendingClient.InitializationStep = serverSettings.HasPassword ? ConnectionInitialization.Password : ConnectionInitialization.ContentPackageOrder; - } - } - } - - pendingClient.Connection.SetAccountInfo(new AccountInfo(packet.SteamId.Select(uid => (AccountId)uid))); - pendingClient.Name = packet.Name; - pendingClient.OwnerKey = packet.OwnerKey; - pendingClient.AuthSessionStarted = true; - } + return; } - else + + pendingClient.AuthSessionStarted = true; + TaskPool.Add($"{nameof(LidgrenServerPeer)}.ProcessAuth", authenticator.VerifyTicket(authTicket), t => { - if (pendingClient.AccountInfo.AccountId != packet.SteamId.Select(uid => (AccountId)uid)) + if (!t.TryGetResult(out AccountInfo accountInfo) + || accountInfo.IsNone) { - RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.SteamAuthenticationFailed)); + rejectClient(); + return; } - } + + acceptClient(accountInfo); + }); } private NetSendResult ForwardToLidgren(IWriteMessage msg, NetworkConnection connection, DeliveryMethod deliveryMethod) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/P2PServerPeer.cs similarity index 59% rename from Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs rename to Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/P2PServerPeer.cs index b51065601..69dc977a6 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/P2PServerPeer.cs @@ -1,30 +1,20 @@ #nullable enable using System; -using System.Collections.Generic; +using System.Linq; namespace Barotrauma.Networking { - internal sealed class SteamP2PServerPeer : ServerPeer + internal sealed class P2PServerPeer : ServerPeer { private bool started; - private readonly SteamId ownerSteamId; + private readonly P2PEndpoint ownerEndpoint; - private UInt64 ownerKey64 => unchecked((UInt64)ownerKey.Fallback(0)); - - private SteamId ReadSteamId(IReadMessage inc) => new SteamId(inc.ReadUInt64() ^ ownerKey64); - private void WriteSteamId(IWriteMessage msg, SteamId val) => msg.WriteUInt64(val.Value ^ ownerKey64); - - public SteamP2PServerPeer(SteamId steamId, int ownerKey, ServerSettings settings, Callbacks callbacks) : base(callbacks) + public P2PServerPeer(P2PEndpoint ownerEp, int ownerKey, ServerSettings settings, Callbacks callbacks) : base(callbacks, settings) { - serverSettings = settings; + this.ownerKey = Option.Some(ownerKey); - connectedClients = new List(); - pendingClients = new List(); - - this.ownerKey = Option.Some(ownerKey); - - ownerSteamId = steamId; + ownerEndpoint = ownerEp; started = false; } @@ -37,7 +27,7 @@ namespace Barotrauma.Networking PacketHeader = PacketHeader.IsConnectionInitializationStep | PacketHeader.IsServerMessage, Initialization = null }; - SendMsgInternal(ownerSteamId, headers, null); + SendMsgInternal(ownerEndpoint, headers, null); started = true; } @@ -55,7 +45,7 @@ namespace Barotrauma.Networking for (int i = connectedClients.Count - 1; i >= 0; i--) { - Disconnect(connectedClients[i], PeerDisconnectPacket.WithReason(DisconnectReason.ServerShutdown)); + Disconnect(connectedClients[i].Connection, PeerDisconnectPacket.WithReason(DisconnectReason.ServerShutdown)); } pendingClients.Clear(); @@ -73,7 +63,7 @@ namespace Barotrauma.Networking //backwards for loop so we can remove elements while iterating for (int i = connectedClients.Count - 1; i >= 0; i--) { - SteamP2PConnection conn = (SteamP2PConnection)connectedClients[i]; + var conn = connectedClients[i].Connection; conn.Decay(deltaTime); if (conn.Timeout < 0.0) { @@ -83,7 +73,7 @@ namespace Barotrauma.Networking try { - while (ChildServerRelay.Read(out byte[] incBuf)) + foreach (var incBuf in ChildServerRelay.Read()) { IReadMessage inc = new ReadOnlyMessage(incBuf, false, 0, incBuf.Length, OwnerConnection); @@ -114,51 +104,34 @@ namespace Barotrauma.Networking { if (!started) { return; } - SteamId senderSteamId = ReadSteamId(inc); - SteamId sentOwnerSteamId = ReadSteamId(inc); - - var (deliveryMethod, packetHeader, initialization) = INetSerializableStruct.Read(inc); - - if (packetHeader.IsServerMessage()) + var senderInfo = INetSerializableStruct.Read(inc); + if (!senderInfo.Endpoint.TryUnwrap(out var senderEndpoint)) { - DebugConsole.ThrowError($"Got server message from {senderSteamId}"); return; } - if (senderSteamId != ownerSteamId) //sender is remote, handle disconnects and heartbeats - { - bool connectionMatches(NetworkConnection conn) => - conn is SteamP2PConnection { Endpoint: SteamP2PEndpoint { SteamId: var steamId } } - && steamId == senderSteamId; + var (_, packetHeader, initialization) = INetSerializableStruct.Read(inc); - PendingClient? pendingClient = pendingClients.Find(c => connectionMatches(c.Connection)); - SteamP2PConnection? connectedClient = connectedClients.Find(connectionMatches) as SteamP2PConnection; + if (packetHeader.IsServerMessage()) + { + DebugConsole.ThrowError($"Got server message from {senderEndpoint}"); + return; + } + + if (senderEndpoint != ownerEndpoint) //sender is remote, handle disconnects and heartbeats + { + bool connectionMatches(P2PConnection conn) => + conn.Endpoint == senderEndpoint; + + var pendingClient = pendingClients.Find(c => connectionMatches(c.Connection)); + var connectedClient = connectedClients.Find(c => connectionMatches(c.Connection)); + pendingClient?.Connection.SetAccountInfo(senderInfo.AccountInfo); pendingClient?.Heartbeat(); - connectedClient?.Heartbeat(); + connectedClient?.Connection.Heartbeat(); - if (packetHeader.IsConnectionInitializationStep()) - { - if (!initialization.HasValue) { return; } - ConnectionInitialization initializationStep = initialization.Value; - - if (pendingClient != null) - { - pendingClient.Connection.SetAccountInfo(new AccountInfo(senderSteamId, sentOwnerSteamId)); - ReadConnectionInitializationStep( - pendingClient, - new ReadWriteMessage(inc.Buffer, inc.BitPosition, inc.LengthBits, false), - initializationStep); - } - else if (initializationStep == ConnectionInitialization.ConnectionStarted) - { - pendingClient = new PendingClient(new SteamP2PConnection(senderSteamId)); - pendingClient.Connection.SetAccountInfo(new AccountInfo(senderSteamId, sentOwnerSteamId)); - pendingClients.Add(pendingClient); - } - } - else if (serverSettings.BanList.IsBanned(senderSteamId, out string banReason) || - serverSettings.BanList.IsBanned(sentOwnerSteamId, out banReason)) + if (serverSettings.BanList.IsBanned(senderEndpoint, out string banReason) + || serverSettings.BanList.IsBanned(senderInfo.AccountInfo, out banReason)) { if (pendingClient != null) { @@ -166,7 +139,7 @@ namespace Barotrauma.Networking } else if (connectedClient != null) { - Disconnect(connectedClient, PeerDisconnectPacket.Banned(banReason)); + Disconnect(connectedClient.Connection, PeerDisconnectPacket.Banned(banReason)); } } else if (packetHeader.IsDisconnectMessage()) @@ -177,7 +150,7 @@ namespace Barotrauma.Networking } else if (connectedClient != null) { - Disconnect(connectedClient, PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected)); + Disconnect(connectedClient.Connection, PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected)); } } else if (packetHeader.IsHeartbeatMessage()) @@ -185,16 +158,44 @@ namespace Barotrauma.Networking //message exists solely as a heartbeat, ignore its contents return; } + else if (packetHeader.IsConnectionInitializationStep()) + { + if (!initialization.HasValue) { return; } + ConnectionInitialization initializationStep = initialization.Value; + + if (pendingClient != null) + { + ReadConnectionInitializationStep( + pendingClient, + new ReadWriteMessage(inc.Buffer, inc.BitPosition, inc.LengthBits, false), + initializationStep); + } + else if (initializationStep == ConnectionInitialization.ConnectionStarted) + { + pendingClients.Add(new PendingClient(senderEndpoint.MakeConnectionFromEndpoint())); + } + } else if (connectedClient != null) { - var packet = INetSerializableStruct.Read(inc); - IReadMessage msg = new ReadOnlyMessage(packet.Buffer, packetHeader.IsCompressed(), 0, packet.Length, connectedClient); - callbacks.OnMessageReceived.Invoke(connectedClient, msg); + if (packetHeader.IsDataFragment()) + { + var completeMessageOption = connectedClient.Defragmenter.ProcessIncomingFragment(INetSerializableStruct.Read(inc)); + if (!completeMessageOption.TryUnwrap(out var completeMessage)) { return; } + + IReadMessage msg = new ReadOnlyMessage(completeMessage.ToArray(), false, 0, completeMessage.Length, connectedClient.Connection); + callbacks.OnMessageReceived.Invoke(connectedClient.Connection, msg); + } + else + { + var packet = INetSerializableStruct.Read(inc); + IReadMessage msg = new ReadOnlyMessage(packet.Buffer, packetHeader.IsCompressed(), 0, packet.Length, connectedClient.Connection); + callbacks.OnMessageReceived.Invoke(connectedClient.Connection, msg); + } } } else //sender is owner { - (OwnerConnection as SteamP2PConnection)?.Heartbeat(); + OwnerConnection?.Heartbeat(); if (packetHeader.IsDisconnectMessage()) { @@ -212,14 +213,12 @@ namespace Barotrauma.Networking { if (OwnerConnection is null) { - var packet = INetSerializableStruct.Read(inc); - OwnerConnection = new SteamP2PConnection(ownerSteamId) - { - Language = GameSettings.CurrentConfig.Language - }; - OwnerConnection.SetAccountInfo(new AccountInfo(ownerSteamId, ownerSteamId)); + var packet = INetSerializableStruct.Read(inc); + OwnerConnection = ownerEndpoint.MakeConnectionFromEndpoint(); + OwnerConnection.Language = GameSettings.CurrentConfig.Language; + OwnerConnection.SetAccountInfo(senderInfo.AccountInfo); - callbacks.OnInitializationComplete.Invoke(OwnerConnection, packet.OwnerName); + callbacks.OnInitializationComplete.Invoke(OwnerConnection, packet.Name); callbacks.OnOwnerDetermined.Invoke(OwnerConnection); } @@ -239,27 +238,38 @@ namespace Barotrauma.Networking } } - public override void InitializeSteamServerCallbacks() - { - throw new InvalidOperationException("Called InitializeSteamServerCallbacks on SteamP2PServerPeer!"); - } - public override void Send(IWriteMessage msg, NetworkConnection conn, DeliveryMethod deliveryMethod, bool compressPastThreshold = true) { if (!started) { return; } - if (conn is not SteamP2PConnection steamP2PConn) { return; } + if (conn is not P2PConnection p2pConn) { return; } - if (!connectedClients.Contains(steamP2PConn) && conn != OwnerConnection) + int ccIndex = connectedClients.FindIndex(cc => cc.Connection == p2pConn); + if (ccIndex < 0 && conn != OwnerConnection) { - DebugConsole.ThrowError($"Tried to send message to unauthenticated connection: {steamP2PConn.AccountInfo.AccountId}"); + DebugConsole.ThrowError($"Tried to send message to unauthenticated connection: {p2pConn.AccountInfo.AccountId}"); return; } - - if (!conn.AccountInfo.AccountId.TryUnwrap(out var connAccountId) || connAccountId is not SteamId) { return; } - byte[] bufAux = msg.PrepareForSending(compressPastThreshold, out bool isCompressed, out _); + if (bufAux.Length > MessageFragment.MaxSize && conn != OwnerConnection) + { + var cc = connectedClients[ccIndex]; + var fragments = cc.Fragmenter.FragmentMessage(msg.Buffer.AsSpan()[..msg.LengthBytes]); + foreach (var fragment in fragments) + { + var fragmentHeaders = new PeerPacketHeaders + { + DeliveryMethod = DeliveryMethod.Reliable, + PacketHeader = PacketHeader.IsDataFragment + | PacketHeader.IsServerMessage, + Initialization = null + }; + SendMsgInternal(p2pConn, fragmentHeaders, fragment); + } + return; + } + var headers = new PeerPacketHeaders { DeliveryMethod = deliveryMethod, @@ -271,10 +281,10 @@ namespace Barotrauma.Networking { Buffer = bufAux }; - SendMsgInternal(steamP2PConn, headers, body); + SendMsgInternal(p2pConn, headers, body); } - private void SendDisconnectMessage(SteamId steamId, PeerDisconnectPacket peerDisconnectPacket) + private void SendDisconnectMessage(P2PEndpoint endpoint, PeerDisconnectPacket peerDisconnectPacket) { if (!started) { return; } @@ -285,44 +295,41 @@ namespace Barotrauma.Networking Initialization = null }; - SendMsgInternal(steamId, headers, peerDisconnectPacket); + SendMsgInternal(endpoint, headers, peerDisconnectPacket); } public override void Disconnect(NetworkConnection conn, PeerDisconnectPacket peerDisconnectPacket) { if (!started) { return; } - if (conn is not SteamP2PConnection steamp2pConn) { return; } + if (conn is not P2PConnection p2pConn) { return; } - if (!conn.AccountInfo.AccountId.TryUnwrap(out var connAccountId) || connAccountId is not SteamId connSteamId) { return; } + SendDisconnectMessage(p2pConn.Endpoint, peerDisconnectPacket); - SendDisconnectMessage(connSteamId, peerDisconnectPacket); - - if (connectedClients.Contains(steamp2pConn)) + if (connectedClients.FindIndex(cc => cc.Connection == p2pConn) is >= 0 and var ccIndex) { - steamp2pConn.Status = NetworkConnectionStatus.Disconnected; - connectedClients.Remove(steamp2pConn); + p2pConn.Status = NetworkConnectionStatus.Disconnected; + connectedClients.RemoveAt(ccIndex); callbacks.OnDisconnect.Invoke(conn, peerDisconnectPacket); - Steam.SteamManager.StopAuthSession(connSteamId); } - else if (steamp2pConn == OwnerConnection) + else if (p2pConn == OwnerConnection) { throw new InvalidOperationException("Cannot disconnect owner peer"); } } - protected override void SendMsgInternal(NetworkConnection conn, PeerPacketHeaders headers, INetSerializableStruct? body) + protected override void SendMsgInternal(P2PConnection conn, PeerPacketHeaders headers, INetSerializableStruct? body) { - var connSteamId = conn is SteamP2PConnection { Endpoint: SteamP2PEndpoint { SteamId: var id } } ? id : null; - if (connSteamId is null) { return; } - - SendMsgInternal(connSteamId, headers, body); + SendMsgInternal(conn.Endpoint, headers, body); } - - private void SendMsgInternal(SteamId connSteamId, PeerPacketHeaders headers, INetSerializableStruct? body) + + private void SendMsgInternal(P2PEndpoint connEndpoint, PeerPacketHeaders headers, INetSerializableStruct? body) { IWriteMessage msgToSend = new WriteOnlyMessage(); - WriteSteamId(msgToSend, connSteamId); + msgToSend.WriteNetSerializableStruct(new P2PServerToOwnerHeader + { + EndpointStr = connEndpoint.StringRepresentation + }); msgToSend.WriteNetSerializableStruct(headers); body?.Write(msgToSend); @@ -336,11 +343,13 @@ namespace Barotrauma.Networking ChildServerRelay.Write(bufToSend); } - protected override void ProcessAuthTicket(ClientSteamTicketAndVersionPacket packet, PendingClient pendingClient) + protected override void ProcessAuthTicket(ClientAuthTicketAndVersionPacket packet, PendingClient pendingClient) { + // Do nothing with the auth ticket because that should be handled by the owner peer, + // just assume that authentication succeeded pendingClient.InitializationStep = serverSettings.HasPassword ? ConnectionInitialization.Password : ConnectionInitialization.ContentPackageOrder; pendingClient.Name = packet.Name; pendingClient.AuthSessionStarted = true; } } -} \ No newline at end of file +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs index 143ac4dae..1c5d06389 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs @@ -7,28 +7,14 @@ using Barotrauma.Extensions; namespace Barotrauma.Networking { - internal abstract class ServerPeer + internal abstract class ServerPeer : ServerPeer where TConnection : NetworkConnection { - public readonly record struct Callbacks( - Callbacks.MessageCallback OnMessageReceived, - Callbacks.DisconnectCallback OnDisconnect, - Callbacks.InitializationCompleteCallback OnInitializationComplete, - Callbacks.ShutdownCallback OnShutdown, - Callbacks.OwnerDeterminedCallback OnOwnerDetermined) - { - public delegate void MessageCallback(NetworkConnection connection, IReadMessage message); - public delegate void DisconnectCallback(NetworkConnection connection, PeerDisconnectPacket peerDisconnectPacket); - public delegate void InitializationCompleteCallback(NetworkConnection connection, string? clientName); - public delegate void ShutdownCallback(); - public delegate void OwnerDeterminedCallback(NetworkConnection connection); - } - - protected readonly Callbacks callbacks; private readonly ImmutableArray contentPackages; - - protected ServerPeer(Callbacks callbacks) + protected ServerPeer(Callbacks callbacks, ServerSettings serverSettings) : base(callbacks) { - this.callbacks = callbacks; + this.serverSettings = serverSettings; + this.connectedClients = new List(); + this.pendingClients = new List(); List contentPackageList = new List(); foreach (var cp in ContentPackageManager.EnabledPackages.All) @@ -45,19 +31,13 @@ namespace Barotrauma.Networking contentPackageList.Add(cp); } contentPackages = contentPackageList.ToImmutableArray(); - } - - public abstract void InitializeSteamServerCallbacks(); - - public abstract void Start(); - public abstract void Close(); - public abstract void Update(float deltaTime); + } protected sealed class PendingClient { public string? Name; public Option OwnerKey; - public readonly NetworkConnection Connection; + public readonly TConnection Connection; public ConnectionInitialization InitializationStep; public double UpdateTime; public double TimeOut; @@ -67,11 +47,11 @@ namespace Barotrauma.Networking public AccountInfo AccountInfo => Connection.AccountInfo; - public PendingClient(NetworkConnection conn) + public PendingClient(TConnection conn) { - OwnerKey = Option.None(); + OwnerKey = Option.None; Connection = conn; - InitializationStep = ConnectionInitialization.SteamTicketAndVersion; + InitializationStep = ConnectionInitialization.AuthInfoAndVersion; Retries = 0; PasswordSalt = null; UpdateTime = Timing.TotalTime + Timing.Step * 3.0; @@ -85,11 +65,26 @@ namespace Barotrauma.Networking } } - protected List connectedClients = null!; - protected List pendingClients = null!; - protected ServerSettings serverSettings = null!; + protected sealed class ConnectedClient + { + public readonly TConnection Connection; + public readonly MessageFragmenter Fragmenter; + public readonly MessageDefragmenter Defragmenter; + + public ConnectedClient(TConnection connection) + { + Connection = connection; + Fragmenter = new(); + Defragmenter = new(); + } + } + + protected readonly List connectedClients; + protected readonly List pendingClients; + protected readonly ServerSettings serverSettings; + + protected TConnection? OwnerConnection; protected Option ownerKey = Option.None; - protected NetworkConnection? OwnerConnection; protected void ReadConnectionInitializationStep(PendingClient pendingClient, IReadMessage inc, ConnectionInitialization initializationStep) { @@ -101,8 +96,8 @@ namespace Barotrauma.Networking switch (initializationStep) { - case ConnectionInitialization.SteamTicketAndVersion: - var authPacket = INetSerializableStruct.Read(inc); + case ConnectionInitialization.AuthInfoAndVersion: + var authPacket = INetSerializableStruct.Read(inc); if (!Client.IsValidName(authPacket.Name, serverSettings)) { @@ -117,8 +112,8 @@ namespace Barotrauma.Networking { RemovePendingClient(pendingClient, PeerDisconnectPacket.InvalidVersion()); - GameServer.Log($"{authPacket.Name} ({authPacket.SteamId}) couldn't join the server (incompatible game version)", ServerLog.MessageType.Error); - DebugConsole.NewMessage($"{authPacket.Name} ({authPacket.SteamId}) couldn't join the server (incompatible game version)", Microsoft.Xna.Framework.Color.Red); + GameServer.Log($"{authPacket.Name} ({authPacket.AccountId}) couldn't join the server (incompatible game version)", ServerLog.MessageType.Error); + DebugConsole.NewMessage($"{authPacket.Name} ({authPacket.AccountId}) couldn't join the server (incompatible game version)", Microsoft.Xna.Framework.Color.Red); return; } @@ -128,7 +123,7 @@ namespace Barotrauma.Networking if (nameTaken != null) { RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.NameTaken)); - GameServer.Log($"{authPacket.Name} ({authPacket.SteamId}) couldn't join the server (name too similar to the name of the client \"" + nameTaken.Name + "\").", ServerLog.MessageType.Error); + GameServer.Log($"{authPacket.Name} ({authPacket.AccountId}) couldn't join the server (name too similar to the name of the client \"" + nameTaken.Name + "\").", ServerLog.MessageType.Error); return; } @@ -172,7 +167,7 @@ namespace Barotrauma.Networking } } - protected abstract void ProcessAuthTicket(ClientSteamTicketAndVersionPacket packet, PendingClient pendingClient); + protected abstract void ProcessAuthTicket(ClientAuthTicketAndVersionPacket packet, PendingClient pendingClient); protected void BanPendingClient(PendingClient pendingClient, string banReason, TimeSpan? duration) { @@ -214,7 +209,7 @@ namespace Barotrauma.Networking return isBanned; } - protected abstract void SendMsgInternal(NetworkConnection conn, PeerPacketHeaders headers, INetSerializableStruct? body); + protected abstract void SendMsgInternal(TConnection conn, PeerPacketHeaders headers, INetSerializableStruct? body); protected void UpdatePendingClient(PendingClient pendingClient) { @@ -231,8 +226,8 @@ namespace Barotrauma.Networking if (pendingClient.InitializationStep == ConnectionInitialization.Success) { - NetworkConnection newConnection = pendingClient.Connection; - connectedClients.Add(newConnection); + TConnection newConnection = pendingClient.Connection; + connectedClients.Add(new ConnectedClient(newConnection)); pendingClients.Remove(pendingClient); callbacks.OnInitializationComplete.Invoke(newConnection, pendingClient.Name); @@ -305,14 +300,38 @@ namespace Barotrauma.Networking pendingClients.Remove(pendingClient); - if (pendingClient.AuthSessionStarted && pendingClient.AccountInfo.AccountId.TryUnwrap(out var steamId)) - { - Steam.SteamManager.StopAuthSession(steamId); - pendingClient.Connection.SetAccountInfo(AccountInfo.None); - pendingClient.AuthSessionStarted = false; - } + pendingClient.Connection.SetAccountInfo(AccountInfo.None); + pendingClient.AuthSessionStarted = false; } } + } + + internal abstract class ServerPeer + { + public readonly record struct Callbacks( + Callbacks.MessageCallback OnMessageReceived, + Callbacks.DisconnectCallback OnDisconnect, + Callbacks.InitializationCompleteCallback OnInitializationComplete, + Callbacks.ShutdownCallback OnShutdown, + Callbacks.OwnerDeterminedCallback OnOwnerDetermined) + { + public delegate void MessageCallback(NetworkConnection connection, IReadMessage message); + public delegate void DisconnectCallback(NetworkConnection connection, PeerDisconnectPacket peerDisconnectPacket); + public delegate void InitializationCompleteCallback(NetworkConnection connection, string? clientName); + public delegate void ShutdownCallback(); + public delegate void OwnerDeterminedCallback(NetworkConnection connection); + } + + protected readonly Callbacks callbacks; + + protected ServerPeer(Callbacks callbacks) + { + this.callbacks = callbacks; + } + + public abstract void Start(); + public abstract void Close(); + public abstract void Update(float deltaTime); public abstract void Send(IWriteMessage msg, NetworkConnection conn, DeliveryMethod deliveryMethod, bool compressPastThreshold = true); public abstract void Disconnect(NetworkConnection conn, PeerDisconnectPacket peerDisconnectPacket); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index 7f29c7662..b75d7efc8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using Barotrauma.Steam; namespace Barotrauma.Networking { @@ -281,9 +282,11 @@ namespace Barotrauma.Networking doc.Root.SetAttributeValue("name", ServerName); doc.Root.SetAttributeValue("port", Port); -#if USE_STEAM - doc.Root.SetAttributeValue("queryport", QueryPort); -#endif + + if (QueryPort != 0) + { + doc.Root.SetAttributeValue("queryport", QueryPort); + } doc.Root.SetAttributeValue("password", password ?? ""); doc.Root.SetAttributeValue("enableupnp", EnableUPnP); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Program.cs b/Barotrauma/BarotraumaServer/ServerSource/Program.cs index e1d371659..5fb8fe15c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Program.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Program.cs @@ -6,6 +6,8 @@ using System.Diagnostics; using Barotrauma.IO; using System.Linq; using System.Text; +using System.Threading; +using Barotrauma.Debugging; using Barotrauma.Networking; #if LINUX using System.Runtime.InteropServices; @@ -48,8 +50,9 @@ namespace Barotrauma [STAThread] static void Main(string[] args) { -#if !DEBUG AppDomain currentDomain = AppDomain.CurrentDomain; + currentDomain.ProcessExit += OnProcessExit; +#if !DEBUG currentDomain.UnhandledException += new UnhandledExceptionEventHandler(CrashHandler); #endif TryStartChildServerRelay(args); @@ -74,11 +77,37 @@ namespace Barotrauma string executableDir = Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location); Directory.SetCurrentDirectory(executableDir); + DebugConsoleCore.Init( + newMessage: (s, c) => DebugConsole.NewMessage(s, c), + log: DebugConsole.Log); Game = new GameMain(args); Game.Run(); + ShutDown(); + } + + private static bool hasShutDown = false; + private static void ShutDown() + { + if (hasShutDown) { return; } + hasShutDown = true; + if (GameAnalyticsManager.SendUserStatistics) { GameAnalyticsManager.ShutDown(); } SteamManager.ShutDown(); + + // Gracefully exit EOS by ticking until the session is closed + EosInterface.Core.CleanupAndQuit(); + while (EosInterface.Core.IsInitialized) + { + EosInterface.Core.Update(); + TaskPool.Update(); + Thread.Sleep(16); + } + } + + private static void OnProcessExit(object sender, EventArgs e) + { + ShutDown(); } static GameMain Game; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs index 039e30109..bfcefab09 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs @@ -1,5 +1,4 @@ -using System.Globalization; -using System.Linq; +using System.Collections.Generic; using Barotrauma.Networking; namespace Barotrauma.Steam @@ -31,8 +30,6 @@ namespace Barotrauma.Steam RefreshServerDetails(server); - server.ServerPeer.InitializeSteamServerCallbacks(); - Steamworks.SteamServer.LogOnAnonymous(); return true; @@ -45,75 +42,40 @@ namespace Barotrauma.Steam return false; } - var contentPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent); - - // These server state variables may be changed at any time. Note that there is no longer a mechanism - // to send the player count. The player count is maintained by Steam and you should use the player - // creation/authentication functions to maintain your player count. - Steamworks.SteamServer.ServerName = server.ServerName; - Steamworks.SteamServer.MaxPlayers = server.ServerSettings.MaxPlayers; - Steamworks.SteamServer.Passworded = server.ServerSettings.HasPassword; Steamworks.SteamServer.MapName = GameMain.NetLobbyScreen?.SelectedSub?.DisplayName?.Value ?? ""; - Steamworks.SteamServer.SetKey("haspassword", server.ServerSettings.HasPassword.ToString()); - Steamworks.SteamServer.SetKey("message", server.ServerSettings.ServerMessageText); - Steamworks.SteamServer.SetKey("version", GameMain.Version.ToString()); - Steamworks.SteamServer.SetKey("playercount", server.ConnectedClients.Count.ToString()); - - //a2s seems to break if too much data is added (seems to be related to MTU?) - //let's restrict the number of packages to 10, clients can use packagecount to tell when the list has been truncated - const int MaxPackagesToList = 10; - int index = 0; - foreach (var contentPackage in contentPackages.Take(MaxPackagesToList)) - { - string ugcIdStr = contentPackage.UgcId.TryUnwrap(out var ugcId) ? ugcId.StringRepresentation : string.Empty; - Steamworks.SteamServer.SetKey( - $"contentpackage{index}", - contentPackage.Name + "," + contentPackage.Hash.StringRepresentation + "," + ugcIdStr); - index++; - } - Steamworks.SteamServer.SetKey("packagecount", contentPackages.Count().ToString()); - Steamworks.SteamServer.SetKey("modeselectionmode", server.ServerSettings.ModeSelectionMode.ToString()); - Steamworks.SteamServer.SetKey("subselectionmode", server.ServerSettings.SubSelectionMode.ToString()); - Steamworks.SteamServer.SetKey("voicechatenabled", server.ServerSettings.VoiceChatEnabled.ToString()); - Steamworks.SteamServer.SetKey("allowspectating", server.ServerSettings.AllowSpectating.ToString()); - Steamworks.SteamServer.SetKey("allowrespawn", server.ServerSettings.AllowRespawn.ToString()); - Steamworks.SteamServer.SetKey("traitors", server.ServerSettings.TraitorProbability.ToString(CultureInfo.InvariantCulture)); - Steamworks.SteamServer.SetKey("friendlyfireenabled", server.ServerSettings.AllowFriendlyFire.ToString()); - Steamworks.SteamServer.SetKey("karmaenabled", server.ServerSettings.KarmaEnabled.ToString()); - Steamworks.SteamServer.SetKey("gamestarted", server.GameStarted.ToString()); - Steamworks.SteamServer.SetKey("gamemode", server.ServerSettings.GameModeIdentifier.Value); - Steamworks.SteamServer.SetKey("playstyle", server.ServerSettings.PlayStyle.ToString()); - Steamworks.SteamServer.SetKey("language", server.ServerSettings.Language.ToString()); - if (GameMain.NetLobbyScreen?.SelectedSub != null) - { - Steamworks.SteamServer.SetKey("submarine", GameMain.NetLobbyScreen.SelectedSub.Name); - } + server.ServerSettings.UpdateServerListInfo(SetServerListInfo); Steamworks.SteamServer.DedicatedServer = true; return true; } - public static Steamworks.BeginAuthResult StartAuthSession(byte[] authTicketData, SteamId clientSteamID) + private static void SetServerListInfo(Identifier key, object value) { - if (!IsInitialized || !Steamworks.SteamServer.IsValid) return Steamworks.BeginAuthResult.ServerNotConnectedToSteam; - - DebugConsole.Log("SteamManager authenticating Steam client " + clientSteamID); - Steamworks.BeginAuthResult startResult = Steamworks.SteamServer.BeginAuthSession(authTicketData, clientSteamID.Value); - if (startResult != Steamworks.BeginAuthResult.OK) + switch (value) { - DebugConsole.Log("Authentication failed: failed to start auth session (" + startResult.ToString() + ")"); + case string stringValue when key == "ServerName": + Steamworks.SteamServer.ServerName = stringValue; + return; + case int maxPlayers when key == "MaxPlayers": + Steamworks.SteamServer.MaxPlayers = maxPlayers; + return; + case bool hasPassword when key == "HasPassword": + Steamworks.SteamServer.Passworded = hasPassword; + return; + case IEnumerable contentPackages: + int index = 0; + foreach (var contentPackage in contentPackages) + { + Steamworks.SteamServer.SetKey( + $"contentpackage{index}", + new ServerListContentPackageInfo(contentPackage).ToString()); + index++; + } + return; } - - return startResult; - } - - public static void StopAuthSession(SteamId clientSteamId) - { - if (!IsInitialized || !Steamworks.SteamServer.IsValid) return; - - DebugConsole.Log("SteamManager ending auth session with Steam client " + clientSteamId); - Steamworks.SteamServer.EndSession(clientSteamId.Value); + + Steamworks.SteamServer.SetKey(key.Value.ToLowerInvariant(), value.ToString()); } public static bool CloseServer() diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs index 498108b29..004e422df 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs @@ -458,10 +458,10 @@ namespace Barotrauma if (activeEvent.TraitorEvent.CurrentState == TraitorEvent.State.Completed) { - SteamAchievementManager.OnTraitorWin(activeEvent.TraitorEvent.Traitor?.Character); + AchievementManager.OnTraitorWin(activeEvent.TraitorEvent.Traitor?.Character); foreach (var secondaryTraitor in activeEvent.TraitorEvent.SecondaryTraitors) { - SteamAchievementManager.OnTraitorWin(secondaryTraitor?.Character); + AchievementManager.OnTraitorWin(secondaryTraitor?.Character); } } } diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 9573174e1..d7aaef3d6 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.2.8.0 + 1.3.0.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer @@ -17,13 +17,13 @@ - DEBUG;TRACE;SERVER;WINDOWS;USE_STEAM + DEBUG;TRACE;SERVER;WINDOWS x64 ..\bin\$(Configuration)Windows\ - TRACE;DEBUG;SERVER;WINDOWS;X64;USE_STEAM + TRACE;DEBUG;SERVER;WINDOWS;X64 x64 ..\bin\$(Configuration)Windows\ full @@ -31,20 +31,20 @@ - TRACE;SERVER;WINDOWS;USE_STEAM + TRACE;SERVER;WINDOWS x64 ..\bin\$(Configuration)Windows\ - TRACE;SERVER;WINDOWS;USE_STEAM + TRACE;SERVER;WINDOWS x64 ..\bin\$(Configuration)Windows\ true - TRACE;SERVER;WINDOWS;X64;USE_STEAM + TRACE;SERVER;WINDOWS;X64 x64 ..\bin\$(Configuration)Windows\ full @@ -52,7 +52,7 @@ - TRACE;SERVER;WINDOWS;X64;USE_STEAM + TRACE;SERVER;WINDOWS;X64 x64 ..\bin\$(Configuration)Windows\ full @@ -62,7 +62,6 @@ - @@ -87,6 +86,11 @@ + + + + + @@ -152,4 +156,10 @@ + + win-x64 + Win64 + + + diff --git a/Barotrauma/BarotraumaShared/DeployEosPrivate.props b/Barotrauma/BarotraumaShared/DeployEosPrivate.props new file mode 100644 index 000000000..a4a1e85e8 --- /dev/null +++ b/Barotrauma/BarotraumaShared/DeployEosPrivate.props @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/DeployGameAnalytics.props b/Barotrauma/BarotraumaShared/DeployGameAnalytics.props index e4818697e..9a7d707e1 100644 --- a/Barotrauma/BarotraumaShared/DeployGameAnalytics.props +++ b/Barotrauma/BarotraumaShared/DeployGameAnalytics.props @@ -9,10 +9,10 @@ false - + - + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/AchievementManager.cs similarity index 74% rename from Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs rename to Barotrauma/BarotraumaShared/SharedSource/AchievementManager.cs index 37064ac10..2cf5ccbb5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/AchievementManager.cs @@ -5,11 +5,15 @@ using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; namespace Barotrauma { - static class SteamAchievementManager + [NetworkSerialize] + internal readonly record struct NetIncrementedStat(AchievementStat Stat, float Amount) : INetSerializableStruct; + + static class AchievementManager { private const float UpdateInterval = 1.0f; @@ -22,7 +26,7 @@ namespace Barotrauma /// /// Keeps track of things that have happened during the round /// - class RoundData + private sealed class RoundData { public readonly List Reactors = new List(); @@ -61,12 +65,15 @@ namespace Barotrauma updateTimer -= deltaTime; if (updateTimer > 0.0f) { return; } updateTimer = UpdateInterval; - + if (Level.Loaded != null && roundData != null && Screen.Selected == GameMain.GameScreen) { if (GameMain.GameSession.EventManager.CurrentIntensity > 0.99f) { - UnlockAchievement("maxintensity".ToIdentifier(), true, c => c != null && !c.IsDead && !c.IsUnconscious); + UnlockAchievement( + identifier: "maxintensity".ToIdentifier(), + unlockClients: true, + conditions: static c => c is { IsDead: false, IsUnconscious: false }); } foreach (Character c in Character.CharacterList) @@ -273,12 +280,12 @@ namespace Barotrauma causeOfDeath.Killer != null && causeOfDeath.Killer == Character.Controlled) { - IncrementStat(causeOfDeath.Killer, (character.IsHuman ? "humanskilled" : "monsterskilled").ToIdentifier(), 1); + IncrementStat(causeOfDeath.Killer, character.IsHuman ? AchievementStat.HumansKilled : AchievementStat.MonstersKilled , 1); } #elif SERVER if (character != causeOfDeath.Killer && causeOfDeath.Killer != null) { - IncrementStat(causeOfDeath.Killer, (character.IsHuman ? "humanskilled" : "monsterskilled").ToIdentifier(), 1); + IncrementStat(causeOfDeath.Killer, character.IsHuman ? AchievementStat.HumansKilled : AchievementStat.MonstersKilled , 1); } #endif @@ -375,14 +382,14 @@ namespace Barotrauma !myCharacter.IsDead && (myCharacter.Submarine == gameSession.Submarine || (Level.Loaded?.EndOutpost != null && myCharacter.Submarine == Level.Loaded.EndOutpost))) { - IncrementStat("kmstraveled".ToIdentifier(), levelLengthKilometers); + IncrementStat(AchievementStat.KMsTraveled, levelLengthKilometers); } #endif } else { //in sp making it to the end is enough - IncrementStat("kmstraveled".ToIdentifier(), levelLengthKilometers); + IncrementStat(AchievementStat.KMsTraveled, levelLengthKilometers); } } @@ -497,31 +504,19 @@ namespace Barotrauma #endif } - private static void IncrementStat(Character recipient, Identifier identifier, int amount) + private static void IncrementStat(Character recipient, AchievementStat stat, int amount) { if (CheatsEnabled || recipient == null) { return; } #if CLIENT if (recipient == Character.Controlled) { - SteamManager.IncrementStat(identifier, amount); + IncrementStat(stat, amount); } #elif SERVER - GameMain.Server?.IncrementStat(recipient, identifier, amount); + GameMain.Server?.IncrementStat(recipient, stat, amount); #endif } - public static void IncrementStat(Identifier identifier, int amount) - { - if (CheatsEnabled) { return; } - SteamManager.IncrementStat(identifier, amount); - } - - public static void IncrementStat(Identifier identifier, float amount) - { - if (CheatsEnabled) { return; } - SteamManager.IncrementStat(identifier, amount); - } - public static void UnlockAchievement(Identifier identifier, bool unlockClients = false, Func conditions = null) { if (CheatsEnabled) { return; } @@ -539,15 +534,150 @@ namespace Barotrauma } } #endif - //already unlocked, no need to do anything - if (unlockedAchievements.Contains(identifier)) { return; } - unlockedAchievements.Add(identifier); #if CLIENT if (conditions != null && !conditions(Character.Controlled)) { return; } #endif - SteamManager.UnlockAchievement(identifier); + UnlockAchievementsOnPlatforms(identifier); + } + + private static void UnlockAchievementsOnPlatforms(Identifier identifier) + { + if (unlockedAchievements.Contains(identifier)) { return; } + + if (SteamManager.IsInitialized) + { + if (SteamManager.UnlockAchievement(identifier)) + { + unlockedAchievements.Add(identifier); + } + } + + if (EosInterface.Core.IsInitialized) + { + TaskPool.Add("Eos.UnlockAchievementsOnPlatforms", EosInterface.Achievements.UnlockAchievements(identifier), t => + { + if (!t.TryGetResult(out Result result)) { return; } + if (result.IsSuccess) { unlockedAchievements.Add(identifier); } + }); + } + } + + public static void IncrementStat(AchievementStat stat, float amount) + { + if (CheatsEnabled) { return; } + + IncrementStatOnPlatforms(stat, amount); + } + + private static void IncrementStatOnPlatforms(AchievementStat stat, float amount) + { + if (SteamManager.IsInitialized) + { + SteamManager.IncrementStats(stat.ToSteam(amount)); + } + + if (EosInterface.Core.IsInitialized) + { + TaskPool.Add("Eos.IncrementStat", EosInterface.Achievements.IngestStats(stat.ToEos(amount)), TaskPool.IgnoredCallback); + } + } + + public static void SyncBetweenPlatforms() + { + if (!SteamManager.IsInitialized || !EosInterface.Core.IsInitialized) { return; } + + var steamStats = SteamManager.GetAllStats(); + + TaskPool.AddWithResult("Eos.SyncBetweenPlatforms.QueryStats", EosInterface.Achievements.QueryStats(AchievementStatExtension.EosStats), result => + { + result.Match( + success: stats => SyncStats(stats, steamStats), + failure: static error => DebugConsole.ThrowError($"Failed to query stats from EOS: {error}")); + }); + + static void SyncStats(ImmutableDictionary eosStats, + ImmutableDictionary steamStats) + { + var steamStatsConverted = steamStats.Select(static s => s.Key.ToEos(s.Value)).ToImmutableDictionary(static s => s.Stat, static s => s.Value); + var eosStatsConverted = eosStats.Select(static s => s.Key.ToEos(s.Value)).ToImmutableDictionary(static s => s.Stat, static s => s.Value); + + static int GetStatValue(AchievementStat stat, ImmutableDictionary stats) => stats.TryGetValue(stat, out int value) ? value : 0; + + var highestStats = AchievementStatExtension.EosStats.ToDictionary( + static key => key, + value => + Math.Max( + GetStatValue(value, steamStatsConverted), + GetStatValue(value, eosStatsConverted))); + + List<(AchievementStat Stat, int Value)> eosStatsToIngest = new(), + steamStatsToIncrement = new(); + + foreach (var (stat, value) in highestStats) + { + int steamDiff = value - GetStatValue(stat, steamStatsConverted), + eosDiff = value - GetStatValue(stat, eosStatsConverted); + + if (steamDiff > 0) { steamStatsToIncrement.Add((stat, steamDiff)); } + if (eosDiff > 0) { eosStatsToIngest.Add((stat, eosDiff)); } + } + + if (steamStatsToIncrement.Any()) + { + SteamManager.IncrementStats(steamStatsToIncrement.Select(static s => s.Stat.ToSteam(s.Value)).ToArray()); + SteamManager.StoreStats(); + } + + if (eosStatsToIngest.Any()) + { + TaskPool.Add("Eos.SyncBetweenPlatforms.IngestStats", EosInterface.Achievements.IngestStats(eosStatsToIngest.ToArray()), TaskPool.IgnoredCallback); + } + } + + if (!SteamManager.TryGetUnlockedAchievements(out List steamUnlockedAchievements)) + { + DebugConsole.ThrowError("Failed to query unlocked achievements from Steam"); + return; + } + + TaskPool.AddWithResult("Eos.SyncBetweenPlatforms.QueryPlayerAchievements", EosInterface.Achievements.QueryPlayerAchievements(), t => + { + t.Match( + success: eosAchievements => SyncAchievements(eosAchievements, steamUnlockedAchievements), + failure: static error => DebugConsole.ThrowError($"Failed to query achievements from EOS: {error}")); + }); + + static void SyncAchievements( + ImmutableDictionary eosAchievements, + List steamUnlockedAchievements) + { + foreach (var (identifier, progress) in eosAchievements) + { + if (!IsUnlocked(progress)) { continue; } + + if (steamUnlockedAchievements.Any(a => a.Identifier.ToIdentifier() == identifier)) { continue; } + + SteamManager.UnlockAchievement(identifier); + } + + List eosAchievementsToUnlock = new(); + foreach (var achievement in steamUnlockedAchievements) + { + Identifier identifier = achievement.Identifier.ToIdentifier(); + if (eosAchievements.TryGetValue(identifier, out double progress) && IsUnlocked(progress)) { continue; } + + eosAchievementsToUnlock.Add(achievement.Identifier.ToIdentifier()); + } + + if (eosAchievementsToUnlock.Any()) + { + TaskPool.Add("Eos.SyncBetweenPlatforms.UnlockAchievements", EosInterface.Achievements.UnlockAchievements(eosAchievementsToUnlock.ToArray()), TaskPool.IgnoredCallback); + } + + static bool IsUnlocked(double progress) => progress >= 100.0d; + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 831287a4b..966417d20 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -1561,7 +1561,7 @@ namespace Barotrauma if (wasCritical && target.Vitality > 0.0f && Timing.TotalTime > lastReviveTime + 10.0f) { character.Info?.ApplySkillGain(Tags.MedicalSkill, SkillSettings.Current.SkillIncreasePerCprRevive); - SteamAchievementManager.OnCharacterRevived(target, character); + AchievementManager.OnCharacterRevived(target, character); lastReviveTime = (float)Timing.TotalTime; #if SERVER GameMain.Server?.KarmaManager?.OnCharacterHealthChanged(target, character, damage: Math.Min(prevVitality - target.Vitality, 0.0f), stun: 0.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 21ce51013..88aa76a5f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -1270,14 +1270,11 @@ namespace Barotrauma return newCharacter; } - private Character(Submarine submarine, ushort id): base(submarine, id) + protected Character(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, RagdollParams ragdollParams = null, bool spawnInitialItems = true) + : base(null, id) { wallet = new Wallet(Option.Some(this)); - } - protected Character(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, RagdollParams ragdollParams = null, bool spawnInitialItems = true) - : this(null, id) - { this.Seed = seed; this.Prefab = prefab; MTRandom random = new MTRandom(ToolBox.StringToInt(seed)); @@ -2478,7 +2475,7 @@ namespace Barotrauma return false; } - public Item GetEquippedItem(Identifier? tagOrIdentifier = null, InvSlotType? slotType = null) + public Item GetEquippedItem(Identifier tagOrIdentifier = default, InvSlotType? slotType = null) { if (Inventory == null) { return null; } for (int i = 0; i < Inventory.Capacity; i++) @@ -2493,7 +2490,7 @@ namespace Barotrauma } var item = Inventory.GetItemAt(i); if (item == null) { continue; } - if (tagOrIdentifier == null || tagOrIdentifier.Value.IsEmpty || item.Prefab.Identifier == tagOrIdentifier || item.HasTag(tagOrIdentifier.Value)) + if (tagOrIdentifier.IsEmpty || item.Prefab.Identifier == tagOrIdentifier || item.HasTag(tagOrIdentifier)) { return item; } @@ -4658,7 +4655,7 @@ namespace Barotrauma if (GameMain.GameSession != null && Screen.Selected == GameMain.GameScreen) { - SteamAchievementManager.OnCharacterKilled(this, CauseOfDeath); + AchievementManager.OnCharacterKilled(this, CauseOfDeath); } KillProjSpecific(causeOfDeath, causeOfDeathAffliction, log); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index 2b8466406..d733b8f92 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -1459,7 +1459,7 @@ namespace Barotrauma charElement.Add(new XAttribute("missionscompletedsincedeath", MissionsCompletedSinceDeath)); - if (MinReputationToHire.factionId != default) + if (!MinReputationToHire.factionId.IsEmpty) { charElement.Add( new XAttribute("factionId", MinReputationToHire.factionId), diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 7e813b74a..c5fa49f23 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -570,7 +570,7 @@ namespace Barotrauma matchingAfflictions.RemoveAt(i); if (i == 0) { i = matchingAfflictions.Count; } if (i > 0) { reduceAmount += surplus / i; } - SteamAchievementManager.OnAfflictionRemoved(matchingAffliction, Character); + AchievementManager.OnAfflictionRemoved(matchingAffliction, Character); } else { @@ -777,7 +777,7 @@ namespace Barotrauma Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab))), newAffliction.Source); afflictions.Add(copyAffliction, limbHealth); - SteamAchievementManager.OnAfflictionReceived(copyAffliction, Character); + AchievementManager.OnAfflictionReceived(copyAffliction, Character); MedicalClinic.OnAfflictionCountChanged(Character); Character.HealthUpdateInterval = 0.0f; @@ -818,7 +818,7 @@ namespace Barotrauma var affliction = kvp.Key; if (affliction.Strength <= 0.0f) { - SteamAchievementManager.OnAfflictionRemoved(affliction, Character); + AchievementManager.OnAfflictionRemoved(affliction, Character); if (!irremovableAfflictions.Contains(affliction)) { afflictionsToRemove.Add(affliction); } continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs index 7d75c9f7d..9fbcfa9ab 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs @@ -18,7 +18,7 @@ namespace Barotrauma.Abilities { if (tags.None()) { - return character.GetEquippedItem(null) != null; + return character.GetEquippedItem(Identifier.Empty) != null; } if (requireAll) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHoldingItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHoldingItem.cs index 00eb57b23..42c78a4a5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHoldingItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHoldingItem.cs @@ -16,7 +16,7 @@ internal sealed class AbilityConditionHoldingItem : AbilityConditionDataless { if (tags.Count is 0) { - return HasItemInHand(character, null); + return HasItemInHand(character, Identifier.Empty); } foreach (Identifier tag in tags) @@ -26,7 +26,7 @@ internal sealed class AbilityConditionHoldingItem : AbilityConditionDataless return false; - static bool HasItemInHand(Character character, Identifier? tagOrIdentifier) => + static bool HasItemInHand(Character character, Identifier tagOrIdentifier) => character.GetEquippedItem(tagOrIdentifier, InvSlotType.RightHand) is not null || character.GetEquippedItem(tagOrIdentifier, InvSlotType.LeftHand) is not null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxCursor.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxCursor.cs index 1340152a0..df7ea9a06 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxCursor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxCursor.cs @@ -54,7 +54,7 @@ namespace Barotrauma public static Color GenerateColor(string name) { Random random = new Random(ToolBox.StringToInt(name)); - return ToolBox.HSVToRGB(random.NextSingle() * 360f, 1f, 1f); + return ToolBoxCore.HSVToRGB(random.NextSingle() * 360f, 1f, 1f); } private const float UpdateTimeout = 5f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SubmarineFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SubmarineFile.cs index f3a777cb0..f24a0fc4b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SubmarineFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SubmarineFile.cs @@ -9,7 +9,7 @@ namespace Barotrauma { using var md5 = MD5.Create(); #warning TODO: this doesn't account for collisions, this should probably be using the PrefabCollection class like everything else - UintIdentifier = ToolBox.StringToUInt32Hash(Barotrauma.IO.Path.GetFileNameWithoutExtension(path.Value), md5); + UintIdentifier = ToolBoxCore.StringToUInt32Hash(Barotrauma.IO.Path.GetFileNameWithoutExtension(path.Value), md5); } public readonly UInt32 UintIdentifier; diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs index c932b4470..a812a773f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs @@ -72,9 +72,9 @@ namespace Barotrauma if (ugcId is not SteamWorkshopId steamWorkshopId) { return true; } if (!InstallTime.TryUnwrap(out var installTime)) { return true; } - Steamworks.Ugc.Item? item = await SteamManager.Workshop.GetItem(steamWorkshopId.Value); - if (item is null) { return true; } - return item.Value.LatestUpdateTime <= installTime.ToUtcValue(); + Option itemOption = await SteamManager.Workshop.GetItem(steamWorkshopId.Value); + if (!itemOption.TryUnwrap(out var item)) { return true; } + return item.LatestUpdateTime <= installTime.ToUtcValue(); } public int Index => ContentPackageManager.EnabledPackages.IndexOf(this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs index dc6a465be..b853d9705 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs @@ -306,7 +306,7 @@ namespace Barotrauma { onLoadFail?.Invoke( fileListPath, - result.TryUnwrapFailure(out var exception) ? exception : throw new Exception("unreachable")); + result.TryUnwrapFailure(out var exception) ? exception : throw new UnreachableCodeException()); continue; } @@ -497,7 +497,7 @@ namespace Barotrauma List enabledRegularPackages = new List(); #if CLIENT - TaskPool.Add("EnqueueWorkshopUpdates", EnqueueWorkshopUpdates(), t => { }); + TaskPool.AddWithResult("EnqueueWorkshopUpdates", EnqueueWorkshopUpdates(), t => { }); #else #warning TODO: implement Workshop updates for servers at some point #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs index 26afb242f..7559fe604 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs @@ -20,6 +20,7 @@ namespace Barotrauma Element = element; } + [return: NotNullIfNotNull("cxe")] public static implicit operator XElement?(ContentXElement? cxe) => cxe?.Element; //public static implicit operator ContentXElement?(XElement? xe) => xe is null ? null : new ContentXElement(null, xe); diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 0805e53d6..a013fabf8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -1,8 +1,6 @@ using Barotrauma.Extensions; using Barotrauma.Items.Components; using Barotrauma.Networking; -using Barotrauma.Steam; -using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Concurrent; @@ -12,8 +10,9 @@ using System.ComponentModel; using System.Globalization; using Barotrauma.IO; using System.Linq; -using System.Text; +using System.Threading.Tasks; using Barotrauma.MapCreatures.Behavior; +using System.Text; namespace Barotrauma { @@ -79,9 +78,7 @@ namespace Barotrauma { NewMessage( $"You need to enable cheats using the command \"enablecheats\" before you can use the command \"{Names.First()}\".", Color.Red); -#if USE_STEAM NewMessage("Enabling cheats will disable Steam achievements during this play session.", Color.Red); -#endif return; } @@ -182,7 +179,6 @@ namespace Barotrauma #if DEBUG CheatsEnabled = true; #endif - commands.Add(new Command("help", "", (string[] args) => { if (args.Length == 0) @@ -1815,7 +1811,7 @@ namespace Barotrauma NewMessage((GameSettings.CurrentConfig.VerboseLogging ? "Enabled" : "Disabled") + " verbose logging.", Color.White); }, isCheat: false)); - commands.Add(new Command("listtasks", "listtasks: Lists all asynchronous tasks currently in the task pool.", (string[] args) => { TaskPool.ListTasks(); })); + commands.Add(new Command("listtasks", "listtasks: Lists all asynchronous tasks currently in the task pool.", (string[] args) => { TaskPool.ListTasks(line => DebugConsole.NewMessage(line)); })); commands.Add(new Command("listcoroutines", "listcoroutines: Lists all coroutines currently running.", (string[] args) => { CoroutineManager.ListCoroutines(); })); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Eos/Session.cs b/Barotrauma/BarotraumaShared/SharedSource/Eos/Session.cs new file mode 100644 index 000000000..d87a7c2e1 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Eos/Session.cs @@ -0,0 +1,117 @@ +#nullable enable +using System.Collections.Generic; +using System.Linq; +using Barotrauma.Networking; +using Barotrauma.Steam; + +namespace Barotrauma.Eos; + +static class EosSessionManager +{ + public static Option CurrentOwnedSession; + + public static void LeaveSession() + { + if (!CurrentOwnedSession.TryUnwrap(out var ownedSession)) { return; } + ownedSession.Dispose(); + CurrentOwnedSession = Option.None; + } + + public static void UpdateOwnedSession(Endpoint endpoint, ServerSettings serverSettings) + => UpdateOwnedSession(Option.Some(endpoint), serverSettings); + + public static void UpdateOwnedSession(Option endpoint, ServerSettings serverSettings) + { + if (!EosInterface.Core.IsInitialized) { return; } + + if (!serverSettings.IsPublic) + { + // Sessions can only be public, so if that's not what we want then + // destroy the current one if it exists and do not attempt to create + // or update one + LeaveSession(); + return; + } + + var selfPuids = EosInterface.IdQueries.GetLoggedInPuids(); + if (!CurrentOwnedSession.TryUnwrap(out var ownedSession)) + { + if (!TaskPool.IsTaskRunning("CreateOwnedSession")) + { + TaskPool.Add( + "CreateOwnedSession", + EosInterface.Sessions.CreateSession(selfPuids.Any() ? Option.Some(selfPuids.First()) : Option.None, internalId: "OwnedSession".ToIdentifier(), maxPlayers: serverSettings.MaxPlayers), + t => + { + LeaveSession(); + if (!t.TryGetResult(out Result? result)) { return; } + if (!result.TryUnwrapSuccess(out var newOwnedSession)) + { + if (result.TryUnwrapFailure(out var error) && + error is EosInterface.Sessions.CreateError.SessionAlreadyExists) + { + // If the session already exists then this failure is not a problem + return; + } + DebugConsole.ThrowError($"Failed to create session: {result}"); + return; + } + CurrentOwnedSession = Option.Some(newOwnedSession); + UpdateOwnedSession(endpoint, serverSettings); + }); + } + return; + } + + if (selfPuids.Length > 0) + { + endpoint = Option.Some(new EosP2PEndpoint(selfPuids.First())); + } + ownedSession.HostAddress = endpoint.Select(e1 => e1.StringRepresentation); + if (endpoint.TryUnwrap(out var e2) && e2 is LidgrenEndpoint { Port: var port }) + { + SetAttributeValue("Port".ToIdentifier(), port.ToString()); + } + else if (serverSettings.Port != 0) + { + SetAttributeValue("Port".ToIdentifier(), serverSettings.Port.ToString()); + } + + if (SteamManager.GetSteamId().TryUnwrap(out var steamId)) + { + SetAttributeValue("SteamP2PEndpoint".ToIdentifier(), steamId.StringRepresentation); + } + + serverSettings.UpdateServerListInfo(SetAttributeValue); + TaskPool.Add( + "UpdateOwnedSessionAttributes", + ownedSession.UpdateAttributes(), + t => + { + if (!t.TryGetResult(out Result? result)) { return; } + DebugConsole.Log($"EOS UpdateOwnedSessionAttributes result: {result}"); + }); + + + void SetAttributeValue(Identifier attributeKey, object value) + { + string valueStr = value.ToString() ?? ""; + + if (attributeKey == "contentpackages" && value is IEnumerable contentPackages) + { + int contentPackageIndex = 0; + foreach (var contentPackage in contentPackages) + { + ownedSession.Attributes[$"contentpackage{contentPackageIndex}".ToIdentifier()] + = new ServerListContentPackageInfo(contentPackage).ToString(); + contentPackageIndex++; + } + } + else + { + ownedSession.Attributes[attributeKey] = valueStr; + } + } + + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs index b68a159dc..a21bc4cf2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs @@ -465,17 +465,17 @@ namespace Barotrauma public float GetCommonness(Level level) { - if (level.GenerationParams?.Identifier != null && + if (level.GenerationParams?.Identifier is { IsEmpty: false } && OverrideCommonness.TryGetValue(level.GenerationParams.Identifier, out float generationParamsCommonness)) { return generationParamsCommonness; } - else if (level.StartOutpost?.Info.OutpostGenerationParams?.Identifier != null && + else if (level.StartOutpost?.Info.OutpostGenerationParams?.Identifier is { IsEmpty: false } && OverrideCommonness.TryGetValue(level.StartOutpost.Info.OutpostGenerationParams.Identifier, out float startOutpostParamsCommonness)) { return startOutpostParamsCommonness; } - else if (level.EndOutpost?.Info.OutpostGenerationParams?.Identifier != null && + else if (level.EndOutpost?.Info.OutpostGenerationParams?.Identifier is { IsEmpty: false } && OverrideCommonness.TryGetValue(level.EndOutpost.Info.OutpostGenerationParams.Identifier, out float endOutpostParamsCommonness)) { return endOutpostParamsCommonness; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs index 6453c4d52..4a12a67c6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs @@ -185,27 +185,6 @@ namespace Barotrauma.Extensions if (value != null) { source.Add(value); } } - public static ImmutableDictionary ToImmutableDictionary(this IEnumerable<(TKey, TValue)> enumerable) - { - return enumerable.ToDictionary().ToImmutableDictionary(); - } - - public static Dictionary ToDictionary(this IEnumerable<(TKey, TValue)> enumerable) - { - var dictionary = new Dictionary(); - foreach (var (k,v) in enumerable) - { - dictionary.Add(k, v); - } - return dictionary; - } - - public static Dictionary ToMutable(this ImmutableDictionary immutableDictionary) - { - if (immutableDictionary == null) { return null; } - return new Dictionary(immutableDictionary); - } - public static NetCollection ToNetCollection(this IEnumerable enumerable) => new NetCollection(enumerable.ToImmutableArray()); /// @@ -236,87 +215,5 @@ namespace Barotrauma.Extensions public static IReadOnlyList ListConcat(this IEnumerable self, IEnumerable other) => new ListConcat(self, other); - - /// - /// Returns the maximum element in a given enumerable, or null if there - /// aren't any elements in the input. - /// - /// Input collection - /// Maximum element or null - public static T? MaxOrNull(this IEnumerable enumerable) where T : struct, IComparable - { - T? retVal = null; - foreach (T v in enumerable) - { - if (!retVal.HasValue || v.CompareTo(retVal.Value) > 0) { retVal = v; } - } - return retVal; - } - - public static TOut? MaxOrNull(this IEnumerable enumerable, Func conversion) - where TOut : struct, IComparable - => enumerable.Select(conversion).MaxOrNull(); - - public static int FindIndex(this IReadOnlyList list, Predicate predicate) - { - for (int i=0; i - /// Same as FirstOrDefault but will always return null instead of default(T) when no element is found - /// - public static T? FirstOrNull(this IEnumerable source, Func predicate) where T : struct - { - if (source.FirstOrDefault(predicate) is var first && !first.Equals(default(T))) - { - return first; - } - - return null; - } - - public static T? FirstOrNull(this IEnumerable source) where T : struct - { - if (source.FirstOrDefault() is var first && !first.Equals(default(T))) - { - return first; - } - - return null; - } - - public static IEnumerable NotNull(this IEnumerable source) where T : struct - => source - .Where(nullable => nullable.HasValue) - .Select(nullable => nullable.Value); - - public static IEnumerable NotNull(this IEnumerable source) where T : class - => source - .Where(nullable => nullable != null) - .Select(nullable => nullable!); - - public static IEnumerable NotNone(this IEnumerable> source) - { - foreach (var o in source) - { - if (o.TryUnwrap(out var v)) { yield return v; } - } - } - - public static IEnumerable Successes( - this IEnumerable> source) - => source - .OfType>() - .Select(s => s.Value); - - public static IEnumerable Failures( - this IEnumerable> source) - => source - .OfType>() - .Select(f => f.Error); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringExtensions.cs index c6c3e3a92..9e72f54f9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringExtensions.cs @@ -6,25 +6,13 @@ namespace Barotrauma { static class StringExtensions { - public static string FallbackNullOrEmpty(this string s, string fallback) => string.IsNullOrEmpty(s) ? fallback : s; - - public static bool IsNullOrEmpty([NotNullWhen(returnValue: false)]this string? s) => string.IsNullOrEmpty(s); - public static bool IsNullOrWhiteSpace([NotNullWhen(returnValue: false)]this string? s) => string.IsNullOrWhiteSpace(s); + public static bool IsNullOrEmpty([NotNullWhen(returnValue: false)]this ContentPath? p) => p?.IsPathNullOrEmpty() ?? true; public static bool IsNullOrWhiteSpace([NotNullWhen(returnValue: false)]this ContentPath? p) => p?.IsPathNullOrWhiteSpace() ?? true; + public static bool IsNullOrEmpty([NotNullWhen(returnValue: false)]this LocalizedString? s) => s is null || string.IsNullOrEmpty(s.Value); public static bool IsNullOrWhiteSpace([NotNullWhen(returnValue: false)]this LocalizedString? s) => s is null || string.IsNullOrWhiteSpace(s.Value); public static bool IsNullOrEmpty([NotNullWhen(returnValue: false)]this RichString? s) => s is null || s.NestedStr.IsNullOrEmpty(); public static bool IsNullOrWhiteSpace([NotNullWhen(returnValue: false)]this RichString? s) => s is null || s.NestedStr.IsNullOrWhiteSpace(); - - public static string RemoveFromEnd(this string s, string substr, StringComparison stringComparison = StringComparison.Ordinal) - => s.EndsWith(substr, stringComparison) ? s.Substring(0, s.Length - substr.Length) : s; - - public static bool IsTrueString(this string s) - => s.Length == 4 - && s[0] is 'T' or 't' - && s[1] is 'R' or 'r' - && s[2] is 'U' or 'u' - && s[3] is 'E' or 'e'; } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs index a6ba4d40d..fe540c172 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs @@ -1,7 +1,8 @@ -#nullable enable +#nullable enable using Barotrauma.Steam; using RestSharp; using System; +using System.Linq; using System.Net; using System.Threading.Tasks; @@ -13,7 +14,7 @@ namespace Barotrauma /// The protocol used to communicate with the remote consent server may change. /// This number tells the server which version the game is using so we can implement backwards-compatibility. /// - private const string RemoteRequestVersion = "2"; + private const string RemoteRequestVersion = "3"; public enum Consent { @@ -47,19 +48,66 @@ namespace Barotrauma public static bool SendUserStatistics => UserConsented == Consent.Yes && loadedImplementation != null; - private static bool consentTextAvailable + private static bool ConsentTextAvailable => TextManager.ContainsTag("statisticsconsentheader") && TextManager.ContainsTag("statisticsconsenttext"); private const string consentServerUrl = "https://barotraumagame.com/baromaster/"; private const string consentServerFile = "consentserver.php"; - private static async Task GetAuthTicket() + enum Platform { - var ticketOption = await SteamManager.GetAuthTicketForGameAnalyticsConsent(); - if (!ticketOption.TryUnwrap(out var authTicket) || authTicket.Data is null) { return ""; } - //convert byte array to hex - return BitConverter.ToString(authTicket.Data).Replace("-", ""); + Steam, + EOS, + None + } + + private class AuthTicket + { + public readonly string Token; + public readonly Platform Platform; + + public AuthTicket(string token, Platform platform) + { + Token = token ?? string.Empty; + Platform = platform; + } + } + + private static async Task GetAuthTicket() + { + if (SteamManager.IsInitialized) + { + return await GetSteamAuthTicket(); + } + else if (EosInterface.IdQueries.IsLoggedIntoEosConnect) + { + return await GetEOSAuthTicket(); + } + return new AuthTicket(string.Empty, Platform.None); + } + + private static async Task GetSteamAuthTicket() + { + var authTicket = await SteamManager.GetAuthTicketForGameAnalyticsConsent(); + return authTicket.TryUnwrap(out var ticketUnwrapped) && ticketUnwrapped.Data is { Length: > 0 } + ? new AuthTicket(ToolBoxCore.ByteArrayToHexString(ticketUnwrapped.Data), Platform.Steam) //convert byte array to hex + : throw new Exception("Could not retrieve Steamworks authentication ticket for GameAnalytics"); + } + + private static async Task GetEOSAuthTicket() + { + var puid = EosInterface.IdQueries.GetLoggedInPuids().First(); + var tokenResult = EosInterface.EosIdToken.FromProductUserId(puid); + if (tokenResult.TryUnwrapFailure(out var error)) + { + throw new Exception($"Could not retrieve EOS authentication ticket for GameAnalytics. {error}"); + } + else if (tokenResult.TryUnwrapSuccess(out var token)) + { + return new AuthTicket(token.JsonWebToken.ToString(), Platform.EOS); + } + throw new UnreachableCodeException(); } /// @@ -92,7 +140,9 @@ namespace Barotrauma if (consent == Consent.Ask) { - CreateConsentPrompt(); +#if CLIENT + GameMain.ExecuteAfterContentFinishedLoading(CreateConsentPrompt); +#endif } if (consent != Consent.No && consent != Consent.Yes) @@ -129,20 +179,24 @@ namespace Barotrauma /// private static async Task SendAnswerToRemoteDatabase(Consent consent) { - string authTicketStr; + AuthTicket authTicket; try { - authTicketStr = await GetAuthTicket(); + authTicket = await GetAuthTicket(); } catch (Exception e) { - DebugConsole.ThrowError("Error in GameAnalyticsManager.SetConsent. Could not get a Steam authentication ticket.", e); + DebugConsole.ThrowError($"Error in {nameof(GameAnalyticsManager)}.{nameof(SendAnswerToRemoteDatabase)}. Could not get an authentication ticket.", e); return false; } - - if (string.IsNullOrEmpty(authTicketStr)) + if (authTicket.Platform == Platform.None) { - DebugConsole.ThrowError("Error in GameAnalyticsManager.SetContent. Steam authentication ticket was empty."); + DebugConsole.AddWarning($"Error in {nameof(GameAnalyticsManager)}.{nameof(SendAnswerToRemoteDatabase)}. Not logged in to any platform."); + return false; + } + if (string.IsNullOrEmpty(authTicket.Token)) + { + DebugConsole.ThrowError($"Error in {nameof(GameAnalyticsManager)}.{nameof(SendAnswerToRemoteDatabase)}. {authTicket.Platform} authentication ticket was empty."); return false; } @@ -152,10 +206,18 @@ namespace Barotrauma var client = new RestClient(consentServerUrl); var request = new RestRequest(consentServerFile, Method.GET); - request.AddParameter("authticket", authTicketStr); - request.AddParameter("action", "setconsent"); - request.AddParameter("consent", consent == Consent.Yes ? 1 : 0); + request.AddParameter("authticket", authTicket.Token); + if (consent == Consent.Ask) + { + request.AddParameter("action", "resetconsent"); + } + else + { + request.AddParameter("action", "setconsent"); + request.AddParameter("consent", consent == Consent.Yes ? 1 : 0); + } request.AddParameter("request_version", RemoteRequestVersion); + request.AddParameter("platform", authTicket.Platform); response = await client.ExecuteAsync(request, Method.GET); } @@ -175,6 +237,18 @@ namespace Barotrauma return true; } + public static void ResetConsent() + { + TaskPool.Add( + "GameAnalyticsConsent.ResetConsentInternal", + SendAnswerToRemoteDatabase(Consent.Ask), + t => + { + if (!t.TryGetResult(out bool success) || !success) { return; } + DebugConsole.NewMessage("Reset GameAnalytics consent."); + }); + } + static partial void CreateConsentPrompt(); public static void InitIfConsented() @@ -183,15 +257,15 @@ namespace Barotrauma return; #endif - if (!consentTextAvailable) + if (!ConsentTextAvailable) { SetConsent(Consent.Unknown); return; } - if (!SteamManager.IsInitialized) + if (!SteamManager.IsInitialized && EosInterface.IdQueries.GetLoggedInPuids() is not { Length: > 0 }) { - DebugConsole.AddWarning("Error in GameAnalyticsManager.GetConsent: Could not get a Steam authentication ticket (not connected to Steam)."); + DebugConsole.AddWarning("Error in GameAnalyticsManager.GetConsent: Could not get a Steam or EOS authentication ticket (not connected to Steam or EOS)."); SetConsent(Consent.Error); return; } @@ -208,20 +282,25 @@ namespace Barotrauma private static async Task RequestAnswerFromRemoteDatabase() { - static void error(string reason, Exception exception) + static void error(string reason, Exception? exception) { - DebugConsole.ThrowError($"Error in GameAnalyticsManager.GetConsent: {reason}", exception); + DebugConsole.ThrowError($"Error in {nameof(GameAnalyticsManager)}.{nameof(RequestAnswerFromRemoteDatabase)}: {reason}", exception); SetConsent(Consent.Error); } - string authTicketStr; + AuthTicket authTicket; try { - authTicketStr = await GetAuthTicket(); + authTicket = await GetAuthTicket(); } catch (Exception e) { - error("Could not get a Steam authentication ticket.", e); + error("Could not get an authentication ticket.", e); + return Consent.Error; + } + if (authTicket.Platform == Platform.None) + { + error($"Could not get an authentication ticket. Not logged in to any platform.", exception: null); return Consent.Error; } @@ -237,9 +316,10 @@ namespace Barotrauma } var request = new RestRequest(consentServerFile, Method.GET); - request.AddParameter("authticket", authTicketStr); + request.AddParameter("authticket", authTicket.Token); request.AddParameter("action", "getconsent"); request.AddParameter("request_version", RemoteRequestVersion); + request.AddParameter("platform", authTicket.Platform); IRestResponse response; try diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs index fbb52eaea..27ff2c57e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs @@ -1,5 +1,6 @@ #nullable enable using Barotrauma.IO; +using Barotrauma.Steam; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -51,6 +52,13 @@ namespace Barotrauma Difficulty90to100, } + public enum CustomDimensions03 + { + UnknownPlatform, + Steam, + EGS + } + public enum ResourceCurrency { Money @@ -149,6 +157,14 @@ namespace Barotrauma internal void ConfigureAvailableResourceCurrencies(params ResourceCurrency[] customDimensions) => configureAvailableResourceCurrencies(customDimensions.Select(d => d.ToString()).ToArray()); + private readonly Action configureAvailableCustomDimensions03; + internal void ConfigureAvailableCustomDimensions03(params CustomDimensions03[] customDimensions) + => configureAvailableCustomDimensions03(customDimensions.Select(d => d.ToString()).ToArray()); + + private readonly Action setCustomDimension03; + internal void SetCustomDimension03(string dimension03) + => setCustomDimension03(dimension03); + private readonly Action configureAvailableResourceItemTypes; internal void ConfigureAvailableResourceItemTypes(params string[] resourceItemTypes) => configureAvailableResourceItemTypes(resourceItemTypes); @@ -238,9 +254,9 @@ namespace Barotrauma { if (resolvingDependency) { return null; } resolvingDependency = true; - Assembly dep = context.LoadFromAssemblyPath(GetAssemblyPath(dependencyName.Name ?? throw new Exception("Dependency name was null"))); + Assembly dependency = context.LoadFromAssemblyPath(GetAssemblyPath(dependencyName.Name ?? throw new Exception("Dependency name was null"))); resolvingDependency = false; - return dep; + return dependency; } internal Implementation() @@ -297,7 +313,10 @@ namespace Barotrauma new Type[] { typeof(string) })); configureAvailableCustomDimensions02 = Call(getMethod(nameof(ConfigureAvailableCustomDimensions02), new Type[] { typeof(string[]) })); - + configureAvailableCustomDimensions03 = Call(getMethod(nameof(ConfigureAvailableCustomDimensions03), + new Type[] { typeof(string[]) })); + setCustomDimension03 = Call(getMethod(nameof(SetCustomDimension03), + new Type[] { typeof(string) })); configureAvailableResourceCurrencies = Call(getMethod(nameof(ConfigureAvailableResourceCurrencies), new Type[] { typeof(string[]) })); configureAvailableResourceItemTypes = Call(getMethod(nameof(ConfigureAvailableResourceItemTypes), @@ -424,6 +443,12 @@ namespace Barotrauma loadedImplementation?.SetCustomDimension01(dimension.ToString()); } + public static void SetCustomDimension03(CustomDimensions03 dimension) + { + if (!SendUserStatistics) { return; } + loadedImplementation?.SetCustomDimension03(dimension.ToString()); + } + public static void SetCurrentLevel(LevelData levelData) { if (!SendUserStatistics) { return; } @@ -509,6 +534,7 @@ namespace Barotrauma + buildConfiguration); loadedImplementation?.ConfigureAvailableCustomDimensions01(Enum.GetValues(typeof(CustomDimensions01)).Cast().ToArray()); loadedImplementation?.ConfigureAvailableCustomDimensions02(Enum.GetValues(typeof(CustomDimensions02)).Cast().ToArray()); + loadedImplementation?.ConfigureAvailableCustomDimensions03(Enum.GetValues(typeof(CustomDimensions03)).Cast().ToArray()); loadedImplementation?.ConfigureAvailableResourceCurrencies(Enum.GetValues(typeof(ResourceCurrency)).Cast().ToArray()); loadedImplementation?.ConfigureAvailableResourceItemTypes( Enum.GetValues(typeof(MoneySink)).Cast().Select(s => s.ToString()).Union(Enum.GetValues(typeof(MoneySource)).Cast().Select(s => s.ToString())).ToArray()); @@ -521,6 +547,19 @@ namespace Barotrauma + (exeHash?.ShortRepresentation ?? "Unknown") + ":" + AssemblyInfo.GitRevision + ":" + buildConfiguration); + + SetCustomDimension01(ContentPackageManager.ModsEnabled ? CustomDimensions01.Modded : CustomDimensions01.Vanilla); + + CustomDimensions03 platform = CustomDimensions03.UnknownPlatform; + if (SteamManager.IsInitialized) + { + platform = CustomDimensions03.Steam; + } + else if (EosInterface.IdQueries.IsLoggedIntoEosConnect) + { + platform = CustomDimensions03.EGS; + } + SetCustomDimension03(platform); } catch (Exception e) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs index b8c680577..d3167c3e2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs @@ -32,7 +32,7 @@ namespace Barotrauma continue; } - Type? type = Type.GetType(valueType); + Type? type = ReflectionUtils.GetType(valueType); if (type == null) { @@ -62,7 +62,7 @@ namespace Barotrauma { DebugConsole.Log($"Set the value \"{identifier}\" to {value}"); - SteamAchievementManager.OnCampaignMetadataSet(identifier, value, unlockClients: true); + AchievementManager.OnCampaignMetadataSet(identifier, value, unlockClients: true); if (!data.ContainsKey(identifier)) { @@ -135,7 +135,7 @@ namespace Barotrauma element.Add(new XElement("Data", new XAttribute("key", key), new XAttribute("value", valueStr), - new XAttribute("type", value.GetType()))); + new XAttribute("type", value.GetType().FullName ?? ""))); } #if DEBUG DebugConsole.Log(element.ToString()); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index a3bce5575..e3f4b4ade 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -156,17 +156,15 @@ namespace Barotrauma if (CheatsEnabled) { DebugConsole.CheatsEnabled = true; -#if USE_STEAM - if (!SteamAchievementManager.CheatsEnabled) + if (!AchievementManager.CheatsEnabled) { - SteamAchievementManager.CheatsEnabled = true; + AchievementManager.CheatsEnabled = true; #if CLIENT - new GUIMessageBox("Cheats enabled", "Cheat commands have been enabled on the server. You will not receive Steam Achievements until you restart the game."); + new GUIMessageBox("Cheats enabled", "Cheat commands have been enabled on the server. You will not receive achievements until you restart the game."); #else DebugConsole.NewMessage("Cheat commands have been enabled.", Color.Red); #endif } -#endif } foreach (var subElement in element.Elements()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 38a259fd5..aa06066e2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -551,7 +551,7 @@ namespace Barotrauma } #endif #if CLIENT - if (campaignMode != null && levelData != null) { SteamAchievementManager.OnBiomeDiscovered(levelData.Biome); } + if (campaignMode != null && levelData != null) { AchievementManager.OnBiomeDiscovered(levelData.Biome); } var existingRoundSummary = GUIMessageBox.MessageBoxes.Find(mb => mb.UserData is RoundSummary)?.UserData as RoundSummary; if (existingRoundSummary?.ContinueButton != null) @@ -653,7 +653,7 @@ namespace Barotrauma ObjectiveManager.ResetObjectives(); #endif EventManager?.StartRound(Level.Loaded); - SteamAchievementManager.OnStartRound(); + AchievementManager.OnStartRound(); GameMode.ShowStartMessage(); @@ -936,7 +936,7 @@ namespace Barotrauma ObjectiveManager.ResetUI(); CharacterHUD.ClearBossProgressBars(); #endif - SteamAchievementManager.OnRoundEnded(this); + AchievementManager.OnRoundEnded(this); #if SERVER GameMain.Server?.TraitorManager?.EndRound(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index 873325957..48a127a8d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -497,7 +497,7 @@ namespace Barotrauma.Items.Components { CurrentFixer.Info?.ApplySkillGain(skill.Identifier, SkillSettings.Current.SkillIncreasePerRepair); } - SteamAchievementManager.OnItemRepaired(item, CurrentFixer); + AchievementManager.OnItemRepaired(item, CurrentFixer); CurrentFixer.CheckTalents(AbilityEffectType.OnRepairComplete, new AbilityRepairable(item)); } if (CurrentFixer?.SelectedItem == item) { CurrentFixer.SelectedItem = null; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ColorComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ColorComponent.cs index fb610a6ce..b9b404fdc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ColorComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ColorComponent.cs @@ -35,7 +35,7 @@ namespace Barotrauma.Items.Components if (UseHSV) { - Color hsvColor = ToolBox.HSVToRGB(signalR, signalG, signalB); + Color hsvColor = ToolBoxCore.HSVToRGB(signalR, signalG, signalB); signalR = hsvColor.R; signalG = hsvColor.G; signalB = hsvColor.B; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index 447f3b41d..d9a727c97 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -150,7 +150,7 @@ namespace Barotrauma { ItemPrefabIdentifier = itemPrefab; using MD5 md5 = MD5.Create(); - UintIdentifier = ToolBox.IdentifierToUint32Hash(itemPrefab, md5); + UintIdentifier = ToolBoxCore.IdentifierToUint32Hash(itemPrefab, md5); } public override string ToString() @@ -197,7 +197,7 @@ namespace Barotrauma { Tag = tag; using MD5 md5 = MD5.Create(); - UintIdentifier = ToolBox.IdentifierToUint32Hash(tag, md5); + UintIdentifier = ToolBoxCore.IdentifierToUint32Hash(tag, md5); } public override string ToString() @@ -349,7 +349,7 @@ namespace Barotrauma private uint GenerateHash() { using var md5 = MD5.Create(); - uint outputId = ToolBox.IdentifierToUint32Hash(TargetItemPrefabIdentifier, md5); + uint outputId = ToolBoxCore.IdentifierToUint32Hash(TargetItemPrefabIdentifier, md5); var requiredItems = string.Join(':', RequiredItems .Select(static i => $"{i.UintIdentifier}:{i.Amount}") @@ -358,7 +358,7 @@ namespace Barotrauma var requiredSkills = string.Join(':', RequiredSkills.Select(s => $"{s.Identifier}:{s.Level}")); - uint retVal = ToolBox.StringToUInt32Hash($"{Amount}|{outputId}|{RequiredTime}|{RequiresRecipe}|{requiredItems}|{requiredSkills}", md5); + uint retVal = ToolBoxCore.StringToUInt32Hash($"{Amount}|{outputId}|{RequiredTime}|{RequiresRecipe}|{requiredItems}|{requiredSkills}", md5); if (retVal == 0) { retVal = 1; } return retVal; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 5db866ad8..592e70844 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -2046,7 +2046,8 @@ namespace Barotrauma caveStartPos.ToVector2(), caveEndPos.ToVector2(), iterations: 3, offsetAmount: Vector2.Distance(caveStartPos.ToVector2(), caveEndPos.ToVector2()) * 0.75f, - bounds: caveArea); + bounds: caveArea, + rng: Rand.GetRNG(Rand.RandSync.ServerAndClient)); if (!caveSegments.Any()) { return; } @@ -2066,7 +2067,8 @@ namespace Barotrauma branchStartPos, branchEndPos, iterations: 3, offsetAmount: Vector2.Distance(branchStartPos, branchEndPos) * 0.75f, - bounds: caveArea); + bounds: caveArea, + rng: Rand.GetRNG(Rand.RandSync.ServerAndClient)); if (!branchSegments.Any()) { continue; } var branch = new Tunnel(TunnelType.Cave, SegmentsToNodes(branchSegments), 150, parentBranch); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs index 9f7dafe82..909aa78ba 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Concurrent; -using System.Linq; +using System.Collections.Generic; +using System.Diagnostics; using System.Text; using System.Threading; using System.Threading.Tasks; -using Barotrauma.Extensions; #if SERVER using PipeType = System.IO.Pipes.AnonymousPipeClientStream; @@ -121,6 +121,7 @@ namespace Barotrauma.Networking readCancellationToken?.Cancel(); return Option.None(); } + try { if (readTask.IsCompleted || readTask.Wait(timeOutMilliseconds, readCancellationToken.Token)) @@ -327,7 +328,36 @@ namespace Barotrauma.Networking writeManualResetEvent.Set(); } - public static bool Read(out byte[] msg) + private static readonly Stopwatch stopwatch = new Stopwatch(); + private const int MaxMilliseconds = 8; + + public static IEnumerable Read() + { + stopwatch.Restart(); + + // To avoid the stopwatch somehow experiencing magical overhead that makes it not even + // start the loop within 8ms, use this bool to force at least one iteration. + bool hasIteratedAtLeastOnce = false; + + // If it's taken more than 8 milliseconds to read dequeued messages, take + // a break from reading and allow all of the other logic to run for a tick. + // Otherwise the server may overwhelm the host client with redundant messages + // that are being read too slowly. + while (!hasIteratedAtLeastOnce || stopwatch.ElapsedMilliseconds < MaxMilliseconds) + { + hasIteratedAtLeastOnce = true; + if (!ReadSingleMessage(out var msg)) + { + // No more messages available to dequeue, we don't need + // to reach 8 milliseconds to know we're done here + break; + } + yield return msg; + } + stopwatch.Stop(); + } + + private static bool ReadSingleMessage(out byte[] msg) { if (HasShutDown) { msg = null; return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs index 4c9adeb13..7432c97cf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -65,6 +65,7 @@ namespace Barotrauma.Networking PERMISSIONS, //tell the client which special permissions they have (if any) ACHIEVEMENT, //give the client a steam achievement + ACHIEVEMENT_STAT, //increment stat for an achievement CHEATS_ENABLED, //tell the clients whether cheats are on or off CAMPAIGN_SETUP_INFO, @@ -147,7 +148,7 @@ namespace Barotrauma.Networking ServerCrashed, ServerFull, AuthenticationRequired, - SteamAuthenticationFailed, + AuthenticationFailed, SessionTaken, TooManyFailedLogins, InvalidName, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/AccountInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/AccountInfo.cs index 608486f0a..5ef184891 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/AccountInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/AccountInfo.cs @@ -18,15 +18,16 @@ namespace Barotrauma.Networking /// Other user IDs that this user might be closely tied to, /// such as the owner of the current copy of Barotrauma /// - #warning TODO: make ImmutableArray once feature/inetserializablestruct-improvements gets merged to dev - public readonly AccountId[] OtherMatchingIds; + public readonly ImmutableArray OtherMatchingIds; + + public bool IsNone => AccountId.IsNone() && OtherMatchingIds.Length == 0; public AccountInfo(AccountId accountId, params AccountId[] otherIds) : this(Option.Some(accountId), otherIds) { } public AccountInfo(Option accountId, params AccountId[] otherIds) { AccountId = accountId; - OtherMatchingIds = otherIds.Where(id => !accountId.ValueEquals(id)).ToArray(); + OtherMatchingIds = otherIds.Where(id => !accountId.ValueEquals(id)).ToImmutableArray(); } public bool Matches(AccountId accountId) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Auth/AuthenticationTicket.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Auth/AuthenticationTicket.cs new file mode 100644 index 000000000..656681388 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Auth/AuthenticationTicket.cs @@ -0,0 +1,65 @@ +#nullable enable +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Barotrauma.Extensions; +using Barotrauma.Steam; + +namespace Barotrauma.Networking; + +public enum AuthenticationTicketKind +{ + SteamAuthTicketForSteamHost = 0, + SteamAuthTicketForEosHost = 1, + EgsOwnershipToken = 2 +} + +[NetworkSerialize(ArrayMaxSize = UInt16.MaxValue)] +readonly record struct AuthenticationTicket( + AuthenticationTicketKind Kind, + ImmutableArray Data) : INetSerializableStruct +{ + public static async Task> Create(Endpoint serverEndpoint) + { + if (SteamManager.IsInitialized && SteamManager.GetSteamId().TryUnwrap(out var steamId)) + { + if (serverEndpoint is EosP2PEndpoint) + { + var authTicket = await SteamManager.GetAuthTicketForEosHostAuth(); + return authTicket + .Bind(t => t.Data != null ? Option.Some(t.Data) : Option.None) + .Select(data => new AuthenticationTicket(AuthenticationTicketKind.SteamAuthTicketForEosHost, data.ToImmutableArray())); + } + else + { + var authTicket = SteamManager.GetAuthSessionTicketForSteamHost(serverEndpoint); + var steamIdBytes = BitConverter.GetBytes(steamId.Value); + return authTicket + .Bind(t => t.Data != null ? Option.Some(t.Data) : Option.None) + .Select(data => new AuthenticationTicket( + AuthenticationTicketKind.SteamAuthTicketForSteamHost, + steamIdBytes.Concat(data).ToImmutableArray())); + } + } + + if (EosInterface.IdQueries.GetLoggedInPuids() is { Length: > 0 } puids) + { + var externalAccountIdsResult = await EosInterface.IdQueries.GetSelfExternalAccountIds(puids[0]); + if (externalAccountIdsResult.TryUnwrapSuccess(out var externalAccountIds)) + { + var epicAccountIdOption = externalAccountIds.OfType().FirstOrNone(); + if (epicAccountIdOption.TryUnwrap(out var epicAccountId)) + { + return (await EosInterface.Ownership.GetGameOwnershipToken(epicAccountId)) + .Select(t => new AuthenticationTicket( + AuthenticationTicketKind.EgsOwnershipToken, + Encoding.UTF8.GetBytes(t.Jwt.ToString()).ToImmutableArray())); + } + } + } + + return Option.None; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Auth/Authenticator.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Auth/Authenticator.cs new file mode 100644 index 000000000..3f9c92729 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Auth/Authenticator.cs @@ -0,0 +1,38 @@ +#nullable enable +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading.Tasks; +using Barotrauma.Steam; + +namespace Barotrauma.Networking; + +abstract class Authenticator +{ + public abstract Task VerifyTicket(AuthenticationTicket ticket); + public abstract void EndAuthSession(AccountId accountId); + + public static ImmutableDictionary GetAuthenticatorsForHost(Option ownerEndpointOption) + { + var authenticators = new Dictionary(); + + if (EosInterface.Core.IsInitialized) + { + // Every kind of host should be able to do EOS ID Token authentication if they have EOS enabled + authenticators.Add(AuthenticationTicketKind.EgsOwnershipToken, new EgsOwnershipTokenAuthenticator()); + + if (ownerEndpointOption.TryUnwrap(out var ownerEndpoint) && ownerEndpoint is EosP2PEndpoint) + { + // EOS P2P hosts do not have access to Steamworks + authenticators.Add(AuthenticationTicketKind.SteamAuthTicketForEosHost, new SteamAuthTicketForEosHostAuthenticator()); + } + } + + if (!(ownerEndpointOption.TryUnwrap(out var ownerEndpoint2) && ownerEndpoint2 is EosP2PEndpoint) && SteamManager.IsInitialized) + { + // Steam P2P hosts and dedicated servers have access to Steamworks + authenticators.Add(AuthenticationTicketKind.SteamAuthTicketForSteamHost, new SteamAuthTicketForSteamHostAuthenticator()); + } + + return authenticators.ToImmutableDictionary(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Auth/EgsOwnershipTokenAuthenticator.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Auth/EgsOwnershipTokenAuthenticator.cs new file mode 100644 index 000000000..e3e2201a0 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Auth/EgsOwnershipTokenAuthenticator.cs @@ -0,0 +1,21 @@ +using System.Text; +using System.Threading.Tasks; + +namespace Barotrauma.Networking; + +sealed class EgsOwnershipTokenAuthenticator : Authenticator +{ + public override async Task VerifyTicket(AuthenticationTicket ticket) + { + var jwtOption = JsonWebToken.Parse(Encoding.UTF8.GetString(ticket.Data.AsSpan())); + + if (!jwtOption.TryUnwrap(out var jwt)) { return AccountInfo.None; } + var ownershipToken = new EosInterface.Ownership.Token(jwt); + var accountIdOption = await ownershipToken.Verify(); + + if (!accountIdOption.TryUnwrap(out var accountId)) { return AccountInfo.None; } + return new AccountInfo(accountId); + } + + public override void EndAuthSession(AccountId accountId) { /* do nothing */ } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Auth/SteamAuthTicketForEosHostAuthenticator.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Auth/SteamAuthTicketForEosHostAuthenticator.cs new file mode 100644 index 000000000..1202e1052 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Auth/SteamAuthTicketForEosHostAuthenticator.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Threading.Tasks; +using RestSharp; + +namespace Barotrauma.Networking; + +sealed class SteamAuthTicketForEosHostAuthenticator : Authenticator +{ + #warning FIXME change URL back to the non-experimental one once this passes QA + private const string consentServerUrl = "https://barotraumagame.com/baromaster/experimental/"; + private const string consentServerFile = "getOwnerSteamId.php"; + private const int RemoteRequestVersion = 1; + + public override async Task VerifyTicket(AuthenticationTicket ticket) + { + string ticketData = ToolBoxCore.ByteArrayToHexString(ticket.Data); + + var client = new RestClient(consentServerUrl); + + var request = new RestRequest(consentServerFile, Method.GET); + request.AddParameter("authticket", ticketData); + request.AddParameter("request_version", RemoteRequestVersion); + + var response = await client.ExecuteAsync(request, Method.GET); + if (!response.IsSuccessful) { return AccountInfo.None; } + + try + { + var jsonDoc = JsonDocument.Parse(response.Content); + Option steamId = Option.None; + Option ownerSteamId = Option.None; + foreach (var property in jsonDoc.RootElement.EnumerateObject()) + { + if (!property.Name.ToIdentifier().Contains("SteamId")) { continue; } + var accountIdOption = SteamId.Parse(property.Value.GetString() ?? ""); + if (accountIdOption.IsNone()) { continue; } + if (property.Name.ToIdentifier() == "SteamId") + { + steamId = accountIdOption; + } + else if (property.Name.ToIdentifier() == "OwnerSteamId") + { + ownerSteamId = accountIdOption; + } + } + var otherIds = ownerSteamId.TryUnwrap(out var id) ? new AccountId[] { id } : Array.Empty(); + return new AccountInfo(steamId.Select(static id => (AccountId)id), otherIds); + } + catch + { + return AccountInfo.None; + } + } + + public override void EndAuthSession(AccountId accountId) { /* do nothing */ } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Auth/SteamAuthTicketForSteamHostAuthenticator.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Auth/SteamAuthTicketForSteamHostAuthenticator.cs new file mode 100644 index 000000000..2c885f215 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Auth/SteamAuthTicketForSteamHostAuthenticator.cs @@ -0,0 +1,77 @@ +#nullable enable +using System; +using System.Linq; +using System.Threading.Tasks; +using Barotrauma.Steam; + +namespace Barotrauma.Networking; + +#if CLIENT +using SteamAuthSessionInterface = Steamworks.SteamUser; +#else +using SteamAuthSessionInterface = Steamworks.SteamServer; +#endif + +sealed class SteamAuthTicketForSteamHostAuthenticator : Authenticator +{ + private static Steamworks.BeginAuthResult BeginAuthSession(Steamworks.AuthTicket authTicket, SteamId clientSteamId) + { + if (!SteamManager.IsInitialized) { return Steamworks.BeginAuthResult.ServerNotConnectedToSteam; } + if (authTicket.Data is null) { return Steamworks.BeginAuthResult.InvalidTicket; } + + DebugConsole.Log("Authenticating Steam client " + clientSteamId); + Steamworks.BeginAuthResult startResult = SteamAuthSessionInterface.BeginAuthSession(authTicket.Data, clientSteamId.Value); + if (startResult != Steamworks.BeginAuthResult.OK) + { + DebugConsole.Log($"Steam authentication failed: failed to start auth session ({startResult})"); + } + + return startResult; + } + + private static void EndAuthSession(SteamId clientSteamId) + { + if (!SteamManager.IsInitialized) { return; } + + DebugConsole.Log($"Ending auth session with Steam client {clientSteamId}"); + SteamAuthSessionInterface.EndAuthSession(clientSteamId.Value); + } + + public override async Task VerifyTicket(AuthenticationTicket ticket) + { + if (ticket.Data.Length < 8) { return AccountInfo.None; } + + var ticketData = ticket.Data.ToArray(); + var steamAuthTicket = new Steamworks.AuthTicket { Data = ticketData[8..] }; + var steamId = new SteamId(BitConverter.ToUInt64(ticketData.AsSpan()[..8])); + + using var janitor = Janitor.Start(); + + (Steamworks.AuthResponse AuthResponse, SteamId OwnerSteamId)? authResult = null; + void onValidateAuthTicketResponse(Steamworks.SteamId clientId, Steamworks.SteamId ownerClientId, Steamworks.AuthResponse response) + { + if (clientId != steamId.Value) { response = Steamworks.AuthResponse.AuthTicketInvalid; } + authResult = (response, new SteamId(ownerClientId)); + } + + SteamAuthSessionInterface.OnValidateAuthTicketResponse += onValidateAuthTicketResponse; + janitor.AddAction(() => SteamAuthSessionInterface.OnValidateAuthTicketResponse -= onValidateAuthTicketResponse); + var beginAuthSessionResult = BeginAuthSession(steamAuthTicket, steamId); + + if (beginAuthSessionResult != Steamworks.BeginAuthResult.OK) { return AccountInfo.None; } + + while (authResult is null) + { + await Task.Delay(32); + } + if (authResult.Value.AuthResponse != Steamworks.AuthResponse.OK) { return AccountInfo.None; } + + return new AccountInfo(steamId, authResult.Value.OwnerSteamId); + } + + public override void EndAuthSession(AccountId accountId) + { + if (accountId is not SteamId steamId) { return; } + SteamAuthSessionInterface.EndAuthSession(steamId.Value); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/EosP2PEndpoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/EosP2PEndpoint.cs new file mode 100644 index 000000000..a11503b25 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/EosP2PEndpoint.cs @@ -0,0 +1,32 @@ +#nullable enable + + +namespace Barotrauma.Networking; + +sealed class EosP2PEndpoint : P2PEndpoint +{ + public EosInterface.ProductUserId ProductUserId => new EosInterface.ProductUserId((Address as EosP2PAddress)!.EosStringRepresentation); + + public EosP2PEndpoint(EosInterface.ProductUserId puid) : this(new EosP2PAddress(puid.Value)) { } + + public EosP2PEndpoint(EosP2PAddress address) : base(address) { } + + public override string StringRepresentation => (Address as EosP2PAddress)!.StringRepresentation; + + public override LocalizedString ServerTypeString { get; } = TextManager.Get("PlayerHostedServer"); + + public override int GetHashCode() + => (Address as EosP2PAddress)!.GetHashCode(); + + public override bool Equals(object? obj) + => obj is EosP2PEndpoint otherEndpoint + && ProductUserId == otherEndpoint.ProductUserId; + + public new static Option Parse(string endpointStr) + => EosP2PAddress.Parse(endpointStr).Select(eosAddress => new EosP2PEndpoint(eosAddress)); + + public const string SocketName = "Barotrauma.EosP2PSocket"; + + public override P2PConnection MakeConnectionFromEndpoint() + => new EosP2PConnection(this); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/LidgrenEndpoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/LidgrenEndpoint.cs index 6fc7a7c8d..8b50a608c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/LidgrenEndpoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/LidgrenEndpoint.cs @@ -60,7 +60,7 @@ namespace Barotrauma.Networking => NetEndpoint.GetHashCode(); public static bool operator ==(LidgrenEndpoint a, LidgrenEndpoint b) - => a.Address.Equals(b.Address) && a.Port == b.Port; + => a.NetEndpoint.EquivalentTo(b.NetEndpoint); public static bool operator !=(LidgrenEndpoint a, LidgrenEndpoint b) => !(a == b); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/P2PEndpoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/P2PEndpoint.cs new file mode 100644 index 000000000..9955ceab9 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/P2PEndpoint.cs @@ -0,0 +1,12 @@ +#nullable enable +namespace Barotrauma.Networking; + +abstract class P2PEndpoint : Endpoint +{ + protected P2PEndpoint(P2PAddress address) : base(address) { } + + public abstract P2PConnection MakeConnectionFromEndpoint(); + + public new static Option Parse(string str) + => Endpoint.Parse(str).Bind(ep => ep is P2PEndpoint pep ? Option.Some(pep) : Option.None); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/SteamP2PEndpoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/SteamP2PEndpoint.cs index 93c6fd1da..85169f1bf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/SteamP2PEndpoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/SteamP2PEndpoint.cs @@ -2,36 +2,27 @@ namespace Barotrauma.Networking { - sealed class SteamP2PEndpoint : Endpoint + sealed class SteamP2PEndpoint : P2PEndpoint { - public readonly SteamId SteamId; + public SteamId SteamId => (Address as SteamP2PAddress)!.SteamId; public override string StringRepresentation => SteamId.StringRepresentation; - public override LocalizedString ServerTypeString { get; } = TextManager.Get("SteamP2PServer"); + public override LocalizedString ServerTypeString { get; } = TextManager.Get("PlayerHostedServer"); - public SteamP2PEndpoint(SteamId steamId) : base(new SteamP2PAddress(steamId)) - { - SteamId = steamId; - } - - public new static Option Parse(string endpointStr) - => SteamId.Parse(endpointStr).Select(steamId => new SteamP2PEndpoint(steamId)); - - public override bool Equals(object? obj) - => obj switch - { - SteamP2PEndpoint otherEndpoint => this == otherEndpoint, - _ => false - }; + public SteamP2PEndpoint(SteamId steamId) : base(new SteamP2PAddress(steamId)) { } public override int GetHashCode() => SteamId.GetHashCode(); - public static bool operator ==(SteamP2PEndpoint a, SteamP2PEndpoint b) - => a.SteamId == b.SteamId; + public override bool Equals(object? obj) + => obj is SteamP2PEndpoint otherEndpoint + && this.SteamId == otherEndpoint.SteamId; - public static bool operator !=(SteamP2PEndpoint a, SteamP2PEndpoint b) - => !(a == b); + public new static Option Parse(string endpointStr) + => SteamId.Parse(endpointStr).Select(steamId => new SteamP2PEndpoint(steamId)); + + public override P2PConnection MakeConnectionFromEndpoint() + => new SteamP2PConnection(this); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Fragmentation/MessageDefragmenter.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Fragmentation/MessageDefragmenter.cs new file mode 100644 index 000000000..b9d0ef514 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Fragmentation/MessageDefragmenter.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Barotrauma.Networking; + +sealed class MessageDefragmenter +{ + private readonly Dictionary partialMessages = new Dictionary(); + + public Option> ProcessIncomingFragment(MessageFragment fragment) + { + if (!partialMessages.ContainsKey(fragment.FragmentId.MessageId)) + { + partialMessages[fragment.FragmentId.MessageId] = new MessageFragment[fragment.FragmentId.FragmentCount]; + } + else if (partialMessages[fragment.FragmentId.MessageId].Length != fragment.FragmentId.FragmentCount) + { + DebugConsole.AddWarning($"Got a fragment for message {fragment.FragmentId.MessageId} " + + $"with a mismatched expected fragment count"); + return Option.None; + } + + var fragmentBuffer = partialMessages[fragment.FragmentId.MessageId]; + if (fragment.FragmentId.FragmentIndex >= fragmentBuffer.Length) + { + DebugConsole.AddWarning($"Got a fragment for message {fragment.FragmentId.MessageId} " + + $"with an index greater than or equal to the expected fragment count ({fragment.FragmentId.FragmentIndex} >= {fragmentBuffer.Length})"); + return Option.None; + } + + fragmentBuffer[fragment.FragmentId.FragmentIndex] = fragment; + if (fragmentBuffer.All(f => !f.Data.IsDefault && f.FragmentId.MessageId == fragment.FragmentId.MessageId)) + { + partialMessages.Remove(fragment.FragmentId.MessageId); + return Option.Some(fragmentBuffer.SelectMany(f => f.Data).ToImmutableArray()); + } + return Option.None; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Fragmentation/MessageFragment.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Fragmentation/MessageFragment.cs new file mode 100644 index 000000000..d4ccf9a2e --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Fragmentation/MessageFragment.cs @@ -0,0 +1,17 @@ +using System.Collections.Immutable; + +namespace Barotrauma.Networking; + +[NetworkSerialize(ArrayMaxSize = MaxSize)] +readonly record struct MessageFragment( + MessageFragment.Id FragmentId, + ImmutableArray Data) : INetSerializableStruct +{ + public const int MaxSize = 1100; + + [NetworkSerialize] + public readonly record struct Id( + ushort FragmentIndex, + ushort FragmentCount, + ushort MessageId) : INetSerializableStruct; +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Fragmentation/MessageFragmenter.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Fragmentation/MessageFragmenter.cs new file mode 100644 index 000000000..011b500e4 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Fragmentation/MessageFragmenter.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Barotrauma.Networking; + +sealed class MessageFragmenter +{ + private UInt16 nextFragmentedMessageId = 0; + private readonly List fragments = new List(); + + public ImmutableArray FragmentMessage(ReadOnlySpan bytes) + { + UInt16 msgId = nextFragmentedMessageId; + nextFragmentedMessageId++; + + int roundedByteCount = bytes.Length; + roundedByteCount += (MessageFragment.MaxSize - (roundedByteCount % MessageFragment.MaxSize)) % MessageFragment.MaxSize; + + int fragmentCount = roundedByteCount / MessageFragment.MaxSize; + fragments.Clear(); + fragments.EnsureCapacity(fragmentCount); + for (int i = 0; i < fragmentCount; i++) + { + var subset = bytes[(i * MessageFragment.MaxSize)..]; + if (subset.Length > MessageFragment.MaxSize) { subset = subset[..MessageFragment.MaxSize]; } + + fragments.Add(new MessageFragment( + FragmentId: new MessageFragment.Id( + FragmentIndex: (ushort)i, + FragmentCount: (ushort)fragmentCount, + MessageId: msgId), + Data: subset.ToArray().ToImmutableArray())); + } + + return fragments.ToImmutableArray(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs index 87e0463a9..8ef2a28b8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs @@ -10,7 +10,11 @@ namespace Barotrauma.Networking { public static class MsgConstants { - public const int MTU = 1200; //TODO: determine dynamically + // MTU currently set to the upper limit of what EOS P2P can do + // TODO: determine dynamically so other protocols can use a larger MTU, + // as well as handle a client with a lower MTU set outside of our control + public const int MTU = 1170; + public const int CompressionThreshold = 1000; public const int InitialBufferSize = 256; public const int BufferOverAllocateAmount = 4; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/EosP2PConnection.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/EosP2PConnection.cs new file mode 100644 index 000000000..41aa5a2ca --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/EosP2PConnection.cs @@ -0,0 +1,8 @@ +#nullable enable + +namespace Barotrauma.Networking; + +sealed class EosP2PConnection : P2PConnection +{ + public EosP2PConnection(EosP2PEndpoint endpoint) : base(endpoint) { } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/LidgrenConnection.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/LidgrenConnection.cs index 397f4836c..8f665e043 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/LidgrenConnection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/LidgrenConnection.cs @@ -2,7 +2,7 @@ namespace Barotrauma.Networking { - sealed class LidgrenConnection : NetworkConnection + sealed class LidgrenConnection : NetworkConnection { public readonly NetConnection NetConnection; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs index e1a46e55d..9c6e1928c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs @@ -8,6 +8,13 @@ namespace Barotrauma.Networking Disconnected = 0x2 } + abstract class NetworkConnection : NetworkConnection where T : Endpoint + { + protected NetworkConnection(T endpoint) : base(endpoint) { } + + public new T Endpoint => (base.Endpoint as T)!; + } + abstract class NetworkConnection { public const double TimeoutThreshold = 60.0; //full minute for timeout because loading screens can take quite a while @@ -23,7 +30,7 @@ namespace Barotrauma.Networking get; set; } - public NetworkConnection(Endpoint endpoint) + protected NetworkConnection(Endpoint endpoint) { Endpoint = endpoint; } @@ -35,9 +42,9 @@ namespace Barotrauma.Networking public void SetAccountInfo(AccountInfo newInfo) { - AccountInfo = newInfo; + if (AccountInfo.IsNone) { AccountInfo = newInfo; } } - + public sealed override string ToString() => Endpoint.StringRepresentation; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/P2PConnection.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/P2PConnection.cs new file mode 100644 index 000000000..790ac90b7 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/P2PConnection.cs @@ -0,0 +1,30 @@ +#nullable enable + +namespace Barotrauma.Networking; + +abstract class P2PConnection : P2PConnection where T : P2PEndpoint +{ + protected P2PConnection(T endpoint) : base(endpoint) { } + + public new T Endpoint => (base.Endpoint as T)!; +} + +abstract class P2PConnection : NetworkConnection +{ + protected P2PConnection(P2PEndpoint endpoint) : base(endpoint) + { + Heartbeat(); + } + + public double Timeout = 0.0; + + public void Decay(float deltaTime) + { + Timeout -= deltaTime; + } + + public void Heartbeat() + { + Timeout = TimeoutThreshold; + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/PipeConnection.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/PipeConnection.cs index bae9ac6e8..a18081a4d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/PipeConnection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/PipeConnection.cs @@ -23,11 +23,11 @@ namespace Barotrauma.Networking => !(a == b); } - sealed class PipeConnection : NetworkConnection + sealed class PipeConnection : NetworkConnection { - public PipeConnection(AccountId accountId) : base(new PipeEndpoint()) + public PipeConnection(Option accountId) : base(new PipeEndpoint()) { - SetAccountInfo(new AccountInfo(Option.Some(accountId))); + SetAccountInfo(new AccountInfo(accountId)); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/SteamP2PConnection.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/SteamP2PConnection.cs index 425e1bfd4..c01e7f5c1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/SteamP2PConnection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/SteamP2PConnection.cs @@ -1,24 +1,9 @@ namespace Barotrauma.Networking { - sealed class SteamP2PConnection : NetworkConnection + sealed class SteamP2PConnection : P2PConnection { - public double Timeout = 0.0; - public SteamP2PConnection(SteamId steamId) : this(new SteamP2PEndpoint(steamId)) { } - public SteamP2PConnection(SteamP2PEndpoint endpoint) : base(endpoint) - { - Heartbeat(); - } - - public void Decay(float deltaTime) - { - Timeout -= deltaTime; - } - - public void Heartbeat() - { - Timeout = TimeoutThreshold; - } + public SteamP2PConnection(SteamP2PEndpoint endpoint) : base(endpoint) { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkExtensions.cs index 40c0e92b1..da74d80f2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkExtensions.cs @@ -52,8 +52,7 @@ namespace Barotrauma.Networking deliveryMethod switch { DeliveryMethod.Unreliable => NetDeliveryMethod.Unreliable, - DeliveryMethod.Reliable => NetDeliveryMethod.ReliableUnordered, - DeliveryMethod.ReliableOrdered => NetDeliveryMethod.ReliableOrdered, + DeliveryMethod.Reliable => NetDeliveryMethod.ReliableOrdered, _ => NetDeliveryMethod.Unreliable }; @@ -61,7 +60,6 @@ namespace Barotrauma.Networking deliveryMethod switch { DeliveryMethod.Reliable => Steamworks.P2PSend.Reliable, - DeliveryMethod.ReliableOrdered => Steamworks.P2PSend.Reliable, _ => Steamworks.P2PSend.Unreliable }; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs index c8042a945..2ba983261 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs @@ -25,34 +25,42 @@ namespace Barotrauma.Networking } [NetworkSerialize(ArrayMaxSize = ushort.MaxValue)] - internal struct ClientSteamTicketAndVersionPacket : INetSerializableStruct + internal struct ClientAuthTicketAndVersionPacket : INetSerializableStruct { public string Name; public Option OwnerKey; - - #warning TODO: do something about the type of this - // It probably should be Option but we shouldn't build support for - // writing SteamIDs to INetSerializableStruct; we should consider adding - // attributes to give custom behaviors to specific members of a struct - public Option SteamId; - - public Option SteamAuthTicket; + public Option AccountId; + public Option AuthTicket; public string GameVersion; public Identifier Language; } [NetworkSerialize] - internal struct SteamP2PInitializationRelayPacket : INetSerializableStruct + internal readonly record struct P2POwnerToServerHeader + (string? EndpointStr, AccountInfo AccountInfo) : INetSerializableStruct + { + public Option Endpoint => P2PEndpoint.Parse(EndpointStr ?? ""); + } + + [NetworkSerialize] + internal readonly record struct P2PServerToOwnerHeader + (string? EndpointStr) : INetSerializableStruct + { + public Option Endpoint => P2PEndpoint.Parse(EndpointStr ?? ""); + } + + [NetworkSerialize] + internal struct P2PInitializationRelayPacket : INetSerializableStruct { public ulong LobbyID; public PeerPacketMessage Message; } [NetworkSerialize] - internal struct SteamP2PInitializationOwnerPacket : INetSerializableStruct - { - public string OwnerName; - } + internal readonly record struct P2PInitializationOwnerPacket( + string Name, + AccountId AccountId) + : INetSerializableStruct; [NetworkSerialize(ArrayMaxSize = ushort.MaxValue)] @@ -68,7 +76,7 @@ namespace Barotrauma.Networking public byte[] Buffer; public readonly int Length => Buffer.Length; - public readonly IReadMessage GetReadMessageUncompressed() => new ReadWriteMessage(Buffer, 0, Length, copyBuf: false); + public readonly IReadMessage GetReadMessageUncompressed() => new ReadWriteMessage(Buffer, 0, Length * 8, copyBuf: false); public readonly IReadMessage GetReadMessage(bool isCompressed, NetworkConnection conn) => new ReadOnlyMessage(Buffer, isCompressed, 0, Length, conn); } @@ -140,7 +148,8 @@ namespace Barotrauma.Networking DisconnectReason.ExcessiveDesyncOldEvent => ServerMessage, DisconnectReason.ExcessiveDesyncRemovedEvent => ServerMessage, DisconnectReason.SyncTimeout => ServerMessage, - _ => TextManager.Get($"DisconnectReason.{DisconnectReason}").Fallback(TextManager.Get("ConnectionLost")) + DisconnectReason.AuthenticationFailed => TextManager.Get($"DisconnectReason.{DisconnectReason}").Fallback(TextManager.Get("ChatMsg.DisconnectReason.AuthenticationRequired")), + _ => TextManager.Get($"DisconnectReason.{DisconnectReason}").Fallback($"{TextManager.Get("ConnectionLost")} ({DisconnectReason})") }; public LocalizedString ReconnectMessage @@ -178,15 +187,12 @@ namespace Barotrauma.Networking or DisconnectReason.TooManyFailedLogins or DisconnectReason.InvalidVersion); - private const string lidgrenSeparator = ":hankey:"; - /// - /// This exists because Lidgren is a piece of shit and - /// doesn't readily support sending anything other than - /// a string through a disconnect packet, so this thing - /// needs a sufficiently nasty string representation that - /// can be decoded with some certainty that it won't get - /// mangled by user input. + /// This exists because Lidgren doesn't readily support + /// sending anything other than a string through a disconnect + /// packet, so this thing needs a sufficiently nasty string + /// representation that can be decoded with some certainty + /// that it won't get mangled by user input. /// public string ToLidgrenStringRepresentation() { @@ -194,7 +200,7 @@ namespace Barotrauma.Networking => Convert.ToBase64String(Encoding.UTF8.GetBytes(str)); return DisconnectReason - + lidgrenSeparator + + NetworkMagicStrings.LidgrenDisconnectSeparator + strToBase64(AdditionalInformation); } @@ -209,16 +215,16 @@ namespace Barotrauma.Networking case Lidgren.Network.NetConnection.NoResponseMessage: case "Connection timed out": case "Reconnecting": - return Option.Some(WithReason(DisconnectReason.Timeout)); + return Option.Some(WithReason(DisconnectReason.Timeout)); } static string base64ToStr(string base64) => Encoding.UTF8.GetString(Convert.FromBase64String(base64)); - string[] split = str.Split(lidgrenSeparator); - if (split.Length != 2) { return Option.None(); } - if (!Enum.TryParse(split[0], out DisconnectReason disconnectReason)) { return Option.None(); } - return Option.Some(new PeerDisconnectPacket(disconnectReason, base64ToStr(split[1]))); + string[] split = str.Split(NetworkMagicStrings.LidgrenDisconnectSeparator); + if (split.Length != 2) { return Option.None; } + if (!Enum.TryParse(split[0], out DisconnectReason disconnectReason)) { return Option.None; } + return Option.Some(new PeerDisconnectPacket(disconnectReason, base64ToStr(split[1]))); } public static PeerDisconnectPacket Custom(string customMessage) @@ -247,12 +253,12 @@ namespace Barotrauma.Networking public static PeerDisconnectPacket SteamAuthError(Steamworks.BeginAuthResult error) => new PeerDisconnectPacket( - DisconnectReason.SteamAuthenticationFailed, + DisconnectReason.AuthenticationFailed, $"{nameof(Steamworks.BeginAuthResult)}.{error}"); public static PeerDisconnectPacket SteamAuthError(Steamworks.AuthResponse error) => new PeerDisconnectPacket( - DisconnectReason.SteamAuthenticationFailed, + DisconnectReason.AuthenticationFailed, $"{nameof(Steamworks.AuthResponse)}.{error}"); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerListContentPackageInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerListContentPackageInfo.cs new file mode 100644 index 000000000..a796a9c4b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerListContentPackageInfo.cs @@ -0,0 +1,23 @@ +namespace Barotrauma.Networking; + +public readonly record struct ServerListContentPackageInfo( + string Name, string Hash, Option Id) +{ + public ServerListContentPackageInfo(ContentPackage pkg) + : this(pkg.Name, pkg.Hash.StringRepresentation, pkg.UgcId) {} + + public static Option ParseSingleEntry(string singleEntry) + { + if (singleEntry.SplitEscaped(',') is not { Count: 3 } split) { return Option.None; } + + return Option.Some( + new ServerListContentPackageInfo( + split[0], + split[1], + ContentPackageId.Parse(split[2]))); + } + + public override string ToString() + => new[] { Name, Hash, Id.Select(id => id.StringRepresentation).Fallback("") } + .JoinEscaped(','); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index c105f27de..d890f70ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Globalization; using System.Linq; using System.Security.Cryptography; using System.Text; @@ -294,7 +295,7 @@ namespace Barotrauma.Networking if (typeName != null || property.PropertyType.IsEnum) { NetPropertyData netPropertyData = new NetPropertyData(this, property, typeName); - UInt32 key = ToolBox.IdentifierToUint32Hash(netPropertyData.Name, md5); + UInt32 key = ToolBoxCore.IdentifierToUint32Hash(netPropertyData.Name, md5); if (key == 0) { key++; } //0 is reserved to indicate the end of the netproperties section of a message if (netProperties.ContainsKey(key)){ throw new Exception("Hashing collision in ServerSettings.netProperties: " + netProperties[key] + " has same key as " + property.Name + " (" + key.ToString() + ")"); } netProperties.Add(key, netPropertyData); @@ -311,7 +312,7 @@ namespace Barotrauma.Networking if (typeName != null || property.PropertyType.IsEnum) { NetPropertyData netPropertyData = new NetPropertyData(networkMember.KarmaManager, property, typeName); - UInt32 key = ToolBox.IdentifierToUint32Hash(netPropertyData.Name, md5); + UInt32 key = ToolBoxCore.IdentifierToUint32Hash(netPropertyData.Name, md5); if (netProperties.ContainsKey(key)) { throw new Exception("Hashing collision in ServerSettings.netProperties: " + netProperties[key] + " has same key as " + property.Name + " (" + key.ToString() + ")"); } netProperties.Add(key, netPropertyData); } @@ -1136,5 +1137,47 @@ namespace Barotrauma.Networking msg.WriteUInt16((UInt16)subList.FindIndex(s => s.Name.Equals(submarineName, StringComparison.OrdinalIgnoreCase))); } } + + public void UpdateServerListInfo(Action setter) + { + void set(string key, object obj) => setter(key.ToIdentifier(), obj); + + set("ServerName", ServerName); + set("MaxPlayers", MaxPlayers); + set("HasPassword", HasPassword); + set("message", ServerMessageText); + set("version", GameMain.Version); + set("playercount", GameMain.NetworkMember.ConnectedClients.Count); + set("contentpackages", ContentPackageManager.EnabledPackages.All.Where(p => p.HasMultiplayerSyncedContent)); + set("modeselectionmode", ModeSelectionMode); + set("subselectionmode", SubSelectionMode); + set("voicechatenabled", VoiceChatEnabled); + set("allowspectating", AllowSpectating); + set("allowrespawn", AllowRespawn); + set("traitors", TraitorProbability.ToString(CultureInfo.InvariantCulture)); + set("friendlyfireenabled", AllowFriendlyFire); + set("karmaenabled", KarmaEnabled); + set("gamestarted", GameMain.NetworkMember.GameStarted); + set("gamemode", GameModeIdentifier); + set("playstyle", PlayStyle); + set("language", Language.ToString()); +#if SERVER + set("eoscrossplay", EosInterface.Core.IsInitialized); +#else + set("eoscrossplay", EosInterface.IdQueries.IsLoggedIntoEosConnect || Eos.EosSessionManager.CurrentOwnedSession.IsSome()); +#endif + if (GameMain.NetLobbyScreen?.SelectedSub != null) + { + set("submarine", GameMain.NetLobbyScreen.SelectedSub.Name); + } + if (Steamworks.SteamClient.IsLoggedOn) + { + string pingLocation = Steamworks.SteamNetworkingUtils.LocalPingLocation?.ToString(); + if (!pingLocation.IsNullOrEmpty()) + { + set("steampinglocation", pingLocation); + } + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs index e11ea36db..cef4c5681 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs @@ -386,7 +386,7 @@ namespace Barotrauma { using (MD5 md5 = MD5.Create()) { - prefabWithUintIdentifier.UintIdentifier = ToolBox.IdentifierToUint32Hash(prefab.Identifier, md5); + prefabWithUintIdentifier.UintIdentifier = ToolBoxCore.IdentifierToUint32Hash(prefab.Identifier, md5); //it's theoretically possible for two different values to generate the same hash, but the probability is astronomically small T? findCollision() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index d6d5d5887..b21c9327e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -5,10 +5,12 @@ using System.Collections.Immutable; using System.Diagnostics; using System.Globalization; using System.Linq; +using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using System.Xml; using System.Xml.Linq; +using Barotrauma.Extensions; using File = Barotrauma.IO.File; using FileStream = Barotrauma.IO.FileStream; using Path = Barotrauma.IO.Path; @@ -841,6 +843,13 @@ namespace Barotrauma return vector; } + private static readonly ImmutableDictionary monoGameColors = + typeof(Color) + .GetProperties(BindingFlags.Static | BindingFlags.Public) + .Where(p => p.PropertyType == typeof(Color)) + .Select(p => (p.Name.ToIdentifier(), p.GetValueFromStaticProperty())) + .ToImmutableDictionary(); + public static Color ParseColor(string stringColor, bool errorMessages = true) { if (stringColor.StartsWith("gui.", StringComparison.OrdinalIgnoreCase)) @@ -864,6 +873,11 @@ namespace Barotrauma return Color.White; } + if (monoGameColors.TryGetValue(stringColor.ToIdentifier(), out var monoGameColor)) + { + return monoGameColor; + } + string[] strComponents = stringColor.Split(','); Color color = Color.White; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index 0ad15815d..12cad8921 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -84,6 +84,7 @@ namespace Barotrauma Graphics = GraphicsSettings.GetDefault(), Audio = AudioSettings.GetDefault(), #if CLIENT + CrossplayChoice = Eos.EosSteamPrimaryLogin.CrossplayChoice.Unknown, DisableGlobalSpamList = false, KeyMap = KeyMapping.GetDefault(), InventoryKeyMap = InventoryKeyMapping.GetDefault() @@ -91,9 +92,7 @@ namespace Barotrauma }; #if DEBUG - config.UseSteamMatchmaking = true; config.QuickStartSub = "Humpback".ToIdentifier(); - config.RequireSteamAuthentication = true; config.AutomaticQuickStartEnabled = false; config.AutomaticCampaignLoadEnabled = false; config.TextManagerDebugModeEnabled = false; @@ -156,20 +155,16 @@ namespace Barotrauma public Identifier QuickStartSub; public string RemoteMainMenuContentUrl; #if CLIENT + public Eos.EosSteamPrimaryLogin.CrossplayChoice CrossplayChoice; public XElement SavedCampaignSettings; public bool DisableGlobalSpamList; #endif #if DEBUG - public bool UseSteamMatchmaking; - public bool RequireSteamAuthentication; public bool AutomaticQuickStartEnabled; public bool AutomaticCampaignLoadEnabled; public bool TestScreenEnabled; public bool TextManagerDebugModeEnabled; public bool ModBreakerMode; -#else - public bool UseSteamMatchmaking => true; - public bool RequireSteamAuthentication => true; #endif public struct GraphicsSettings diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/AuthTicket.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/AuthTicket.cs index a68b959dd..a41889ad4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/AuthTicket.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/AuthTicket.cs @@ -7,19 +7,20 @@ namespace Barotrauma.Steam { static partial class SteamManager { - private static Option currentMultiplayerTicket = Option.None; - public static Option GetAuthSessionTicketForMultiplayer(Endpoint remoteHostEndpoint) + #region Auth ticket for Steam host + private static Option currentSteamHostAuthTicket = Option.None; + public static Option GetAuthSessionTicketForSteamHost(Endpoint remoteHostEndpoint) { if (!IsInitialized) { return Option.None; } - if (currentMultiplayerTicket.TryUnwrap(out var ticketToCancel)) + if (currentSteamHostAuthTicket.TryUnwrap(out var ticketToCancel)) { ticketToCancel.Cancel(); } - currentMultiplayerTicket = Option.None; + currentSteamHostAuthTicket = Option.None; var netIdentity = remoteHostEndpoint switch { @@ -32,13 +33,42 @@ namespace Barotrauma.Steam }; var newTicket = Steamworks.SteamUser.GetAuthSessionTicket(netIdentity); - currentMultiplayerTicket = newTicket != null + currentSteamHostAuthTicket = newTicket != null ? Option.Some(newTicket) : Option.None; - return currentMultiplayerTicket; + return currentSteamHostAuthTicket; } + #endregion Auth ticket for Steam host + #region Auth ticket for EOS host + private const string EosHostAuthIdentity = "BarotraumaRemotePlayerAuth"; + + private static Option currentEosHostAuthTicket = Option.None; + public static async Task> GetAuthTicketForEosHostAuth() + { + if (!IsInitialized) + { + return Option.None; + } + + if (currentEosHostAuthTicket.TryUnwrap(out var ticketToCancel)) + { + ticketToCancel.Cancel(); + } + currentEosHostAuthTicket = Option.None; + + var newTicket = await Steamworks.SteamUser.GetAuthTicketForWebApi(identity: EosHostAuthIdentity); + + currentEosHostAuthTicket = newTicket != null + ? Option.Some(newTicket) + : Option.None; + + return currentEosHostAuthTicket; + } + #endregion Auth ticket for EOS host + + #region Auth ticket for GameAnalytics consent server private const string GameAnalyticsConsentIdentity = "BarotraumaGameAnalyticsConsent"; private static Option currentGameAnalyticsConsentTicket = Option.None; @@ -63,5 +93,6 @@ namespace Barotrauma.Steam return currentGameAnalyticsConsentTicket; } + #endregion Auth ticket for GameAnalytics consent server } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs index 5de759e46..6467d4e54 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs @@ -1,8 +1,10 @@ -using Steamworks.Data; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; +using System.Runtime.InteropServices; using Barotrauma.Networking; +using Barotrauma.IO; namespace Barotrauma.Steam { @@ -38,6 +40,15 @@ namespace Barotrauma.Steam } } + public static bool SteamworksLibExists + => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? File.Exists("steam_api64.dll") + : RuntimeInformation.IsOSPlatform(OSPlatform.OSX) + ? File.Exists("libsteam_api64.dylib") + : RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + ? File.Exists("libsteam_api64.so") + : false; + public static void Initialize() { InitializeProjectSpecific(); @@ -53,6 +64,16 @@ namespace Barotrauma.Steam return Option.Some(new SteamId(Steamworks.SteamClient.SteamId)); } + public static Option GetOwnerSteamId() + { + if (!IsInitialized || !Steamworks.SteamClient.IsValid) + { + return Option.None(); + } + + return Option.Some(new SteamId(Steamworks.SteamClient.SteamId)); + } + public static bool IsFamilyShared() { if (!IsInitialized || !Steamworks.SteamClient.IsValid) { return false; } @@ -112,42 +133,78 @@ namespace Barotrauma.Steam return unlocked; } - public static bool IncrementStat(Identifier statName, int increment) + /// + /// Increment multiple stats in bulk. + /// Make sure to call StoreStats() after calling this method since it doesn't do it automatically. + /// + /// + public static void IncrementStats(params (AchievementStat Identifier, float Increment)[] stats) + => Array.ForEach(stats, static s + => IncrementStat(s.Identifier, s.Increment, storeStats: false)); + + public static bool IncrementStat(AchievementStat statName, int increment, bool storeStats = true) { if (!IsInitialized || !Steamworks.SteamClient.IsValid) { return false; } DebugConsole.Log($"Incremented stat \"{statName}\" by " + increment); - bool success = Steamworks.SteamUserStats.AddStat(statName.Value.ToLowerInvariant(), increment); + bool success = Steamworks.SteamUserStats.AddStatInt(statName.ToIdentifier().Value.ToLowerInvariant(), increment); if (!success) { DebugConsole.Log("Failed to increment stat \"" + statName + "\"."); } - else + else if (storeStats) { StoreStats(); } return success; } - public static bool IncrementStat(Identifier statName, float increment) + public static bool IncrementStat(AchievementStat statName, float increment, bool storeStats = true) { if (!IsInitialized || !Steamworks.SteamClient.IsValid) { return false; } DebugConsole.Log($"Incremented stat \"{statName}\" by " + increment); - bool success = Steamworks.SteamUserStats.AddStat(statName.Value.ToLowerInvariant(), increment); + bool success = Steamworks.SteamUserStats.AddStatFloat(statName.ToIdentifier().Value.ToLowerInvariant(), increment); if (!success) { DebugConsole.Log("Failed to increment stat \"" + statName + "\"."); } - else + else if (storeStats) { StoreStats(); } return success; } - public static int GetStatInt(Identifier statName) + public static int GetStatInt(AchievementStat stat) { if (!IsInitialized || !Steamworks.SteamClient.IsValid) { return 0; } - return Steamworks.SteamUserStats.GetStatInt(statName.Value.ToLowerInvariant()); + return Steamworks.SteamUserStats.GetStatInt(stat.ToString().ToLowerInvariant()); + } + + public static float GetStatFloat(AchievementStat stat) + { + if (!IsInitialized || !Steamworks.SteamClient.IsValid) { return 0f; } + return Steamworks.SteamUserStats.GetStatFloat(stat.ToString().ToLowerInvariant()); + } + + public static ImmutableDictionary GetAllStats() + { + if (!IsInitialized || !Steamworks.SteamClient.IsValid) { return ImmutableDictionary.Empty; } + + var builder = ImmutableDictionary.CreateBuilder(); + + foreach (AchievementStat stat in AchievementStatExtension.SteamStats) + { + if (stat.IsFloatStat()) + { + builder.Add(stat, GetStatFloat(stat)); + } + else + { + builder.Add(stat, GetStatInt(stat)); + } + } + + return builder.ToImmutable(); } public static bool StoreStats() @@ -177,7 +234,7 @@ namespace Barotrauma.Steam { //this should be run even if SteamManager is uninitialized //servers need to be able to notify clients of unlocked talents even if the server isn't connected to Steam - SteamAchievementManager.Update(deltaTime); + AchievementManager.Update(deltaTime); if (!IsInitialized) { return; } @@ -193,22 +250,6 @@ namespace Barotrauma.Steam if (Steamworks.SteamServer.IsValid) { Steamworks.SteamServer.Shutdown(); } } - public static IEnumerable ParseWorkshopIds(string workshopIdData) - { - string[] workshopIds = workshopIdData.Split(','); - foreach (string id in workshopIds) - { - if (ulong.TryParse(id, out ulong idCast)) - { - yield return idCast; - } - else - { - yield return 0; - } - } - } - public static IEnumerable WorkshopUrlsToIds(IEnumerable urls) { return urls.Select((u) => diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs index a832a5abe..ce4225cc2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs @@ -140,7 +140,7 @@ namespace Barotrauma.Steam return await queryTask; } - public static async Task MakeRequest(UInt64 id) + public static async Task> MakeRequest(UInt64 id) { Task ourTask; lock (mutex) @@ -155,7 +155,7 @@ namespace Barotrauma.Steam } var items = await ourTask; - var result = items.FirstOrNull(it => it.Id == id); + var result = items.FirstOrNone(it => it.Id == id); return result; } } @@ -165,7 +165,7 @@ namespace Barotrauma.Steam /// The description of the returned item is truncated to save bandwidth. /// /// Workshop Item ID - public static Task GetItem(UInt64 itemId) + public static Task> GetItem(UInt64 itemId) => SingleItemRequestPool.MakeRequest(itemId); /// @@ -176,15 +176,17 @@ namespace Barotrauma.Steam /// /// If true, ask for the item's entire description, otherwise it'll be truncated. /// - public static async Task GetItemAsap(UInt64 itemId, bool withLongDescription = false) + public static async Task> GetItemAsap(UInt64 itemId, bool withLongDescription = false) { - if (!IsInitialized) { return null; } + if (!IsInitialized) { return Option.None; } var items = await GetWorkshopItems( Steamworks.Ugc.Query.All .WithFileId(itemId) .WithLongDescription(withLongDescription)); - return items.Any() ? items.First() : null; + return items.Any() + ? Option.Some(items.First()) + : Option.None; } public static async Task ForceRedownload(UInt64 itemId) @@ -379,7 +381,7 @@ namespace Barotrauma.Steam // made private. Players cannot download updates for these, so // we treat them as if they were deleted. allItems = (await Task.WhenAll(allItems.Select(it => GetItem(it.Id.Value)))) - .NotNull() + .NotNone() .Where(it => it.ConsumerApp == AppID) .ToHashSet(); @@ -399,7 +401,7 @@ namespace Barotrauma.Steam TaskPool.Add("DeleteUnsubscribedMods", GetPublishedAndSubscribedItems().WaitForLoadingScreen(), t => { - if (!t.TryGetResult(out ISet items)) { return; } + if (!t.TryGetResult(out ISet? items)) { return; } var ids = items.Select(it => it.Id.Value).ToHashSet(); var toUninstall = ContentPackageManager.WorkshopPackages .Where(pkg @@ -428,8 +430,8 @@ namespace Barotrauma.Steam { using var installCounter = await InstallTaskCounter.Create(id); - var itemNullable = await GetItem(id); - if (!(itemNullable is { } item)) { return; } + var itemOption = await GetItem(id); + if (!itemOption.TryUnwrap(out var item)) { return; } await Task.Yield(); string itemTitle = item.Title?.Trim() ?? ""; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/LocalizedString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/LocalizedString.cs index 6ddc13c18..a34f3dff8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/LocalizedString.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/LocalizedString.cs @@ -48,10 +48,23 @@ namespace Barotrauma public abstract void RetrieveValue(); - public static implicit operator LocalizedString(string value) => new RawLString(value); + public static readonly RawLString EmptyString = new RawLString(""); + public static implicit operator LocalizedString(string value) + => !value.IsNullOrEmpty() + ? new RawLString(value) + : EmptyString; public static implicit operator LocalizedString(char value) => new RawLString(value.ToString()); - public static LocalizedString operator+(LocalizedString left, LocalizedString right) => new ConcatLString(left, right); + public static LocalizedString operator+(LocalizedString left, LocalizedString right) + { + // If either side of the concatenation is an empty string, + // return the other string instead of creating a new object + if (left is RawLString { Value.Length: 0 }) { return right; } + if (right is RawLString { Value.Length: 0 }) { return left; } + + return new ConcatLString(left, right); + } + public static LocalizedString operator+(LocalizedString left, object right) => left + (right.ToString() ?? ""); public static LocalizedString operator+(object left, LocalizedString right) => (left.ToString() ?? "") + right; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/AssemblyInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/AssemblyInfo.cs index 7a904fdac..22e4b96cd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/AssemblyInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/AssemblyInfo.cs @@ -1,11 +1,11 @@ -using System; -using System.Linq; +using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; [assembly:InternalsVisibleTo("WindowsTest"), InternalsVisibleTo("MacTest"), InternalsVisibleTo("LinuxTest")] + public static class AssemblyInfo { public static readonly string GitRevision; @@ -13,6 +13,38 @@ public static class AssemblyInfo public static readonly string ProjectDir; public static readonly string BuildString; + public enum Platform + { + Windows, + MacOS, + Linux + } + + public enum Configuration + { + Release, + Unstable, + Debug + } + +#if WINDOWS + public const Platform CurrentPlatform = Platform.Windows; +#elif OSX + public const Platform CurrentPlatform = Platform.MacOS; +#elif LINUX + public const Platform CurrentPlatform = Platform.Linux; +#else + #error Unknown platform +#endif + +#if DEBUG + public const Configuration CurrentConfiguration = Configuration.Debug; +#elif UNSTABLE + public const Configuration CurrentConfiguration = Configuration.Unstable; +#else + public const Configuration CurrentConfiguration = Configuration.Release; +#endif + static AssemblyInfo() { var asm = typeof(AssemblyInfo).Assembly; @@ -27,22 +59,7 @@ public static class AssemblyInfo string[] dirSplit = ProjectDir.Split('/', '\\'); ProjectDir = string.Join(ProjectDir.Contains('/') ? '/' : '\\', dirSplit.Take(dirSplit.Length - 2)); - BuildString = "Unknown"; -#if WINDOWS - BuildString = "Windows"; -#elif OSX - BuildString = "Mac"; -#elif LINUX - BuildString = "Linux"; -#endif - -#if DEBUG - BuildString = "Debug" + BuildString; -#elif UNSTABLE - BuildString = "Unstable" + BuildString; -#else - BuildString = "Release" + BuildString; -#endif + BuildString = $"{CurrentConfiguration}{CurrentPlatform}"; } public static string CleanupStackTrace(this string stackTrace) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs index fbfe75287..c672b5745 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs @@ -44,26 +44,10 @@ namespace Barotrauma } private static string ByteRepresentationToStringRepresentation(byte[] byteHash) - { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < byteHash.Length; i++) - { - sb.Append(byteHash[i].ToString("X2")); - } - - return sb.ToString(); - } + => ToolBoxCore.ByteArrayToHexString(byteHash); private static byte[] StringRepresentationToByteRepresentation(string strHash) - { - var byteRepresentation = new byte[strHash.Length / 2]; - for (int i = 0; i < byteRepresentation.Length; i++) - { - byteRepresentation[i] = Convert.ToByte(strHash.Substring(i * 2, 2), 16); - } - - return byteRepresentation; - } + => ToolBoxCore.HexStringToByteArray(strHash); public static string GetShortHash(string fullHash) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Rand.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Rand.cs index 3e739b555..8bda617ae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Rand.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Rand.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -30,6 +31,7 @@ namespace Barotrauma public static Random GetRNG(RandSync randSync) { + CheckRandThreadSafety(randSync); return randSync == RandSync.Unsynced ? localRandom : syncedRandom[randSync]; } @@ -70,16 +72,10 @@ namespace Barotrauma } public static float Range(float minimum, float maximum, RandSync sync=RandSync.Unsynced) - { - CheckRandThreadSafety(sync); - return (float)(sync == RandSync.Unsynced ? localRandom : (syncedRandom[sync])).NextDouble() * (maximum - minimum) + minimum; - } + => GetRNG(sync).Range(minimum, maximum); public static double Range(double minimum, double maximum, RandSync sync = RandSync.Unsynced) - { - CheckRandThreadSafety(sync); - return (sync == RandSync.Unsynced ? localRandom : (syncedRandom[sync])).NextDouble() * (maximum - minimum) + minimum; - } + => GetRNG(sync).Range(minimum, maximum); /// /// Min inclusive, Max exclusive! diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Result.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Result.cs deleted file mode 100644 index 20e5f3290..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Result.cs +++ /dev/null @@ -1,85 +0,0 @@ -#nullable enable -using System; -using System.Diagnostics.CodeAnalysis; - -namespace Barotrauma -{ - public abstract class Result - where T: notnull - where TError: notnull - { - public abstract bool IsSuccess { get; } - public bool IsFailure => !IsSuccess; - - public static Result Success(T value) - => new Success(value); - - public static Result Failure(TError error) - => new Failure(error); - - public abstract bool TryUnwrapSuccess([MaybeNullWhen(returnValue: false)] out T value); - public abstract bool TryUnwrapFailure([MaybeNullWhen(returnValue: false)] out TError value); - - public abstract override string? ToString(); - - public static (Func> Success, Func> Failure) GetFactoryMethods() - => (Success, Failure); - } - - public sealed class Success : Result - where T: notnull - where TError: notnull - { - public readonly T Value; - public override bool IsSuccess => true; - - public override bool TryUnwrapSuccess([MaybeNullWhen(returnValue: false)] out T value) - { - value = Value; - return true; - } - - public override bool TryUnwrapFailure([MaybeNullWhen(returnValue: false)] out TError value) - { - value = default; - return false; - } - - public override string ToString() - => $"Success<{typeof(T).NameWithGenerics()}, {typeof(TError).NameWithGenerics()}>({Value})"; - - public Success(T value) - { - Value = value; - } - } - - public sealed class Failure : Result - where T: notnull - where TError: notnull - { - public readonly TError Error; - - public override bool IsSuccess => false; - - public override bool TryUnwrapSuccess([MaybeNullWhen(returnValue: false)] out T value) - { - value = default; - return false; - } - - public override bool TryUnwrapFailure([MaybeNullWhen(returnValue: false)] out TError value) - { - value = Error; - return true; - } - - public override string ToString() - => $"Failure<{typeof(T).NameWithGenerics()}, {typeof(TError).NameWithGenerics()}>({Error})"; - - public Failure(TError error) - { - Error = error; - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskExtensions.cs index 022fcdb3f..236ed7e41 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskExtensions.cs @@ -1,20 +1,10 @@ -using System.Threading.Tasks; +#nullable enable +using System.Threading.Tasks; namespace Barotrauma { - static class TaskExtensions + public static class TaskExtensions { - public static bool TryGetResult(this Task task, out T result) - { - if (task is Task { IsCompletedSuccessfully: true } castTask) - { - result = castTask.Result; - return true; - } - result = default; - return false; - } - public static async Task WaitForLoadingScreen(this Task task) { var result = await task; @@ -27,4 +17,4 @@ namespace Barotrauma return result; } } -} +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs index 9605361b0..ccea18bfa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -199,38 +199,6 @@ namespace Barotrauma return inputType; } - /// - /// Convert a HSV value into a RGB value. - /// - /// Value between 0 and 360 - /// Value between 0 and 1 - /// Value between 0 and 1 - /// Reference - /// - public static Color HSVToRGB(float hue, float saturation, float value) - { - float c = value * saturation; - - float h = Math.Clamp(hue, 0, 360) / 60f; - - float x = c * (1 - Math.Abs(h % 2 - 1)); - - float r = 0, - g = 0, - b = 0; - - if (0 <= h && h <= 1) { r = c; g = x; b = 0; } - else if (1 < h && h <= 2) { r = x; g = c; b = 0; } - else if (2 < h && h <= 3) { r = 0; g = c; b = x; } - else if (3 < h && h <= 4) { r = 0; g = x; b = c; } - else if (4 < h && h <= 5) { r = x; g = 0; b = c; } - else if (5 < h && h <= 6) { r = c; g = 0; b = x; } - - float m = value - c; - - return new Color(r + m, g + m, b + m); - } - /// /// Returns either a green [x] or a red [o] /// @@ -459,24 +427,6 @@ namespace Barotrauma return default; } - public static UInt32 IdentifierToUint32Hash(Identifier id, MD5 md5) - => StringToUInt32Hash(id.Value.ToLowerInvariant(), md5); - - public static UInt32 StringToUInt32Hash(string str, MD5 md5) - { - //calculate key based on MD5 hash instead of string.GetHashCode - //to ensure consistent results across platforms - byte[] inputBytes = Encoding.UTF8.GetBytes(str); - byte[] hash = md5.ComputeHash(inputBytes); - - UInt32 key = (UInt32)((str.Length & 0xff) << 24); //could use more of the hash here instead? - key |= (UInt32)(hash[hash.Length - 3] << 16); - key |= (UInt32)(hash[hash.Length - 2] << 8); - key |= (UInt32)(hash[hash.Length - 1]); - - return key; - } - /// /// Returns a new instance of the class with all properties and fields copied. /// @@ -542,14 +492,6 @@ namespace Barotrauma } } - public static string ByteArrayToString(byte[] ba) - { - StringBuilder hex = new StringBuilder(ba.Length * 2); - foreach (byte b in ba) - hex.AppendFormat("{0:x2}", b); - return hex.ToString(); - } - public static string EscapeCharacters(string str) { return str.Replace("\\", "\\\\").Replace("\"", "\\\""); @@ -692,13 +634,6 @@ namespace Barotrauma return new Rectangle(topLeft, size); } - public static Exception GetInnermost(this Exception e) - { - while (e.InnerException != null) { e = e.InnerException; } - - return e; - } - public static void ThrowIfNull([NotNull] T o) { if (o is null) { throw new ArgumentNullException(); } diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index e0b94102d..6c6ee9198 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,17 @@ +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.3.0.1 +------------------------------------------------------------------------------------------------------------------------------------------------- + +- Fixed character face selection not working in the singleplayer campaign setup menu. +- Made the "connection lost" error messages when connecting to a server fails more descriptive. + +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.3.0.0 +------------------------------------------------------------------------------------------------------------------------------------------------- + +- Epic Games Store release. +- Steam players have the option to enable crossplay between Steam and the Epic Store. When you launch Barotrauma for the first time following our EGS release, you will see a prompt asking whether you want to enable crossplay. If you do not wish to do so, you can decline, and that will be all. You can also update your preferences at any time in the game settings. + ------------------------------------------------------------------------------------------------------------------------------------------------- v1.2.8.0 ------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Barotrauma/BarotraumaTest/SplitEscapedTest.cs b/Barotrauma/BarotraumaTest/SplitEscapedTest.cs new file mode 100644 index 000000000..ae2ab641f --- /dev/null +++ b/Barotrauma/BarotraumaTest/SplitEscapedTest.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Barotrauma; +using FluentAssertions; +using FsCheck; +using Xunit; + +namespace TestProject; + +public sealed class SplitEscapedTest +{ + private const char Joiner = ','; + private readonly record struct PathologicalString(string Value); + + private class CustomGenerators + { + public static Arbitrary SplittableStringGenerator() + { + var rng = new System.Random(); + return Arb.Generate() + .Where(s => s != null) + .Select(s => new PathologicalString( + // Generate a string that only contains backslashes and commas + string.Join("", s.Select(_ => rng.Next() % 100 < 50 ? '\\' : Joiner)))) + .ToArbitrary(); + } + } + + public SplitEscapedTest() + { + Arb.Register(); + } + + [Fact] + public void EqualityTest() + { + Prop.ForAll(EqualityCheck).QuickCheckThrowOnFailure(); + Prop.ForAll(EqualityCheck).QuickCheckThrowOnFailure(); + } + + private static void EqualityCheck(PathologicalString pathologicalString) + { + EqualityCheck(pathologicalString.Value); + } + + private static void EqualityCheck(string? str) + { + if (str is null) { return; } + IReadOnlyList splitted; + try + { + splitted = str.SplitEscaped(Joiner); + } + catch (ArgumentOutOfRangeException) + { + // Didn't fail the test, the input just had bad escapes + return; + } + string recombined = splitted.JoinEscaped(Joiner); + recombined.Should().BeEquivalentTo(str); + } +} \ No newline at end of file diff --git a/Deploy/DeployAll/Deployables.cs b/Deploy/DeployAll/Deployables.cs index a83936516..4048f1e2b 100644 --- a/Deploy/DeployAll/Deployables.cs +++ b/Deploy/DeployAll/Deployables.cs @@ -10,10 +10,10 @@ public static class Deployables { public const string ResultPath = "Deploy/bin/content"; - private const string clientProjFmt = "Barotrauma/BarotraumaClient/{0}Client.csproj"; - private const string serverProjFmt = "Barotrauma/BarotraumaServer/{0}Server.csproj"; + private const string ClientProjFmt = "Barotrauma/BarotraumaClient/{0}Client.csproj"; + private const string ServerProjFmt = "Barotrauma/BarotraumaServer/{0}Server.csproj"; - private static readonly ImmutableArray<(string Project, string Runtime)> platforms = new[] + private static readonly ImmutableArray<(string Project, string Runtime)> Platforms = new[] { ("Windows", "win-x64"), ("Mac", "osx-x64"), @@ -28,11 +28,11 @@ public static class Deployables Path.Combine(ResultPath, "readme.txt"), $"This is Barotrauma {configuration} v{version} ({gitBranch}, {gitRevision}) built on {DateTime.Now}"); - foreach (var (project, runtime) in platforms) + foreach (var (project, runtime) in Platforms) { string serverPath = Path.Combine(ResultPath, project, "Server"); - void checkVersion(string projPath) + void CheckVersion(string projPath) { Version projVersion = Version.Parse( XDocument.Load(projPath).Root? @@ -45,11 +45,11 @@ public static class Deployables } } - string serverProj = string.Format(serverProjFmt, project); - string clientProj = string.Format(clientProjFmt, project); + string serverProj = string.Format(ServerProjFmt, project); + string clientProj = string.Format(ClientProjFmt, project); - checkVersion(serverProj); - checkVersion(clientProj); + CheckVersion(serverProj); + CheckVersion(clientProj); Console.WriteLine( $"*** Building Barotrauma {configuration}{project} v{version} ({gitBranch}, {gitRevision}) to \"{Path.Combine(ResultPath, project)}\" ***"); diff --git a/Deploy/DeployAll/DotnetCmd.cs b/Deploy/DeployAll/DotnetCmd.cs index c6435e313..2ce19f8e6 100644 --- a/Deploy/DeployAll/DotnetCmd.cs +++ b/Deploy/DeployAll/DotnetCmd.cs @@ -16,7 +16,7 @@ public static class DotnetCmd { private const string DotnetAppName = "dotnet"; - private const string desiredRuntimeVersion = "6.0.8"; + private const string DesiredRuntimeVersion = "6.0.8"; public static void Publish(string projPath, string configuration, string runtime, string resultPath) { @@ -36,7 +36,7 @@ public static class DotnetCmd "/p:Platform=x64", "/p:ErrorOnDuplicatePublishOutputFiles=false", //TODO: fix our duplicate files "/p:RollForward=Disable", - $"/p:RuntimeFrameworkVersion={desiredRuntimeVersion}", + $"/p:RuntimeFrameworkVersion={DesiredRuntimeVersion}", "-o", resultPath }, diff --git a/Deploy/DeployAll/EgsAssistant.cs b/Deploy/DeployAll/EgsAssistant.cs new file mode 100644 index 000000000..c69cf4c0c --- /dev/null +++ b/Deploy/DeployAll/EgsAssistant.cs @@ -0,0 +1,143 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Xml; +using System.Xml.Linq; + +namespace DeployAll; + +public static class EgsAssistant +{ + private static string BuildToolFilePath + => Path.Combine(BuildToolExtractRootPath, true switch + { + _ when RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + => "Engine/Binaries/Win64/BuildPatchTool.exe", + _ when RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + => "Engine/Binaries/Linux/BuildPatchTool", + _ when RuntimeInformation.IsOSPlatform(OSPlatform.OSX) + => "Engine/Binaries/Mac/BuildPatchTool", + _ => throw new Exception($"Unsupported host platform: {RuntimeInformation.OSDescription}") + }); + + private const string BuildToolExtractRootPath = "Deploy/bin/EpicBuildTool"; + private const string BuildToolConfig = $"{BuildToolExtractRootPath}/epic_build_tool_config.xml"; + + private const string CloudDir = $"{BuildToolExtractRootPath}/CloudDir"; + + private const string FileIgnoreListPath = $"{BuildToolExtractRootPath}/ignore.txt"; + private const string FileAttributeListPath = $"{BuildToolExtractRootPath}/fileattributes.txt"; + + public static void Upload(Version version, string configuration, string revision) + { + while (!File.Exists(BuildToolFilePath)) + { + Directory.CreateDirectory(BuildToolExtractRootPath); + if (Util.AskQuestion( + $"Epic BuildPatchTool not found. Extract it to {BuildToolExtractRootPath}, then enter Y to continue. Enter nothing to cancel.") + .AnsweredNo()) + { + return; + } + } + + XDocument? cfg = null; + while (!Util.TryLoadXml(BuildToolConfig, out cfg) + || cfg.Root!.Attributes().Any(attr => !attr.Value.IsValidEpicCfg())) + { + if (!File.Exists(BuildToolConfig)) + { + var doc = new XDocument( + new XElement("config")); + doc.Root!.Add(new XAttribute("OrganizationId", " ORGANIZATION ID ")); + doc.Root!.Add(new XAttribute("ProductId", " PRODUCT ID ")); + doc.Root!.Add(new XAttribute("ArtifactId", " ARTIFACT ID ")); + doc.Root!.Add(new XAttribute("ClientId", " BUILDPATCHTOOL CLIENT ID ")); + doc.Root!.Add(new XAttribute("ClientSecret", " BUILDPATCHTOOL CLIENT SECRET ")); + + var xmlWriterSettings = new XmlWriterSettings + { + Indent = true, + OmitXmlDeclaration = true, + NewLineOnAttributes = true + }; + + using var writer = XmlWriter.Create(BuildToolConfig, xmlWriterSettings); + doc.WriteTo(writer); + writer.Flush(); + } + + if (Util.AskQuestion( + $"Parameters for BuildPatchTool are missing or invalid. Fill in {BuildToolConfig} with the appropriate values, then enter Y to continue. Enter nothing to cancel.") + .AnsweredNo()) + { + return; + } + } + + Directory.CreateDirectory(CloudDir); + + XElement configElement = cfg.Root; + string organizationId = configElement.GetAttributeOrThrow("OrganizationId"); + string productId = configElement.GetAttributeOrThrow("ProductId"); + string artifactId = configElement.GetAttributeOrThrow("ArtifactId"); + string clientId = configElement.GetAttributeOrThrow("ClientId"); + string clientSecret = configElement.GetAttributeOrThrow("ClientSecret"); + + var supportedPlatforms = new (string Platform, string ExecutablePath)[] + { + ("Windows", "Barotrauma.exe"), + + // TODO: reevaluate macOS support for the Epic Games Store version of Barotrauma + // This was dropped because of QA difficulty and missing features on the platform + // but it may be possible to get it working well enough to be shipped. + //("Mac", "Barotrauma.app/Contents/MacOS/Barotrauma") + }; + + foreach ((string platform, string executablePath) in supportedPlatforms) + { + string RelativeToAbsolute(string relativePath) + => Path.Combine(Path.GetDirectoryName(executablePath) ?? "", relativePath).NormalizePathSeparators(); + + var filesToIgnore = new[] { "steam_api64.dll", "libsteam_api64.dylib", "libsteam_api64.so" } + .Select(RelativeToAbsolute) + .ToArray(); + File.WriteAllLines(FileIgnoreListPath, filesToIgnore); + var fileAttributes = platform == "Mac" + ? new[] { "DedicatedServer" } + : Array.Empty(); + fileAttributes = fileAttributes + .Select(RelativeToAbsolute) + .Select(f => $"\"{f}\" executable") + .ToArray(); + File.WriteAllLines(FileAttributeListPath, fileAttributes); + + var psi = new ProcessStartInfo + { + FileName = BuildToolFilePath, + ArgumentList = + { + $"-OrganizationId=\"{organizationId}\"", + $"-ProductId=\"{productId}\"", + $"-ArtifactId=\"{artifactId}\"", + $"-ClientId=\"{clientId}\"", + $"-ClientSecret=\"{clientSecret}\"", + "-mode=UploadBinary", + $"-BuildRoot=\"{Path.GetFullPath(Path.Combine(Deployables.ResultPath, platform, "Client")).NormalizePathSeparators()}\"", + $"-CloudDir=\"{Path.GetFullPath(CloudDir).NormalizePathSeparators()}\"", + $"-BuildVersion=\"{version}-{platform}{configuration}-{revision}\"", + $"-AppLaunch=\"{executablePath}\"", + "-AppArgs=\"\"", + $"-FileIgnoreList={Path.GetFullPath(FileIgnoreListPath).NormalizePathSeparators()}", + $"-FileAttributeList={Path.GetFullPath(FileAttributeListPath).NormalizePathSeparators()}" + }, + RedirectStandardOutput = false, + RedirectStandardError = false + }; + var process = Util.StartProcess(psi); + process.WaitForExit(); + } + } +} \ No newline at end of file diff --git a/Deploy/DeployAll/GitCmd.cs b/Deploy/DeployAll/GitCmd.cs index 624c418e3..c441707fd 100644 --- a/Deploy/DeployAll/GitCmd.cs +++ b/Deploy/DeployAll/GitCmd.cs @@ -5,13 +5,13 @@ namespace DeployAll; public static class GitCmd { - private const string gitCmdName = "git"; + private const string GitCmdName = "git"; private static ProcessStartInfo MakePsi(params string[] args) { var psi = new ProcessStartInfo { - FileName = gitCmdName, + FileName = GitCmdName, RedirectStandardError = true, RedirectStandardOutput = true }; diff --git a/Deploy/DeployAll/Program.cs b/Deploy/DeployAll/Program.cs index 3de060435..5f5c53202 100644 --- a/Deploy/DeployAll/Program.cs +++ b/Deploy/DeployAll/Program.cs @@ -4,6 +4,8 @@ using System.Linq; using System.Xml.Linq; using DeployAll; +AppDomain.CurrentDomain.ProcessExit += (_, _) => Console.WriteLine("Bye!"); + while (!Directory.GetFiles(".").Any(f => f.EndsWith(".sln"))) { Directory.SetCurrentDirectory(".."); @@ -46,6 +48,13 @@ if (string.IsNullOrWhiteSpace(configuration)) { return; } Deployables.Generate(configuration, gameVersion, gitBranch, gitRevision); +if (Util.AskQuestion("Would you like to upload the generated builds to EGS? [y/n]") + .AnsweredYes()) +{ + EgsAssistant.Upload(gameVersion, configuration, gitRevision); +} + + if (Util.AskQuestion("Would you like to upload the generated builds to Steam? [y/n]") .AnsweredNo()) { return; } diff --git a/Deploy/DeployAll/SteamPipeAssistant.cs b/Deploy/DeployAll/SteamPipeAssistant.cs index e111af644..6e29dd98b 100644 --- a/Deploy/DeployAll/SteamPipeAssistant.cs +++ b/Deploy/DeployAll/SteamPipeAssistant.cs @@ -34,7 +34,7 @@ public static class SteamPipeAssistant } } - private static string steamCmdUrl + private static string SteamCmdUrl => true switch { _ when RuntimeInformation.IsOSPlatform(OSPlatform.Windows) @@ -46,7 +46,7 @@ public static class SteamPipeAssistant _ => throw new Exception($"Unsupported host platform: {RuntimeInformation.OSDescription}") }; - private static string[] steamCmdFilenames + private static string[] SteamCmdFilenames => true switch { _ when RuntimeInformation.IsOSPlatform(OSPlatform.Windows) @@ -71,9 +71,9 @@ public static class SteamPipeAssistant Util.RecreateDirectory(SteamCmdPath); - var steamCmdPkg = Util.DownloadFile(steamCmdUrl).ToArray(); + var steamCmdPkg = Util.DownloadFile(SteamCmdUrl, out _).ToArray(); - if (Path.GetExtension(steamCmdUrl) == ".zip") + if (Path.GetExtension(SteamCmdUrl) == ".zip") { using var memStream = new MemoryStream(steamCmdPkg); using ZipArchive archive = new ZipArchive(memStream, ZipArchiveMode.Read); @@ -81,7 +81,7 @@ public static class SteamPipeAssistant } else { - string downloadResultPath = Path.Combine(SteamCmdPath, Path.GetFileName(steamCmdUrl)); + string downloadResultPath = Path.Combine(SteamCmdPath, Path.GetFileName(SteamCmdUrl)); File.WriteAllBytes(downloadResultPath, steamCmdPkg); var psi = new ProcessStartInfo @@ -102,7 +102,7 @@ public static class SteamPipeAssistant File.Delete(downloadResultPath); - foreach (var filename in steamCmdFilenames) + foreach (var filename in SteamCmdFilenames) { psi = new ProcessStartInfo { @@ -126,7 +126,7 @@ public static class SteamPipeAssistant private const string ScriptPath = "Deploy/bin/scripts"; private const string BuildOutput = "Deploy/bin/output"; - private const string appIdScriptFileFmt = "app_{0}.vdf"; + private const string AppIdScriptFileFmt = "app_{0}.vdf"; private const ulong ClientAppId = 602960; private const ulong ClientWindowsDepotId = 602961; @@ -194,14 +194,14 @@ public static class SteamPipeAssistant new SingleItem("contentroot", Path.GetFullPath(Deployables.ResultPath)), new SingleItem("setlive", appId switch { - ClientAppId => "experimental", - ServerAppId => "development", + ClientAppId => "refactor_our_souls", + ServerAppId => "refactor_our_souls", _ => throw new InvalidOperationException() }), new SingleItem("preview", "0"), depotScripts); - var scriptFileName = Path.Combine(ScriptPath, string.Format(appIdScriptFileFmt, appId)); + var scriptFileName = Path.Combine(ScriptPath, string.Format(AppIdScriptFileFmt, appId)); File.WriteAllText(scriptFileName, script.ToString()); } @@ -223,7 +223,7 @@ public static class SteamPipeAssistant ProcessStartInfo psi = new ProcessStartInfo { - FileName = Path.Combine(SteamCmdPath, steamCmdFilenames.First()), + FileName = Path.Combine(SteamCmdPath, SteamCmdFilenames.First()), ArgumentList = { "+login", @@ -233,13 +233,13 @@ public static class SteamPipeAssistant RedirectStandardError = false }; - void addScriptCmd(ulong appId) + void AddScriptCmd(ulong appId) { psi.ArgumentList.Add("+run_app_build"); - psi.ArgumentList.Add(Path.GetFullPath(Path.Combine(ScriptPath, string.Format(appIdScriptFileFmt, appId)))); + psi.ArgumentList.Add(Path.GetFullPath(Path.Combine(ScriptPath, string.Format(AppIdScriptFileFmt, appId)))); } - addScriptCmd(ClientAppId); - if (configuration == "Release") { addScriptCmd(ServerAppId); } + AddScriptCmd(ClientAppId); + if (configuration == "Release") { AddScriptCmd(ServerAppId); } psi.ArgumentList.Add("+quit"); var process = Util.StartProcess(psi); diff --git a/Deploy/DeployAll/Util.cs b/Deploy/DeployAll/Util.cs index 6962ac512..0a4fc8091 100644 --- a/Deploy/DeployAll/Util.cs +++ b/Deploy/DeployAll/Util.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Net.Http; +using System.Xml.Linq; namespace DeployAll; @@ -64,13 +66,16 @@ public static class Util DeleteDirectory(path); Directory.CreateDirectory(path); } - - public static IReadOnlyList DownloadFile(string url) + + public static IReadOnlyList DownloadFile(string url, out string finalUrl) { - var httpClient = new HttpClient(); + finalUrl = url; + + using var httpClient = new HttpClient(); var response = httpClient.Send(new HttpRequestMessage( HttpMethod.Get, new Uri(url))); + finalUrl = response.RequestMessage?.RequestUri?.AbsoluteUri ?? url; using var stream = response.Content.ReadAsStream(); using var reader = new BinaryReader(stream); @@ -100,6 +105,50 @@ public static class Util public static bool AnsweredNo(this string answer) => !answer.AnsweredYes(); + public static bool IsValidEpicCfg(this char c) + => char.IsDigit(c) + || c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z') or '-' or '_' or '+' or '/'; + + public static bool IsValidEpicCfg(this string s) + => !string.IsNullOrEmpty(s) && s.All(IsValidEpicCfg); + + public static bool TryLoadXml(string path, [NotNullWhen(returnValue: true)]out XDocument? doc) + { + try + { + doc = XDocument.Load(path); + return true; + } + catch + { + doc = null; + return false; + } + } + + public static string GetAttributeOrThrow(this XElement element, string attributeName) + { + var attribute = element + .Attributes() + .FirstOrDefault(e => e.Name.LocalName.Equals(attributeName, StringComparison.OrdinalIgnoreCase)); + if (attribute != null + && !string.IsNullOrEmpty(attribute.Value)) + { + return attribute.Value; + } + + throw new Exception($"{attributeName} is not set"); + } + + public static string ThrowIfNullOrEmpty(this string? s, string msg) + { + if (string.IsNullOrEmpty(s)) { throw new Exception(msg); } + return s; + } + + public static string NormalizePathSeparators(this string s) + => s.Replace("\\", "/"); + public static Process StartProcess(ProcessStartInfo info) => Process.Start(info) ?? throw new Exception($"Failed to start process \"{info.FileName}\""); diff --git a/Libraries/BarotraumaLibs/BarotraumaCore/Achievements/AchievementStats.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Achievements/AchievementStats.cs new file mode 100644 index 000000000..35135a685 --- /dev/null +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Achievements/AchievementStats.cs @@ -0,0 +1,59 @@ +#nullable enable +using System; +using System.Collections.Immutable; + +namespace Barotrauma; + +public enum AchievementStat +{ + GameLaunchCount, + MonstersKilled, + HumansKilled, + KMsTraveled, + HoursInEditor, + MetersTraveled, + MinutesInEditor +} + +public static class AchievementStatExtension +{ + public static readonly ImmutableArray SteamStats = new [] + { + AchievementStat.KMsTraveled, + AchievementStat.HoursInEditor, + AchievementStat.HumansKilled, + AchievementStat.MonstersKilled + }.ToImmutableArray(); + + public static readonly ImmutableArray EosStats = new [] + { + AchievementStat.MetersTraveled, + AchievementStat.MinutesInEditor, + AchievementStat.HumansKilled, + AchievementStat.MonstersKilled + }.ToImmutableArray(); + + public static bool IsFloatStat(this AchievementStat stat) => + stat switch + { + AchievementStat.KMsTraveled => true, + AchievementStat.HoursInEditor => true, + _ => false + }; + + public static AchievementStat FromIdentifier(Identifier identifier) => + Enum.TryParse(value: identifier.ToString().ToLowerInvariant(), ignoreCase: true, result: out AchievementStat stat) + ? stat + : throw new ArgumentException($"Invalid achievement stat identifier \"{identifier}\""); + + public static (AchievementStat Stat, int Value) ToEos(this AchievementStat stat, float value) => + stat switch + { + AchievementStat.KMsTraveled => (AchievementStat.MetersTraveled, (int)MathF.Floor(value * 1000f)), + AchievementStat.HoursInEditor => (AchievementStat.MinutesInEditor, (int)MathF.Floor(value * 60f)), + _ => (stat, (int)value) + }; + + public static (AchievementStat Stat, float Value) ToSteam(this AchievementStat stat, float value) => + (stat, value); +} \ No newline at end of file diff --git a/Libraries/BarotraumaLibs/BarotraumaCore/BarotraumaCore.csproj b/Libraries/BarotraumaLibs/BarotraumaCore/BarotraumaCore.csproj new file mode 100644 index 000000000..cb7dea18c --- /dev/null +++ b/Libraries/BarotraumaLibs/BarotraumaCore/BarotraumaCore.csproj @@ -0,0 +1,28 @@ + + + + net6.0 + Barotrauma + disable + enable + + + + full + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + true + x64 + + + + full + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + true + x64 + + + + + + + diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/ColorExtensions.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Extensions/ColorExtensions.cs similarity index 100% rename from Barotrauma/BarotraumaShared/SharedSource/Extensions/ColorExtensions.cs rename to Libraries/BarotraumaLibs/BarotraumaCore/Extensions/ColorExtensions.cs diff --git a/Libraries/BarotraumaLibs/BarotraumaCore/Extensions/EnumerableExtensionsCore.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Extensions/EnumerableExtensionsCore.cs new file mode 100644 index 000000000..e78ac9442 --- /dev/null +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Extensions/EnumerableExtensionsCore.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; + +namespace Barotrauma.Extensions; + +public static class EnumerableExtensionsCore +{ + public static ImmutableDictionary ToImmutableDictionary(this IEnumerable<(TKey, TValue)> enumerable) + where TKey : notnull + { + return enumerable.ToDictionary().ToImmutableDictionary(); + } + + public static Dictionary ToDictionary(this IEnumerable<(TKey, TValue)> enumerable) + where TKey : notnull + { + var dictionary = new Dictionary(); + foreach (var (k,v) in enumerable) + { + dictionary.Add(k, v); + } + return dictionary; + } + + [return: NotNullIfNotNull("immutableDictionary")] + public static Dictionary? ToMutable(this ImmutableDictionary? immutableDictionary) + where TKey : notnull + { + if (immutableDictionary == null) { return null; } + return new Dictionary(immutableDictionary); + } + +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/PointExtensions.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Extensions/PointExtensions.cs similarity index 100% rename from Barotrauma/BarotraumaShared/SharedSource/Extensions/PointExtensions.cs rename to Libraries/BarotraumaLibs/BarotraumaCore/Extensions/PointExtensions.cs diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/RectangleExtensions.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Extensions/RectangleExtensions.cs similarity index 100% rename from Barotrauma/BarotraumaShared/SharedSource/Extensions/RectangleExtensions.cs rename to Libraries/BarotraumaLibs/BarotraumaCore/Extensions/RectangleExtensions.cs diff --git a/Libraries/BarotraumaLibs/BarotraumaCore/Extensions/RngExtensions.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Extensions/RngExtensions.cs new file mode 100644 index 000000000..7a2909251 --- /dev/null +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Extensions/RngExtensions.cs @@ -0,0 +1,11 @@ +using System; +namespace Barotrauma.Extensions; + +public static class RngExtensions +{ + public static float Range(this Random rng, float minimum, float maximum) + => (float)rng.Range((double)minimum, (double)maximum); + + public static double Range(this Random rng, double minimum, double maximum) + => rng.NextDouble() * (maximum - minimum) + minimum; +} diff --git a/Libraries/BarotraumaLibs/BarotraumaCore/Extensions/StringExtensions.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Extensions/StringExtensions.cs new file mode 100644 index 000000000..7ab5bc366 --- /dev/null +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Extensions/StringExtensions.cs @@ -0,0 +1,66 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace Barotrauma +{ + public static class StringExtensions + { + [return: NotNullIfNotNull("fallback")] + public static string? FallbackNullOrEmpty(this string? s, string? fallback) => string.IsNullOrEmpty(s) ? fallback : s; + + public static bool IsNullOrEmpty([NotNullWhen(returnValue: false)]this string? s) => string.IsNullOrEmpty(s); + public static bool IsNullOrWhiteSpace([NotNullWhen(returnValue: false)]this string? s) => string.IsNullOrWhiteSpace(s); + public static string RemoveFromEnd(this string s, string substr, StringComparison stringComparison = StringComparison.Ordinal) + => s.EndsWith(substr, stringComparison) ? s.Substring(0, s.Length - substr.Length) : s; + + public static bool IsTrueString(this string s) + => s.Length == 4 + && s[0] is 'T' or 't' + && s[1] is 'R' or 'r' + && s[2] is 'U' or 'u' + && s[3] is 'E' or 'e'; + + public static string JoinEscaped(this IEnumerable strings, char joiner) + { + return string.Join( + joiner, + strings.Select(s => s + .Replace("\\", "\\\\") + .Replace(joiner.ToString(), $"\\{joiner}"))); + } + + public static IReadOnlyList SplitEscaped(this string str, char joiner) + { + bool isEscape(int i) + { + return i >= 0 && str[i] == '\\' && !isEscape(i - 1); + } + + var retVal = new List(); + int lastSplitIndex = 0; + for (int i = 0; i < str.Length; i++) + { + if (str[i] == joiner && !isEscape(i - 1)) + { + retVal.Add(str[lastSplitIndex..i]); + lastSplitIndex = i + 1; + } + if (isEscape(i) && (i >= str.Length - 1 || (str[i+1] != joiner && str[i+1] != '\\'))) + { + throw new ArgumentOutOfRangeException($"The string \"{str}\" could not have been produced by a call to {nameof(JoinEscaped)} with joiner '{joiner}'"); + } + } + retVal.Add(str[lastSplitIndex..]); + for (int i = 0; i < retVal.Count; i++) + { + retVal[i] = retVal[i] + .Replace($"\\{joiner}", joiner.ToString()) + .Replace("\\\\", "\\"); + } + return retVal; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringFormatter.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Extensions/StringFormatter.cs similarity index 97% rename from Barotrauma/BarotraumaShared/SharedSource/Extensions/StringFormatter.cs rename to Libraries/BarotraumaLibs/BarotraumaCore/Extensions/StringFormatter.cs index 64fd0ab3b..96bd5c15c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringFormatter.cs +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Extensions/StringFormatter.cs @@ -121,7 +121,7 @@ namespace Barotrauma } } - public static ICollection ParseCommaSeparatedStringToCollection(string input, ICollection texts = null, bool convertToLowerInvariant = true) + public static ICollection ParseCommaSeparatedStringToCollection(string input, ICollection? texts = null, bool convertToLowerInvariant = true) { if (texts == null) { @@ -149,7 +149,7 @@ namespace Barotrauma return texts; } - public static ICollection ParseSeparatedStringToCollection(string input, string[] separators, ICollection texts = null, bool convertToLowerInvariant = true) + public static ICollection ParseSeparatedStringToCollection(string input, string[] separators, ICollection? texts = null, bool convertToLowerInvariant = true) { if (texts == null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/StructExtensions.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Extensions/StructExtensions.cs similarity index 100% rename from Barotrauma/BarotraumaShared/SharedSource/Extensions/StructExtensions.cs rename to Libraries/BarotraumaLibs/BarotraumaCore/Extensions/StructExtensions.cs diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/VectorExtensions.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Extensions/VectorExtensions.cs similarity index 100% rename from Barotrauma/BarotraumaShared/SharedSource/Extensions/VectorExtensions.cs rename to Libraries/BarotraumaLibs/BarotraumaCore/Extensions/VectorExtensions.cs diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/AccountId/AccountId.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/AccountId/AccountId.cs similarity index 60% rename from Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/AccountId/AccountId.cs rename to Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/AccountId/AccountId.cs index 47fc84fd3..b93df20ef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/AccountId/AccountId.cs +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/AccountId/AccountId.cs @@ -2,9 +2,10 @@ namespace Barotrauma.Networking { - abstract class AccountId + public abstract class AccountId { public abstract string StringRepresentation { get; } + public abstract string EosStringRepresentation { get; } public static Option Parse(string str) => ReflectionUtils.ParseDerived(str); @@ -15,10 +16,12 @@ namespace Barotrauma.Networking public override string ToString() => StringRepresentation; - public static bool operator ==(AccountId a, AccountId b) - => a.Equals(b); + public static bool operator ==(AccountId? a, AccountId? b) + => a is null + ? b is null + : a.Equals(b); - public static bool operator !=(AccountId a, AccountId b) + public static bool operator !=(AccountId? a, AccountId? b) => !(a == b); } } \ No newline at end of file diff --git a/Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/AccountId/EpicAccountId.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/AccountId/EpicAccountId.cs new file mode 100644 index 000000000..e00574a08 --- /dev/null +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/AccountId/EpicAccountId.cs @@ -0,0 +1,32 @@ +#nullable enable +using System; +namespace Barotrauma.Networking; + +public sealed class EpicAccountId : AccountId +{ + private EpicAccountId(string value) + { + EosStringRepresentation = value.ToLowerInvariant(); + } + + private const string prefix = "EPIC_"; + + public override string StringRepresentation => $"{prefix}{EosStringRepresentation.ToUpperInvariant()}"; + public override string EosStringRepresentation { get; } + + public override bool Equals(object? obj) + => obj is EpicAccountId otherId + && otherId.EosStringRepresentation.Equals(EosStringRepresentation, StringComparison.OrdinalIgnoreCase); + + public override int GetHashCode() + => EosStringRepresentation.GetHashCode(StringComparison.OrdinalIgnoreCase); + + public new static Option Parse(string str) + { + if (str.IsNullOrWhiteSpace()) { return Option.None; } + if (str.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { str = str[prefix.Length..]; } + if (!str.IsHexString()) { return Option.None; } + + return Option.Some(new EpicAccountId(str)); + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/AccountId/SteamId.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/AccountId/SteamId.cs similarity index 96% rename from Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/AccountId/SteamId.cs rename to Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/AccountId/SteamId.cs index 5ae6f4fe5..69fb000aa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/AccountId/SteamId.cs +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/AccountId/SteamId.cs @@ -1,14 +1,17 @@ #nullable enable using System; +using System.Globalization; namespace Barotrauma.Networking { - sealed class SteamId : AccountId + public sealed class SteamId : AccountId { public readonly UInt64 Value; public override string StringRepresentation { get; } + public override string EosStringRepresentation => Value.ToString(CultureInfo.InvariantCulture); + /// Based on information found here: https://developer.valvesoftware.com/wiki/SteamID /// ------------------------------------------------------------------------------------ /// A SteamID is a 64-bit value (16 hexadecimal digits) that's broken up as follows: diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/Address.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/Address/Address.cs similarity index 95% rename from Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/Address.cs rename to Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/Address/Address.cs index f5ca6da14..67b3870e4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/Address.cs +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/Address/Address.cs @@ -2,7 +2,7 @@ namespace Barotrauma.Networking { - abstract class Address + public abstract class Address { public abstract string StringRepresentation { get; } diff --git a/Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/Address/EosP2PAddress.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/Address/EosP2PAddress.cs new file mode 100644 index 000000000..aaf938b7c --- /dev/null +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/Address/EosP2PAddress.cs @@ -0,0 +1,38 @@ +#nullable enable +using System; +using System.Linq; +using System.Security.Cryptography; +namespace Barotrauma.Networking; + +public sealed class EosP2PAddress : P2PAddress +{ + private const string prefix = "EOS_"; + + public readonly string EosStringRepresentation; + + public EosP2PAddress(string value) + { + EosStringRepresentation = value.ToLowerInvariant(); + } + + public new static Option Parse(string addressStr) + { + if (addressStr.StartsWith(prefix)) { addressStr = addressStr[prefix.Length..]; } + if (!addressStr.IsHexString()) { return Option.None; } + + return Option.Some(new EosP2PAddress(addressStr)); + } + + public override string StringRepresentation => $"{prefix}{EosStringRepresentation}"; + public override bool IsLocalHost => false; + + public override bool Equals(object? obj) + => obj is EosP2PAddress other + && other.EosStringRepresentation.ToString().Equals(EosStringRepresentation.ToString(), StringComparison.OrdinalIgnoreCase); + + public override int GetHashCode() + { + using var md5 = MD5.Create(); + return unchecked((int)ToolBoxCore.StringToUInt32Hash(EosStringRepresentation, md5)); + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/LidgrenAddress.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/Address/LidgrenAddress.cs similarity index 98% rename from Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/LidgrenAddress.cs rename to Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/Address/LidgrenAddress.cs index bb8eeaa40..3c925bb0a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/LidgrenAddress.cs +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/Address/LidgrenAddress.cs @@ -6,7 +6,7 @@ using System.Net.Sockets; namespace Barotrauma.Networking { - sealed class LidgrenAddress : Address + public sealed class LidgrenAddress : Address { public readonly IPAddress NetAddress; diff --git a/Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/Address/P2PAddress.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/Address/P2PAddress.cs new file mode 100644 index 000000000..e6dd52858 --- /dev/null +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/Address/P2PAddress.cs @@ -0,0 +1,7 @@ +namespace Barotrauma.Networking; + +public abstract class P2PAddress : Address +{ + public new static Option Parse(string str) + => Address.Parse(str).Bind(addr => addr is P2PAddress p2pAddr ? Option.Some(p2pAddr) : Option.None); +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/PipeAddress.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/Address/PipeAddress.cs similarity index 91% rename from Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/PipeAddress.cs rename to Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/Address/PipeAddress.cs index 1508dc239..58d1150fe 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/PipeAddress.cs +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/Address/PipeAddress.cs @@ -2,7 +2,7 @@ namespace Barotrauma.Networking { - sealed class PipeAddress : Address + public sealed class PipeAddress : Address { public override string StringRepresentation => "PIPE"; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/SteamP2PAddress.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/Address/SteamP2PAddress.cs similarity index 81% rename from Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/SteamP2PAddress.cs rename to Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/Address/SteamP2PAddress.cs index 641815caa..5cba33fda 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/SteamP2PAddress.cs +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/Address/SteamP2PAddress.cs @@ -2,7 +2,7 @@ namespace Barotrauma.Networking { - sealed class SteamP2PAddress : Address + public sealed class SteamP2PAddress : P2PAddress { public readonly SteamId SteamId; @@ -19,11 +19,7 @@ namespace Barotrauma.Networking => SteamId.Parse(endpointStr).Select(steamId => new SteamP2PAddress(steamId)); public override bool Equals(object? obj) - => obj switch - { - SteamP2PAddress otherAddress => this == otherAddress, - _ => false - }; + => obj is SteamP2PAddress otherAddress && this == otherAddress; public override int GetHashCode() => SteamId.GetHashCode(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/UnknownAddress.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/Address/UnknownAddress.cs similarity index 87% rename from Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/UnknownAddress.cs rename to Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/Address/UnknownAddress.cs index 394d9c56d..ad57c6a3f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/UnknownAddress.cs +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/Address/UnknownAddress.cs @@ -2,7 +2,7 @@ namespace Barotrauma.Networking { - sealed class UnknownAddress : Address + public sealed class UnknownAddress : Address { public override string StringRepresentation => "Hidden"; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkEnums.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/NetworkEnums.cs similarity index 65% rename from Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkEnums.cs rename to Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/NetworkEnums.cs index 03712e66f..b7c9866e0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkEnums.cs +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Networking/Primitives/NetworkEnums.cs @@ -5,19 +5,18 @@ namespace Barotrauma.Networking public enum DeliveryMethod : int { Unreliable = 0x0, - Reliable = 0x1, - ReliableOrdered = 0x2 + Reliable = 0x1 } public enum ConnectionInitialization : int { //used by all peer implementations - SteamTicketAndVersion = 0x1, + AuthInfoAndVersion = 0x1, ContentPackageOrder = 0x2, Password = 0x3, Success = 0x0, - //used only by SteamP2P implementations + //used only by P2P implementations ConnectionStarted = 0x4 } @@ -29,10 +28,11 @@ namespace Barotrauma.Networking IsCompressed = 0x1, IsConnectionInitializationStep = 0x2, - //used only by SteamP2P implementations + //used only by P2P implementations IsDisconnectMessage = 0x4, IsServerMessage = 0x8, - IsHeartbeatMessage = 0x10 + IsHeartbeatMessage = 0x10, + IsDataFragment = 0x20 } public static class NetworkEnumExtensions @@ -40,7 +40,6 @@ namespace Barotrauma.Networking public static bool IsCompressed(this PacketHeader h) => h.HasFlag(PacketHeader.IsCompressed); - #warning TODO: remove? public static bool IsConnectionInitializationStep(this PacketHeader h) => h.HasFlag(PacketHeader.IsConnectionInitializationStep); @@ -52,6 +51,17 @@ namespace Barotrauma.Networking public static bool IsHeartbeatMessage(this PacketHeader h) => h.HasFlag(PacketHeader.IsHeartbeatMessage); + + public static bool IsDataFragment(this PacketHeader h) + => h.HasFlag(PacketHeader.IsDataFragment); + } + + public static class NetworkMagicStrings + { + // This separator exists because Lidgren's disconnect messages + // can only readily support strings. We want to send something that + // isn't exactly a string, so we use this as part of its encoding. + public const string LidgrenDisconnectSeparator = "}Separator["; } } diff --git a/Libraries/BarotraumaLibs/BarotraumaCore/Social/FriendStatus.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Social/FriendStatus.cs new file mode 100644 index 000000000..b3b46eee6 --- /dev/null +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Social/FriendStatus.cs @@ -0,0 +1,9 @@ +namespace Barotrauma; + +public enum FriendStatus +{ + Offline, + NotPlaying, + PlayingAnotherGame, + PlayingBarotrauma +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/CollectionConcat.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/CollectionConcat.cs similarity index 100% rename from Barotrauma/BarotraumaShared/SharedSource/Utils/CollectionConcat.cs rename to Libraries/BarotraumaLibs/BarotraumaCore/Utils/CollectionConcat.cs diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Either.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/Either.cs similarity index 92% rename from Barotrauma/BarotraumaShared/SharedSource/Utils/Either.cs rename to Libraries/BarotraumaLibs/BarotraumaCore/Utils/Either.cs index 8d863767f..ffee31838 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Either.cs +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/Either.cs @@ -27,6 +27,14 @@ namespace Barotrauma public static bool operator !=(Either? a, Either? b) => !(a == b); + + public V Match(Func t, Func u) + => this switch + { + EitherT e => t(e.Value), + EitherU e => u(e.Value), + _ => throw new Exception("Invalid Either type") + }; } public sealed class EitherT : Either where T : notnull where U : notnull diff --git a/Libraries/BarotraumaLibs/BarotraumaCore/Utils/GameVersion.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/GameVersion.cs new file mode 100644 index 000000000..3460e7db8 --- /dev/null +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/GameVersion.cs @@ -0,0 +1,9 @@ +using System; +using System.Reflection; +namespace Barotrauma; + +public static class GameVersion +{ + public static readonly Version CurrentVersion + = Assembly.GetEntryAssembly()?.GetName().Version ?? new Version(0,0,0,0); +} diff --git a/Libraries/BarotraumaLibs/BarotraumaCore/Utils/IEnumerableExtensionsCore.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/IEnumerableExtensionsCore.cs new file mode 100644 index 000000000..76fce3681 --- /dev/null +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/IEnumerableExtensionsCore.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma.Extensions; + +public static class IEnumerableExtensionsCore +{ + /// + /// Returns the maximum element in a given enumerable, or null if there + /// aren't any elements in the input. + /// + /// Input collection + /// Maximum element or null + public static T? MaxOrNull(this IEnumerable enumerable) where T : struct, IComparable + { + T? retVal = null; + foreach (T v in enumerable) + { + if (!retVal.HasValue || v.CompareTo(retVal.Value) > 0) { retVal = v; } + } + return retVal; + } + + public static TOut? MaxOrNull(this IEnumerable enumerable, Func conversion) + where TOut : struct, IComparable + => enumerable.Select(conversion).MaxOrNull(); + + public static int FindIndex(this IReadOnlyList list, Predicate predicate) + { + for (int i = 0; i < list.Count; i++) + { + if (predicate(list[i])) { return i; } + } + return -1; + } + + /// + /// Same as FirstOrDefault but will always return null instead of default(T) when no element is found + /// + public static T? FirstOrNull(this IEnumerable source, Func predicate) where T : struct + => source.FirstOrNone(predicate).TryUnwrap(out T t) ? t : null; + + public static T? FirstOrNull(this IEnumerable source) where T : struct + => source.FirstOrNone().TryUnwrap(out T t) ? t : null; + + public static Option FirstOrNone(this IEnumerable source, Func predicate) where T : notnull + { + foreach (T t in source) + { + if (predicate(t)) { return Option.Some(t); } + } + return Option.None; + } + + public static Option FirstOrNone(this IEnumerable source) where T : notnull + { + using IEnumerator enumerator = source.GetEnumerator(); + return enumerator.MoveNext() + ? Option.Some(enumerator.Current) + : Option.None; + } + + public static IEnumerable NotNone(this IEnumerable> source) where T : notnull + { + foreach (var o in source) + { + if (o.TryUnwrap(out var v)) { yield return v; } + } + } + + public static IEnumerable Successes( + this IEnumerable> source) + where TSuccess : notnull + where TFailure : notnull + => source + .OfType>() + .Select(s => s.Value); + + public static IEnumerable Failures( + this IEnumerable> source) + where TSuccess : notnull + where TFailure : notnull + => source + .OfType>() + .Select(f => f.Error); +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/Identifier.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/Identifier.cs similarity index 82% rename from Barotrauma/BarotraumaShared/SharedSource/ContentManagement/Identifier.cs rename to Libraries/BarotraumaLibs/BarotraumaCore/Utils/Identifier.cs index 7fb3453ce..e1b8f7c0d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/Identifier.cs +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/Identifier.cs @@ -32,16 +32,16 @@ namespace Barotrauma => IsEmpty ? id : this; public Identifier Replace(in Identifier subStr, in Identifier newStr) - => Replace(subStr.Value ?? "", newStr.Value ?? ""); + => Replace(subStr.Value, newStr.Value); public Identifier Replace(string subStr, string newStr) - => (Value?.Replace(subStr, newStr, StringComparison.OrdinalIgnoreCase)).ToIdentifier(); + => Value.Replace(subStr, newStr, StringComparison.OrdinalIgnoreCase).ToIdentifier(); public Identifier Remove(Identifier subStr) - => Remove(subStr.Value ?? ""); + => Remove(subStr.Value); public Identifier Remove(string subStr) - => (Value?.Remove(subStr, StringComparison.OrdinalIgnoreCase)).ToIdentifier(); + => Value.Remove(subStr, StringComparison.OrdinalIgnoreCase).ToIdentifier(); public override bool Equals(object? obj) => obj switch @@ -51,25 +51,25 @@ namespace Barotrauma _ => base.Equals(obj) }; - public bool StartsWith(string str) => Value?.StartsWith(str, StringComparison.OrdinalIgnoreCase) ?? str.IsNullOrEmpty(); + public bool StartsWith(string str) => Value.StartsWith(str, StringComparison.OrdinalIgnoreCase); - public bool StartsWith(Identifier id) => StartsWith(id.Value ?? ""); + public bool StartsWith(Identifier id) => StartsWith(id.Value); - public bool EndsWith(string str) => Value?.EndsWith(str, StringComparison.OrdinalIgnoreCase) ?? str.IsNullOrEmpty(); + public bool EndsWith(string str) => Value.EndsWith(str, StringComparison.OrdinalIgnoreCase); - public bool EndsWith(Identifier id) => EndsWith(id.Value ?? ""); + public bool EndsWith(Identifier id) => EndsWith(id.Value); public Identifier AppendIfMissing(string suffix) => EndsWith(suffix) ? this : $"{this}{suffix}".ToIdentifier(); public Identifier RemoveFromEnd(string suffix) - => (Value?.RemoveFromEnd(suffix, StringComparison.OrdinalIgnoreCase)).ToIdentifier(); + => Value.RemoveFromEnd(suffix, StringComparison.OrdinalIgnoreCase).ToIdentifier(); - public bool Contains(string str) => Value?.Contains(str, StringComparison.OrdinalIgnoreCase) ?? str.IsNullOrEmpty(); + public bool Contains(string str) => Value.Contains(str, StringComparison.OrdinalIgnoreCase); - public bool Contains(in Identifier id) => Contains(id.Value ?? ""); + public bool Contains(in Identifier id) => Contains(id.Value); - public override string ToString() => Value ?? ""; + public override string ToString() => Value; public override int GetHashCode() => HashCode; @@ -122,10 +122,10 @@ namespace Barotrauma public static bool operator !=(string str, in Identifier? identifier) => !(identifier == str); - internal int IndexOf(char c) => Value.IndexOf(c); + public int IndexOf(char c) => Value.IndexOf(c); - internal Identifier this[Range range] => Value[range].ToIdentifier(); - internal Char this[int i] => Value[i]; + public Identifier this[Range range] => Value[range].ToIdentifier(); + public Char this[int i] => Value[i]; } public static class IdentifierExtensions diff --git a/Libraries/BarotraumaLibs/BarotraumaCore/Utils/Janitor.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/Janitor.cs new file mode 100644 index 000000000..9c9b467d8 --- /dev/null +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/Janitor.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Barotrauma; + +/// +/// This type is intended to be used in using statements to automatically +/// clean up resources that are allocated incrementally +/// +public readonly struct Janitor : IDisposable +{ + private readonly List cleanupActions; + private Janitor(List cleanupActions) + { + this.cleanupActions = cleanupActions; + } + + public static Janitor Start() + => new Janitor(new List()); + + /// + /// Give the janitor a new action to perform when disposed + /// + public void AddAction([NotNull]Action action) + { + // Null check to punish misuse early instead of having the Janitor blow up upon disposal. + // Make sure you use nullable contexts so the compiler will stop you instead! + if (action is null) + { + throw new ArgumentException($"Cannot add null as an action for {nameof(Janitor)}"); + } + cleanupActions.Add(action); + } + + /// + /// Relieve the janitor of all current duties, + /// i.e. all of the currently enqueued cleanup + /// actions are cleared and will not execute + /// + public void Dismiss() + => cleanupActions.Clear(); + + public void Dispose() + { + cleanupActions.ForEach(a => a()); + Dismiss(); + } +} \ No newline at end of file diff --git a/Libraries/BarotraumaLibs/BarotraumaCore/Utils/JsonWebToken.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/JsonWebToken.cs new file mode 100644 index 000000000..b044b1ef2 --- /dev/null +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/JsonWebToken.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Text; + +namespace Barotrauma; + +public static class UnixTime +{ + public static readonly DateTime UtcEpoch = new DateTime(year: 1970, month: 1, day: 1, hour: 0, minute: 0, second: 0, kind: DateTimeKind.Utc); + + public static Option ParseUtc(string str) + { + if (!ulong.TryParse(str, out var seconds)) { return Option.None; } + return Option.Some(UtcEpoch + TimeSpan.FromSeconds(seconds)); + } +} + +/// +/// URL-safe Base64. See https://datatracker.ietf.org/doc/html/rfc4648#section-5 +/// +public static class Base64Url +{ + public static bool IsBase64Url(this string str) + => str.All(c + => c is + (>= 'A' and <= 'Z') + or (>= 'a' and <= 'z') + or (>= '0' and <= '9') + or '-' or '_'); + + public static Option DecodeUtf8String(string encodedForm) + { + return DecodeBytes(encodedForm).Select(bytes => Encoding.UTF8.GetString(bytes.AsSpan())); + } + + public static Option> DecodeBytes(string encodedForm) + { + if (!encodedForm.IsBase64Url()) { return Option.None; } + string base64Form = encodedForm.Replace("-", "+").Replace("_", "/"); + base64Form += new string('=', (4 - (base64Form.Length % 4)) % 4); + return Option.Some(Convert.FromBase64String(base64Form).ToImmutableArray()); + } +} + +/// +/// Rudimentary JSON Web Token implementation. See https://en.wikipedia.org/wiki/JSON_Web_Token. +/// This is used by continuance tokens and ID tokens as part of their internal representation. +/// We can use the data encoded in them for some things, such as determining a token's expiry time. +/// +public readonly record struct JsonWebToken( + string Header, + string Payload, + string Signature) +{ + public bool IsValid => Header.IsBase64Url() && Payload.IsBase64Url() && Signature.IsBase64Url(); + + public override string ToString() + => $"{Header}.{Payload}.{Signature}"; + + public string HeaderDecoded => Base64Url.DecodeUtf8String(Header).Fallback(""); + public string PayloadDecoded => Base64Url.DecodeUtf8String(Payload).Fallback(""); + + public static Option Parse(string str) + { + if (str.Split(".") is not { Length: 3 } split) { return Option.None; } + var newToken = new JsonWebToken( + Header: split[0], + Payload: split[1], + Signature: split[2]); + if (!newToken.IsValid) { return Option.None; } + return Option.Some(newToken); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/MathUtils.cs similarity index 98% rename from Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs rename to Libraries/BarotraumaLibs/BarotraumaCore/Utils/MathUtils.cs index 1f116b00f..357a3335a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/MathUtils.cs @@ -17,7 +17,7 @@ namespace Barotrauma Any = Left | Right | Top | Bottom | Center } - static class MathUtils + public static class MathUtils { public static Vector2 DiscardZ(this Vector3 vector) => new Vector2(vector.X, vector.Y); @@ -729,7 +729,7 @@ namespace Barotrauma return wrappedPoints; } - public static List GenerateJaggedLine(Vector2 start, Vector2 end, int iterations, float offsetAmount, Rectangle? bounds = null) + public static List GenerateJaggedLine(Vector2 start, Vector2 end, int iterations, float offsetAmount, Random rng, Rectangle? bounds = null) { List segments = new List { @@ -749,7 +749,7 @@ namespace Barotrauma Vector2 normal = Vector2.Normalize(endSegment - startSegment); normal = new Vector2(-normal.Y, normal.X); - midPoint += normal * Rand.Range(-offsetAmount, offsetAmount, Rand.RandSync.ServerAndClient); + midPoint += normal * rng.Range(-offsetAmount, offsetAmount); if (bounds.HasValue) { @@ -886,6 +886,7 @@ namespace Barotrauma // https://stackoverflow.com/questions/3874627/floating-point-comparison-functions-for-c-sharp public static bool NearlyEqual(float a, float b, float epsilon = 0.0001f) { + // ReSharper disable once CompareOfFloatsByEqualityOperator if (a == b) { //shortcut, handles infinities @@ -1089,9 +1090,19 @@ namespace Barotrauma { return vals.Max(); } + + public static uint RoundUpToPowerOfTwo(uint val) + { + // Handle the input exceeding the max power of two uint can represent + if (val > (uint.MaxValue >> 1)) { return (uint.MaxValue >> 1) + 1; } + + uint po2 = 1; + while (val > po2) { po2 <<= 1; } + return po2; + } } - class CompareCW : IComparer + public class CompareCW : IComparer { private Vector2 center; @@ -1128,7 +1139,7 @@ namespace Barotrauma } } - class CompareCCW : IComparer + public class CompareCCW : IComparer { private Vector2 center; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/NamedEvent.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/NamedEvent.cs similarity index 62% rename from Barotrauma/BarotraumaShared/SharedSource/Utils/NamedEvent.cs rename to Libraries/BarotraumaLibs/BarotraumaCore/Utils/NamedEvent.cs index 3f80b5500..0d6283bc2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/NamedEvent.cs +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/NamedEvent.cs @@ -1,35 +1,28 @@ using System; -using System.Collections.Generic; +using System.Collections.Concurrent; namespace Barotrauma { - internal sealed class NamedEvent : IDisposable + public sealed class NamedEvent : IDisposable { - private readonly Dictionary> events = new Dictionary>(); + private readonly ConcurrentDictionary> events = new ConcurrentDictionary>(); public void Register(Identifier identifier, Action action) { - if (HasEvent(identifier)) + if (!events.TryAdd(identifier, action)) { throw new ArgumentException($"Event with the identifier \"{identifier}\" has already been registered.", nameof(identifier)); } - - events.Add(identifier, action); } public void RegisterOverwriteExisting(Identifier identifier, Action action) { - if (HasEvent(identifier)) - { - Deregister(identifier); - } - - Register(identifier, action); + events.AddOrUpdate(identifier, action, (k, v) => action); } public void Deregister(Identifier identifier) { - events.Remove(identifier); + events.TryRemove(identifier, out _); } public void TryDeregister(Identifier identifier) @@ -38,7 +31,8 @@ namespace Barotrauma Deregister(identifier); } - public bool HasEvent(Identifier identifier) => events.ContainsKey(identifier); + public bool HasEvent(Identifier identifier) + => events.ContainsKey(identifier); public void Invoke(T data) { diff --git a/Libraries/BarotraumaLibs/BarotraumaCore/Utils/OneOf.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/OneOf.cs new file mode 100644 index 000000000..4fe11c542 --- /dev/null +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/OneOf.cs @@ -0,0 +1,49 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Barotrauma; + +/// +/// Discriminated union of three types. +/// Essentially the same thing as Either<T1, T2>, except for three types instead of two types. +/// +public readonly struct OneOf + where T1 : notnull + where T2 : notnull + where T3 : notnull +{ + private readonly Option value1; + private readonly Option value2; + private readonly Option value3; + + private OneOf(Option value1, Option value2, Option value3) + { + this.value1 = value1; + this.value2 = value2; + this.value3 = value3; + } + + public static implicit operator OneOf(T1 value1) + => new OneOf(value1: Option.Some(value1), value2: Option.None, value3: Option.None); + public static implicit operator OneOf(T2 value2) + => new OneOf(value1: Option.None, value2: Option.Some(value2), value3: Option.None); + public static implicit operator OneOf(T3 value3) + => new OneOf(value1: Option.None, value2: Option.None, value3: Option.Some(value3)); + + public bool TryGet([NotNullWhen(returnValue: true)] out T1? t1) + => value1.TryUnwrap(out t1); + public bool TryGet([NotNullWhen(returnValue: true)] out T2? t2) + => value2.TryUnwrap(out t2); + public bool TryGet([NotNullWhen(returnValue: true)] out T3? t3) + => value3.TryUnwrap(out t3); + + private static string ObjectToStringWithType(T obj) + => $"{obj}: {typeof(T).Name}"; + + public override string ToString() + => $"OneOf<{typeof(T1).Name}, {typeof(T2).Name}, {typeof(T3).Name}>(" + + value1.Select(ObjectToStringWithType) + .Fallback(value2.Select(ObjectToStringWithType)) + .Fallback(value3.Select(ObjectToStringWithType)) + .Fallback("None") + + ")"; +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/Option/Option.cs similarity index 94% rename from Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs rename to Libraries/BarotraumaLibs/BarotraumaCore/Utils/Option/Option.cs index 63e0d5af6..75714375c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/Option/Option.cs @@ -1,6 +1,7 @@ #nullable enable using System; using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; namespace Barotrauma { @@ -41,6 +42,9 @@ namespace Barotrauma public Option Bind(Func> binder) where TType : notnull => TryUnwrap(out T? selfValue) ? binder(selfValue) : Option.None; + public async Task> Bind(Func>> binder) where TType : notnull + => TryUnwrap(out T? selfValue) ? await binder(selfValue) : Option.None; + public T Match(Func some, Func none) => TryUnwrap(out T? selfValue) ? some(selfValue) : none(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Range.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/Range.cs similarity index 100% rename from Barotrauma/BarotraumaShared/SharedSource/Utils/Range.cs rename to Libraries/BarotraumaLibs/BarotraumaCore/Utils/Range.cs diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/ReflectionUtils.cs similarity index 79% rename from Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs rename to Libraries/BarotraumaLibs/BarotraumaCore/Utils/ReflectionUtils.cs index feedf094a..b89762131 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/ReflectionUtils.cs @@ -15,6 +15,22 @@ namespace Barotrauma private static readonly Dictionary>> cachedDerivedNonAbstract = new Dictionary>>(); + public static T GetValueFromStaticProperty(this PropertyInfo property) + { + if (property.GetMethod is not { IsStatic: true }) + { + throw new ArgumentException($"Property {property} is not static"); + } + + var value = property.GetValue(obj: null); + if (value is not T castValue) + { + throw new ArgumentException($"Property {property} is null or not of type {typeof(T)}"); + } + + return castValue; + } + public static IEnumerable GetDerivedNonAbstract() { Type t = typeof(T); @@ -35,7 +51,19 @@ namespace Barotrauma return newArray; } - public static Option ParseDerived(TInput input) where TInput : notnull where TBase : notnull + public static Type? GetType(string nameWithNamespace) + { + if (Type.GetType(nameWithNamespace) is Type t) { return t; } + + var entryAssembly = Assembly.GetEntryAssembly(); + if (entryAssembly?.GetType(nameWithNamespace) is Type t2) { return t2; } + + return null; + } + + public static Option ParseDerived(TInput input) + where TBase : notnull + where TInput : notnull { static Option none() => Option.None(); @@ -75,7 +103,7 @@ namespace Barotrauma public static string NameWithGenerics(this Type t) { if (!t.IsGenericType) { return t.Name; } - + string result = t.Name[..t.Name.IndexOf('`')]; result += $"<{string.Join(", ", t.GetGenericArguments().Select(NameWithGenerics))}>"; return result; diff --git a/Libraries/BarotraumaLibs/BarotraumaCore/Utils/Result.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/Result.cs new file mode 100644 index 000000000..e2ea051f0 --- /dev/null +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/Result.cs @@ -0,0 +1,120 @@ +#nullable enable +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Barotrauma +{ + public abstract class Result + where TSuccess: notnull + where TFailure: notnull + { + public abstract bool IsSuccess { get; } + public bool IsFailure => !IsSuccess; + + public static Success Success(TSuccess value) + => new Success(value); + + public static Failure Failure(TFailure error) + => new Failure(error); + + public abstract bool TryUnwrapSuccess([NotNullWhen(returnValue: true)] out TSuccess? value); + public abstract bool TryUnwrapFailure([NotNullWhen(returnValue: true)] out TFailure? value); + + public abstract override string ToString(); + + public static (Func> Success, Func> Failure) GetFactoryMethods() + => (Success, Failure); + + public static implicit operator Result(Result.UnspecifiedSuccess unspecifiedSuccess) + => Success(unspecifiedSuccess.Value); + + public static implicit operator Result(Result.UnspecifiedFailure unspecifiedFailure) + => Failure(unspecifiedFailure.Value); + + public void Match(Action success, Action failure) + { + if (TryUnwrapSuccess(out var successValue)) { success(successValue); } + if (TryUnwrapFailure(out var failureValue)) { failure(failureValue); } + } + } + + public sealed class Success : Result + where TSuccess: notnull + where TFailure: notnull + { + public readonly TSuccess Value; + public override bool IsSuccess => true; + + public override bool TryUnwrapSuccess([MaybeNullWhen(returnValue: false)] out TSuccess value) + { + value = Value; + return true; + } + + public override bool TryUnwrapFailure([MaybeNullWhen(returnValue: false)] out TFailure value) + { + value = default; + return false; + } + + public override string ToString() + => $"Success<{typeof(TSuccess).NameWithGenerics()}, {typeof(TFailure).NameWithGenerics()}>({Value})"; + + public Success(TSuccess value) + { + Value = value; + } + } + + public sealed class Failure : Result + where TSuccess: notnull + where TFailure: notnull + { + public readonly TFailure Error; + + public override bool IsSuccess => false; + + public override bool TryUnwrapSuccess([MaybeNullWhen(returnValue: false)] out TSuccess value) + { + value = default; + return false; + } + + public override bool TryUnwrapFailure([MaybeNullWhen(returnValue: false)] out TFailure value) + { + value = Error; + return true; + } + + public override string ToString() + => $"Failure<{typeof(TSuccess).NameWithGenerics()}, {typeof(TFailure).NameWithGenerics()}>({Error})"; + + public Failure(TFailure error) + { + Error = error; + } + } + + public static class Result + { + public readonly ref struct UnspecifiedSuccess + where TSuccess : notnull + { + internal readonly TSuccess Value; + internal UnspecifiedSuccess(TSuccess value) { Value = value; } + } + + public readonly ref struct UnspecifiedFailure + where TFailure : notnull + { + internal readonly TFailure Value; + internal UnspecifiedFailure(TFailure value) { Value = value; } + } + + public static UnspecifiedSuccess Success(TSuccess value) where TSuccess : notnull + => new UnspecifiedSuccess(value); + + public static UnspecifiedFailure Failure(TFailure value) where TFailure : notnull + => new UnspecifiedFailure(value); + } +} diff --git a/Libraries/BarotraumaLibs/BarotraumaCore/Utils/TaskExtensions.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/TaskExtensions.cs new file mode 100644 index 000000000..367a77023 --- /dev/null +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/TaskExtensions.cs @@ -0,0 +1,40 @@ +#nullable enable +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; + +namespace Barotrauma +{ + public static class TaskExtensionsCore + { + public static async Task> ToOptionTask(this Task nullableTask) where T : struct + { + var nullableResult = await nullableTask; + return nullableResult is { } result + ? Option.Some(result) + : Option.None; + } + + public static bool TryGetResult(this Task task, [NotNullWhen(returnValue: true)]out T? result) where T : notnull + { + if (task is Task { IsCompletedSuccessfully: true, Result: not null } castTask) + { + result = castTask.Result; + return true; + } +#if DEBUG + if (task.Exception != null) + { + var ex = task.Exception.GetInnermost(); + throw new InvalidOperationException($"Failed to get result from task: task failed with exception {ex.Message} ({ex.GetType()}) {ex.StackTrace}"); + } + if (task is not Task) + { + throw new InvalidOperationException($"Failed to get result from task: expected Task<{typeof(T).NameWithGenerics()}>, got {task.GetType().NameWithGenerics()}"); + } +#endif + result = default; + return false; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskPool.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/TaskPool.cs similarity index 58% rename from Barotrauma/BarotraumaShared/SharedSource/Utils/TaskPool.cs rename to Libraries/BarotraumaLibs/BarotraumaCore/Utils/TaskPool.cs index f2b0fceb6..b2c004a48 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskPool.cs +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/TaskPool.cs @@ -1,33 +1,37 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading.Tasks; namespace Barotrauma { public static class TaskPool { + /// + /// Empty callback that can be used when we do not care about the completion status of a task. + /// + public static void IgnoredCallback(Task task) { } + const int MaxTasks = 5000; private struct TaskAction { public string Name; public Task Task; - public Action OnCompletion; - public object UserData; + public Action OnCompletion; + public object? UserData; } private static readonly List taskActions = new List(); - public static void ListTasks() + public static void ListTasks(Action log) { lock (taskActions) { - DebugConsole.NewMessage($"Task count: {taskActions.Count}"); + log($"Task count: {taskActions.Count}"); for (int i = 0; i < taskActions.Count; i++) { - DebugConsole.NewMessage($" -{i}: {taskActions[i].Name}, {taskActions[i].Task.Status}"); + log($" -{i}: {taskActions[i].Name}, {taskActions[i].Task.Status}"); } } } @@ -40,7 +44,7 @@ namespace Barotrauma } } - private static void AddInternal(string name, Task task, Action onCompletion, object userdata, bool addIfFound = true) + private static void AddInternal(string name, Task task, Action onCompletion, object? userdata, bool addIfFound = true) { lock (taskActions) { @@ -55,22 +59,27 @@ namespace Barotrauma ); } taskActions.Add(new TaskAction() { Name = name, Task = task, OnCompletion = onCompletion, UserData = userdata }); - DebugConsole.Log($"New task: {name} ({taskActions.Count}/{MaxTasks})"); } } - public static void Add(string name, Task task, Action onCompletion) + public static Unit Add(string name, Task task, Action? onCompletion) { - AddInternal(name, task, (Task t, object obj) => { onCompletion?.Invoke(t); }, null); - } - public static void AddIfNotFound(string name, Task task, Action onCompletion) - { - AddInternal(name, task, (Task t, object obj) => { onCompletion?.Invoke(t); }, null, addIfFound: false); + AddInternal(name, task, (t, _) => { onCompletion?.Invoke(t); }, null); + return Unit.Value; } - public static void Add(string name, Task task, U userdata, Action onCompletion) where U : class + public static Unit AddWithResult(string name, Task task, Action? onCompletion) where T : notnull { - AddInternal(name, task, (Task t, object obj) => { onCompletion?.Invoke(t, (U)obj); }, userdata); + AddInternal(name, task, (t, _) => + { + if (t.TryGetResult(out T? result)) { onCompletion?.Invoke(result); } + }, null); + return Unit.Value; + } + public static Unit AddIfNotFound(string name, Task task, Action onCompletion) + { + AddInternal(name, task, (t, _) => { onCompletion?.Invoke(t); }, null, addIfFound: false); + return Unit.Value; } public static void Update() @@ -82,7 +91,7 @@ namespace Barotrauma if (taskActions[i].Task.IsCompleted) { taskActions[i].OnCompletion?.Invoke(taskActions[i].Task, taskActions[i].UserData); - DebugConsole.Log($"Task {taskActions[i].Name} completed ({taskActions.Count-1}/{MaxTasks})"); + taskActions[i].Task.Dispose(); taskActions.RemoveAt(i); i--; } @@ -90,12 +99,12 @@ namespace Barotrauma } } - public static void PrintTaskExceptions(Task task, string msg) + public static void PrintTaskExceptions(Task task, string msg, Action throwError) { - DebugConsole.ThrowError(msg); - foreach (Exception e in task.Exception.InnerExceptions) + throwError(msg); + foreach (Exception e in task.Exception?.InnerExceptions ?? Enumerable.Empty()) { - DebugConsole.ThrowError(e.Message + "\n" + e.StackTrace.CleanupStackTrace()); + throwError($"{e.Message}\n{e.StackTrace}"); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Threading.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/Threading.cs similarity index 88% rename from Barotrauma/BarotraumaShared/SharedSource/Utils/Threading.cs rename to Libraries/BarotraumaLibs/BarotraumaCore/Utils/Threading.cs index 25a2ac7fa..a5aa1ad4d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Threading.cs +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/Threading.cs @@ -2,7 +2,7 @@ using System.Threading; namespace Barotrauma.Threading { - internal readonly ref struct ReadLock + public readonly ref struct ReadLock { private readonly ReaderWriterLockSlim rwl; public ReadLock(ReaderWriterLockSlim rwl) @@ -17,7 +17,7 @@ namespace Barotrauma.Threading } } - internal readonly ref struct WriteLock + public readonly ref struct WriteLock { private readonly ReaderWriterLockSlim rwl; public WriteLock(ReaderWriterLockSlim rwl) diff --git a/Libraries/BarotraumaLibs/BarotraumaCore/Utils/ToolBoxCore.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/ToolBoxCore.cs new file mode 100644 index 000000000..d4c7f54f2 --- /dev/null +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/ToolBoxCore.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using Microsoft.Xna.Framework; + +namespace Barotrauma; + +public static class ToolBoxCore +{ + public static string ByteArrayToHexString(IReadOnlyList ba) + { + var hex = new StringBuilder(ba.Count * 2); + foreach (byte b in ba) + { + hex.AppendFormat("{0:X2}", b); + } + return hex.ToString(); + } + + public static byte[] HexStringToByteArray(string str) + { + var byteRepresentation = new byte[str.Length / 2]; + for (int i = 0; i < byteRepresentation.Length; i++) + { + byteRepresentation[i] = Convert.ToByte(str.Substring(i * 2, 2), 16); + } + + return byteRepresentation; + } + + public static bool IsHexadecimalDigit(this char c) + => char.IsDigit(c) + || c is (>= 'a' and <= 'f') or (>= 'A' and <= 'F'); + + public static bool IsHexString(this string s) + => !s.IsNullOrEmpty() && s.All(IsHexadecimalDigit); + + public static UInt32 IdentifierToUint32Hash(Identifier id, MD5 md5) + => StringToUInt32Hash(id.Value.ToLowerInvariant(), md5); + + public static UInt32 StringToUInt32Hash(string str, MD5 md5) + { + //calculate key based on MD5 hash instead of string.GetHashCode + //to ensure consistent results across platforms + byte[] inputBytes = Encoding.UTF8.GetBytes(str); + byte[] hash = md5.ComputeHash(inputBytes); + + UInt32 key = (UInt32)((str.Length & 0xff) << 24); //could use more of the hash here instead? + key |= (UInt32)(hash[hash.Length - 3] << 16); + key |= (UInt32)(hash[hash.Length - 2] << 8); + key |= (UInt32)(hash[hash.Length - 1]); + + return key; + } + + /// + /// Convert a HSV value into a RGB value. + /// + /// Value between 0 and 360 + /// Value between 0 and 1 + /// Value between 0 and 1 + /// Reference + /// + public static Color HSVToRGB(float hue, float saturation, float value) + { + float c = value * saturation; + + float h = Math.Clamp(hue, 0, 360) / 60f; + + float x = c * (1 - Math.Abs(h % 2 - 1)); + + float r = 0, + g = 0, + b = 0; + + if (0 <= h && h <= 1) { r = c; g = x; b = 0; } + else if (1 < h && h <= 2) { r = x; g = c; b = 0; } + else if (2 < h && h <= 3) { r = 0; g = c; b = x; } + else if (3 < h && h <= 4) { r = 0; g = x; b = c; } + else if (4 < h && h <= 5) { r = x; g = 0; b = c; } + else if (5 < h && h <= 6) { r = c; g = 0; b = x; } + + float m = value - c; + + return new Color(r + m, g + m, b + m); + } + + public static Exception GetInnermost(this Exception e) + { + while (e.InnerException != null) { e = e.InnerException; } + + return e; + } +} diff --git a/Libraries/BarotraumaLibs/BarotraumaCore/Utils/Unit.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/Unit.cs new file mode 100644 index 000000000..3a6a77e3b --- /dev/null +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/Unit.cs @@ -0,0 +1,8 @@ +namespace Barotrauma; + +/// +/// Unit type, i.e. type with only one possible value. +/// Can be used instead of void to form expressions and +/// fill in generic parameters. +/// +public enum Unit { Value } diff --git a/Libraries/BarotraumaLibs/BarotraumaCore/Utils/UnreachableCodeException.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/UnreachableCodeException.cs new file mode 100644 index 000000000..b84505bc2 --- /dev/null +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/UnreachableCodeException.cs @@ -0,0 +1,8 @@ +using System; + +namespace Barotrauma; + +public sealed class UnreachableCodeException : Exception +{ + public UnreachableCodeException() : base(message: "Code that was supposed to be unreachable was executed.") { } +} \ No newline at end of file diff --git a/Libraries/BarotraumaLibs/EosInterface/Achievements/AchievementErrors.cs b/Libraries/BarotraumaLibs/EosInterface/Achievements/AchievementErrors.cs new file mode 100644 index 000000000..9a61d4525 --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterface/Achievements/AchievementErrors.cs @@ -0,0 +1,45 @@ +namespace Barotrauma; + +public static partial class EosInterface +{ + public enum AchievementUnlockError + { + Unknown, + InvalidUser, + EosNotInitialized, + TimedOut, + InvalidParameters, + NotFound + } + + public enum IngestStatError + { + Unknown, + InvalidUser, + EosNotInitialized, + TimedOut, + InvalidParameters, + NotFound + } + + public enum QueryStatsError + { + Unknown, + InvalidUser, + EosNotInitialized, + TimedOut, + InvalidParameters, + NotFound + } + + public enum QueryAchievementsError + { + Unknown, + InvalidUser, + InvalidProductUserID, + EosNotInitialized, + TimedOut, + InvalidParameters, + NotFound + } +} \ No newline at end of file diff --git a/Libraries/BarotraumaLibs/EosInterface/Achievements/Achievements.cs b/Libraries/BarotraumaLibs/EosInterface/Achievements/Achievements.cs new file mode 100644 index 000000000..1506c282a --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterface/Achievements/Achievements.cs @@ -0,0 +1,55 @@ +using System.Collections.Immutable; +using System.Threading.Tasks; + +namespace Barotrauma; + +public static partial class EosInterface +{ + public static class Achievements + { + private static Implementation? LoadedImplementation => Core.LoadedImplementation; + + public static async Task> UnlockAchievements( + params Identifier[] achievementIds) + => LoadedImplementation.IsInitialized() + ? await LoadedImplementation.UnlockAchievements(achievementIds) + : Result.Failure(AchievementUnlockError.EosNotInitialized); + + public static async Task> IngestStats( + params (AchievementStat Stat, int IngestAmount)[] stats) + => LoadedImplementation.IsInitialized() + ? await LoadedImplementation.IngestStats(stats) + : Result.Failure(IngestStatError.EosNotInitialized); + + public static Task, QueryStatsError>> QueryStats( + params AchievementStat[] stats) + => QueryStats(stats.ToImmutableArray()); + + public static async Task, QueryStatsError>> QueryStats( + ImmutableArray stats) + => LoadedImplementation.IsInitialized() + ? await LoadedImplementation.QueryStats(stats) + : Result.Failure(QueryStatsError.EosNotInitialized); + + public static async Task, QueryAchievementsError>> + QueryPlayerAchievements() + => LoadedImplementation.IsInitialized() + ? await LoadedImplementation.QueryPlayerAchievements() + : Result.Failure(QueryAchievementsError.EosNotInitialized); + } + + internal abstract partial class Implementation + { + public abstract Task> UnlockAchievements( + params Identifier[] achievementIds); + + public abstract Task> IngestStats( + params (AchievementStat Stat, int IngestAmount)[] stats); + + public abstract Task, QueryStatsError>> QueryStats( + ImmutableArray stats); + + public abstract Task, QueryAchievementsError>> + QueryPlayerAchievements(); + } +} \ No newline at end of file diff --git a/Libraries/BarotraumaLibs/EosInterface/Core/ApplicationCredentials.cs b/Libraries/BarotraumaLibs/EosInterface/Core/ApplicationCredentials.cs new file mode 100644 index 000000000..c1b52d1d7 --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterface/Core/ApplicationCredentials.cs @@ -0,0 +1,10 @@ +namespace Barotrauma; + +public static partial class EosInterface +{ + public enum ApplicationCredentials + { + Client, + Server + } +} \ No newline at end of file diff --git a/Libraries/BarotraumaLibs/EosInterface/Core/AssemblyInfo.cs b/Libraries/BarotraumaLibs/EosInterface/Core/AssemblyInfo.cs new file mode 100644 index 000000000..cd8161fba --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterface/Core/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("EosInterface.Implementation.Win64")] +[assembly: InternalsVisibleTo("EosInterface.Implementation.MacOS")] +[assembly: InternalsVisibleTo("EosInterface.Implementation.Linux")] \ No newline at end of file diff --git a/Libraries/BarotraumaLibs/EosInterface/Core/Core.cs b/Libraries/BarotraumaLibs/EosInterface/Core/Core.cs new file mode 100644 index 000000000..cdd8f8ecb --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterface/Core/Core.cs @@ -0,0 +1,258 @@ +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Runtime.Loader; + +namespace Barotrauma; + +public static partial class EosInterface +{ + public static class Core + { + internal static Implementation? LoadedImplementation { get; private set; } = null; + private static AssemblyLoadContext? assemblyLoadContext = null; + + private static bool hasShutDown = false; + private static bool failedToInitialize = false; + + private static string GetAssemblyPath(string assemblyName) + => Path.Combine( + Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, + $"{assemblyName}.dll"); + + private static bool resolvingDependency; + + private static Assembly? ResolveDependency(AssemblyLoadContext context, AssemblyName dependencyName) + { + if (resolvingDependency) + { + return null; + } + + resolvingDependency = true; + Assembly dependency = + context.LoadFromAssemblyPath( + GetAssemblyPath(dependencyName.Name ?? throw new Exception("Dependency name was null"))); + resolvingDependency = false; + return dependency; + } + + public enum InitError + { + PlatformInterfaceNotCreated, + AlreadyInitialized, + UnknownOsPlatform, + ImplementationDllLoadFailed, + ImplementationDllHasNoValidClasses, + ImplementationFailedToInstantiate, + NativeDllLoadFailed, + CannotRestartAfterShutdown, + UnhandledErrorCondition + } + + public enum Status + { + NotInitialized, + InitializationError, + ShutDown, + InitializedButOffline, + Online + } + + public static bool IsInitialized + => LoadedImplementation != null && LoadedImplementation.IsInitialized(); + + public static Status CurrentStatus + { + get + { + if (hasShutDown) + { + return Status.ShutDown; + } + + if (failedToInitialize) + { + return Status.InitializationError; + } + + if (LoadedImplementation is { CurrentStatus: var status }) + { + return status; + } + + return Status.NotInitialized; + } + } + + public static Result Init(ApplicationCredentials applicationCredentials, bool enableOverlay) + { + var (success, failure) = Result.GetFactoryMethods(); + if (LoadedImplementation != null) + { + return !LoadedImplementation.IsInitialized() + ? LoadedImplementation.Init(applicationCredentials, enableOverlay) + : failure(InitError.AlreadyInitialized); + } + + if (hasShutDown) + { + return failure(InitError.CannotRestartAfterShutdown); + } + + string platformSuffix; + string nativeDllName; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + platformSuffix = "Win64"; + nativeDllName = "./EOSSDK-Win64-Shipping.dll"; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + platformSuffix = "MacOS"; + nativeDllName = "./libEOSSDK-Mac-Shipping.dylib"; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + platformSuffix = "Linux"; + nativeDllName = "./libEOSSDK-Linux-Shipping.so"; + } + else + { + failedToInitialize = true; + return failure(InitError.UnknownOsPlatform); + } + + if (!NativeLibrary.TryLoad(nativeDllName, out var nativeLib)) + { + failedToInitialize = true; + return failure(InitError.NativeDllLoadFailed); + } + + NativeLibrary.Free(nativeLib); + + string assemblyName = $"EosInterface.Implementation.{platformSuffix}"; + + assemblyLoadContext = new AssemblyLoadContext(assemblyName, isCollectible: true); + assemblyLoadContext.Resolving += ResolveDependency; + + Assembly implementationAssembly; + try + { + implementationAssembly = assemblyLoadContext.LoadFromAssemblyPath(GetAssemblyPath(assemblyName)); + } + catch + { + failedToInitialize = true; + return failure(InitError.ImplementationDllLoadFailed); + } + + var implementationTypes = + implementationAssembly.DefinedTypes + .Where(t => t.IsSubclassOf(typeof(Implementation))) + .Where(t => t is { IsAbstract: false, IsGenericType: false }) + .ToArray(); + if (!implementationTypes.Any()) + { + failedToInitialize = true; + return failure(InitError.ImplementationDllHasNoValidClasses); + } + + Implementation implementationInstance; + try + { + var implementationInstanceNullable = + (Implementation?)Activator.CreateInstance(implementationTypes.First()); + if (implementationInstanceNullable is null) + { + failedToInitialize = true; + return failure(InitError.ImplementationFailedToInstantiate); + } + + implementationInstance = implementationInstanceNullable; + } + catch + { + failedToInitialize = true; + return failure(InitError.ImplementationFailedToInstantiate); + } + + LoadedImplementation = implementationInstance; + + var initResult = implementationInstance.Init(applicationCredentials, enableOverlay); + if (initResult.IsFailure) + { + failedToInitialize = true; + } + + return initResult; + } + + public enum WillRestartThroughLauncher + { + No, + Yes + } + + public enum CheckForLauncherAndRestartError + { + EosNotInitialized, + UnexpectedError, + UnhandledErrorCondition + } + + public static Result CheckForLauncherAndRestart() + => LoadedImplementation.IsInitialized() + ? LoadedImplementation.CheckForLauncherAndRestart() + : Result.Failure(CheckForLauncherAndRestartError.EosNotInitialized); + + public static void Update() + { + if (LoadedImplementation.IsInitialized()) + { + LoadedImplementation.Update(); + } + } + + public static void CleanupAndQuit() + { + var loadedImplementation = LoadedImplementation; + if (!loadedImplementation.IsInitialized()) + { + return; + } + + TaskPool.Add( + "CleanupAndQuit", + loadedImplementation.CloseAllOwnedSessions(), + _ => QuitNow()); + } + + private static void QuitNow() + { + hasShutDown = CurrentStatus != Status.NotInitialized; + LoadedImplementation?.Quit(); + LoadedImplementation = null; + assemblyLoadContext?.Unload(); + assemblyLoadContext = null; + } + } + + internal abstract partial class Implementation + { + public abstract Core.Status CurrentStatus { get; } + public abstract string NativeLibraryName { get; } + + public abstract Result Init(ApplicationCredentials applicationCredentials, + bool enableOverlay); + + public abstract Result + CheckForLauncherAndRestart(); + + public abstract void Update(); + public abstract void Quit(); + } +} \ No newline at end of file diff --git a/Libraries/BarotraumaLibs/EosInterface/Core/StatusExtensions.cs b/Libraries/BarotraumaLibs/EosInterface/Core/StatusExtensions.cs new file mode 100644 index 000000000..25be83be8 --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterface/Core/StatusExtensions.cs @@ -0,0 +1,13 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Barotrauma; + +public static class EosStatusExtensions +{ + public static bool IsInitialized(this EosInterface.Core.Status status) + => status is EosInterface.Core.Status.InitializedButOffline or EosInterface.Core.Status.Online; + + internal static bool IsInitialized( + [NotNullWhen(returnValue: true)] this EosInterface.Implementation? implementation) + => implementation is { CurrentStatus: var status } && status.IsInitialized(); +} \ No newline at end of file diff --git a/Libraries/BarotraumaLibs/EosInterface/EosInterface.csproj b/Libraries/BarotraumaLibs/EosInterface/EosInterface.csproj new file mode 100644 index 000000000..dc10c5393 --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterface/EosInterface.csproj @@ -0,0 +1,26 @@ + + + + net6.0 + disable + enable + Barotrauma + + + + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + true + x64 + + + + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + true + x64 + + + + + + + diff --git a/Libraries/BarotraumaLibs/EosInterface/Friends/EgsFriend.cs b/Libraries/BarotraumaLibs/EosInterface/Friends/EgsFriend.cs new file mode 100644 index 000000000..ae6b6e05a --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterface/Friends/EgsFriend.cs @@ -0,0 +1,13 @@ +using Barotrauma.Networking; + +namespace Barotrauma; + +public static partial class EosInterface +{ + public readonly record struct EgsFriend( + string DisplayName, + EpicAccountId EpicAccountId, + FriendStatus Status, + string ConnectCommand, + string ServerName); +} \ No newline at end of file diff --git a/Libraries/BarotraumaLibs/EosInterface/Friends/Friends.cs b/Libraries/BarotraumaLibs/EosInterface/Friends/Friends.cs new file mode 100644 index 000000000..9ef92c6a3 --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterface/Friends/Friends.cs @@ -0,0 +1,62 @@ +using System.Collections.Immutable; +using System.Threading.Tasks; +using Barotrauma.Networking; + +namespace Barotrauma; + +public static partial class EosInterface +{ + public static class Friends + { + private static Implementation? LoadedImplementation => Core.LoadedImplementation; + + public enum GetFriendsError + { + EosNotInitialized, + + EgsFriendsQueryTimedOut, + EgsFriendsQueryFailed, + + UserInfoQueryTimedOut, + UserInfoQueryFailed, + CopyUserInfoFailed, + DisplayNameIsEmpty, + + EgsPresenceQueryTimedOut, + EgsPresenceQueryFailed, + CopyPresenceFailed, + + UnhandledErrorCondition + } + + public static async Task> GetFriend( + EpicAccountId selfEaid, + EpicAccountId friendEaid) + => LoadedImplementation.IsInitialized() + ? await LoadedImplementation.GetFriend(selfEaid, friendEaid) + : Result.Failure(GetFriendsError.EosNotInitialized); + + public static async Task, GetFriendsError>> GetFriends( + EpicAccountId epicAccountId) + => LoadedImplementation.IsInitialized() + ? await LoadedImplementation.GetFriends(epicAccountId) + : Result.Failure(GetFriendsError.EosNotInitialized); + + public static async Task> GetSelfUserInfo(EpicAccountId epicAccountId) + => LoadedImplementation.IsInitialized() + ? await LoadedImplementation.GetSelfUserInfo(epicAccountId) + : Result.Failure(GetFriendsError.EosNotInitialized); + } + + internal abstract partial class Implementation + { + public abstract Task> GetFriend( + EpicAccountId selfEaid, + EpicAccountId friendEaid); + + public abstract Task, Friends.GetFriendsError>> GetFriends( + EpicAccountId epicAccountId); + + public abstract Task> GetSelfUserInfo(EpicAccountId epicAccountId); + } +} \ No newline at end of file diff --git a/Libraries/BarotraumaLibs/EosInterface/Friends/Presence.cs b/Libraries/BarotraumaLibs/EosInterface/Friends/Presence.cs new file mode 100644 index 000000000..9b794cf27 --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterface/Friends/Presence.cs @@ -0,0 +1,105 @@ +using System.Threading.Tasks; +using Barotrauma.Networking; + +namespace Barotrauma; + +public static partial class EosInterface +{ + public static class Presence + { + public readonly record struct JoinGameInfo( + EpicAccountId RecipientId, + string JoinCommand); + + public readonly record struct AcceptInviteInfo( + EpicAccountId RecipientId, + string JoinCommand); + + public readonly record struct ReceiveInviteInfo( + EpicAccountId RecipientId, + EpicAccountId SenderId, + string JoinCommand); + + private static readonly NamedEvent dummyJoinGameEvent = + new NamedEvent(); + + private static readonly NamedEvent dummyAcceptInviteEvent = + new NamedEvent(); + + private static readonly NamedEvent dummyReceiveInviteEvent = + new NamedEvent(); + + public static NamedEvent OnJoinGame + => Core.LoadedImplementation.IsInitialized() + ? Core.LoadedImplementation.OnJoinGame + : dummyJoinGameEvent; + + public static NamedEvent OnInviteAccepted + => Core.LoadedImplementation.IsInitialized() + ? Core.LoadedImplementation.OnInviteAccepted + : dummyAcceptInviteEvent; + + public static NamedEvent OnInviteReceived + => Core.LoadedImplementation.IsInitialized() + ? Core.LoadedImplementation.OnInviteReceived + : dummyReceiveInviteEvent; + + public enum SetJoinCommandError + { + EosNotInitialized, + FailedToSetCustomInvite, + FailedToCreatePresenceModification, + JoinCommandTooLong, + ServerNameTooLong, + FailedToSetJoinInfo, + FailedToGetPuid, + DescTooLong, + FailedToSetRichText, + FailedToSetRecords, + SetPresenceTimedOut, + FailedToSetPresence + } + + public static async Task> SetJoinCommand( + EpicAccountId epicAccountId, string desc, string serverName, string joinCommand) + => Core.LoadedImplementation.IsInitialized() + ? await Core.LoadedImplementation.SetJoinCommand(epicAccountId, desc, serverName, joinCommand) + : Result.Failure(SetJoinCommandError.EosNotInitialized); + + public enum SendInviteError + { + EosNotInitialized, + FailedToGetSelfPuid, + FailedToGetRemotePuid, + TimedOut, + InternalError + } + + public static async Task> SendInvite( + EpicAccountId selfEpicAccountId, EpicAccountId remoteEpicAccountId) + => Core.LoadedImplementation.IsInitialized() + ? await Core.LoadedImplementation.SendInvite(selfEpicAccountId, remoteEpicAccountId) + : Result.Failure(SendInviteError.EosNotInitialized); + + public static void DeclineInvite(EpicAccountId selfEpicAccountId, EpicAccountId senderEpicAccountId) + { + if (Core.LoadedImplementation.IsInitialized()) + { + Core.LoadedImplementation.DeclineInvite(selfEpicAccountId, senderEpicAccountId); + } + } + } + + internal abstract partial class Implementation + { + public abstract NamedEvent OnJoinGame { get; } + public abstract NamedEvent OnInviteAccepted { get; } + public abstract NamedEvent OnInviteReceived { get; } + + public abstract Task> SetJoinCommand( + EpicAccountId epicAccountId, string desc, string joinCommand, string s); + public abstract Task> SendInvite( + EpicAccountId selfEpicAccountId, EpicAccountId remoteEpicAccountId); + public abstract void DeclineInvite(EpicAccountId selfEpicAccountId, EpicAccountId senderEpicAccountId); + } +} \ No newline at end of file diff --git a/Libraries/BarotraumaLibs/EosInterface/IdAndAuth/EgsAuthContinuanceToken.cs b/Libraries/BarotraumaLibs/EosInterface/IdAndAuth/EgsAuthContinuanceToken.cs new file mode 100644 index 000000000..bd8e6f8cc --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterface/IdAndAuth/EgsAuthContinuanceToken.cs @@ -0,0 +1,34 @@ +using System; + +namespace Barotrauma; + +public static partial class EosInterface +{ + public sealed class EgsAuthContinuanceToken + { + // Got this number by checking a decoded continuance token, may be subject to change + public static readonly TimeSpan Duration = TimeSpan.FromMinutes(30); + + public readonly DateTime ExpiryTime; + public bool IsValid => value != IntPtr.Zero && DateTime.Now < ExpiryTime; + + private IntPtr value; + + public EgsAuthContinuanceToken(IntPtr value, DateTime expiryTime) + { + this.value = value; + ExpiryTime = expiryTime; + } + + public IntPtr Spend() + { + var retVal = IsValid ? value : IntPtr.Zero; + value = IntPtr.Zero; + return retVal; + } + + public override string ToString() + => $"{(IsValid ? "Valid" : "Invalid")} EGS ContinuanceToken" + + (IsValid ? $" (expires on {ExpiryTime})" : ""); + } +} \ No newline at end of file diff --git a/Libraries/BarotraumaLibs/EosInterface/IdAndAuth/EgsIdToken.cs b/Libraries/BarotraumaLibs/EosInterface/IdAndAuth/EgsIdToken.cs new file mode 100644 index 000000000..e03b32fe1 --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterface/IdAndAuth/EgsIdToken.cs @@ -0,0 +1,52 @@ +using System.Threading.Tasks; +using Barotrauma.Networking; + +namespace Barotrauma; + +public static partial class EosInterface +{ + public enum GetEgsSelfIdTokenError + { + EosNotInitialized, + NotLoggedIn, + InvalidToken, + UnhandledErrorCondition + } + + public enum VerifyEgsIdTokenResult + { + Verified, + Failed + } + + /// + /// Represents an Epic Games ID Token, used to authenticate an Epic Account ID. + /// This is distinct from , which represents an EOS ID Token. + /// + public abstract class EgsIdToken + { + public abstract EpicAccountId AccountId { get; } + + public static Option Parse(string str) + => Core.LoadedImplementation.IsInitialized() + ? Core.LoadedImplementation.ParseEgsIdToken(str) + : Option.None; + + public static Result FromEpicAccountId(EpicAccountId accountId) + => Core.LoadedImplementation.IsInitialized() + ? Core.LoadedImplementation.GetEgsIdTokenForEpicAccountId(accountId) + : Result.Failure(GetEgsSelfIdTokenError.EosNotInitialized); + + public abstract override string ToString(); + + public abstract Task Verify(AccountId accountId); + } + + internal abstract partial class Implementation + { + public abstract Option ParseEgsIdToken(string str); + + public abstract Result GetEgsIdTokenForEpicAccountId( + EpicAccountId accountId); + } +} diff --git a/Libraries/BarotraumaLibs/EosInterface/IdAndAuth/EosConnectContinuanceToken.cs b/Libraries/BarotraumaLibs/EosInterface/IdAndAuth/EosConnectContinuanceToken.cs new file mode 100644 index 000000000..953986f29 --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterface/IdAndAuth/EosConnectContinuanceToken.cs @@ -0,0 +1,37 @@ +using System; +using Barotrauma.Networking; + +namespace Barotrauma; + +public static partial class EosInterface +{ + public sealed class EosConnectContinuanceToken + { + // Got this number by checking a decoded continuance token, may be subject to change + public static readonly TimeSpan Duration = TimeSpan.FromMinutes(30); + + public readonly AccountId ExternalAccountId; + public readonly DateTime ExpiryTime; + public bool IsValid => value != IntPtr.Zero && DateTime.Now < ExpiryTime; + + private IntPtr value; + + public EosConnectContinuanceToken(IntPtr value, AccountId externalAccountId, DateTime expiryTime) + { + this.value = value; + this.ExternalAccountId = externalAccountId; + ExpiryTime = expiryTime; + } + + public IntPtr Spend() + { + var retVal = IsValid ? value : IntPtr.Zero; + value = IntPtr.Zero; + return retVal; + } + + public override string ToString() + => $"{(IsValid ? "Valid" : "Invalid")} {ExternalAccountId} ContinuanceToken" + + (IsValid ? $" (expires on {ExpiryTime})" : ""); + } +} \ No newline at end of file diff --git a/Libraries/BarotraumaLibs/EosInterface/IdAndAuth/EosIdToken.cs b/Libraries/BarotraumaLibs/EosInterface/IdAndAuth/EosIdToken.cs new file mode 100644 index 000000000..9cb490123 --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterface/IdAndAuth/EosIdToken.cs @@ -0,0 +1,114 @@ +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Barotrauma.Networking; + +namespace Barotrauma; + +public static partial class EosInterface +{ + public enum GetEosSelfIdTokenError + { + EosNotInitialized, + NotLoggedIn, + InvalidToken, + CouldNotParseJwt, + UnhandledErrorCondition + } + + public enum VerifyEosIdTokenError + { + EosNotInitialized, + TimedOut, + ProductIdDidNotMatch, + CouldNotParseExternalAccountId, + UnhandledErrorCondition + } + + /// + /// Represents an EOS ID Token, used to authenticate a Product User ID. + /// This is distinct from , which represents an Epic Games ID Token. + /// + public readonly record struct EosIdToken( + ProductUserId ProductUserId, + JsonWebToken JsonWebToken) + { + public async Task> Verify() + => Core.LoadedImplementation is { } loadedImplementation + ? await loadedImplementation.VerifyEosIdToken(this) + : Result.Failure(VerifyEosIdTokenError.EosNotInitialized); + + public static Option Parse(string str) + { + var jsonReader = new Utf8JsonReader(Encoding.UTF8.GetBytes(str)); + JsonDocument? jsonDoc = null; + try + { + if (!JsonDocument.TryParseValue(ref jsonReader, out jsonDoc)) + { + return Option.None; + } + + if (!jsonDoc.RootElement.TryGetProperty(nameof(ProductUserId), out var puidElement)) + { + return Option.None; + } + + if (!jsonDoc.RootElement.TryGetProperty(nameof(JsonWebToken), out var jwtElement)) + { + return Option.None; + } + + var puidStr = puidElement.ToString(); + if (!puidStr.IsHexString()) + { + return Option.None; + } + + var puid = new ProductUserId(puidStr); + + var jwtStr = jwtElement.ToString(); + if (!JsonWebToken.Parse(jwtStr).TryUnwrap(out var jsonWebToken)) + { + return Option.None; + } + + var newToken = new EosIdToken(puid, jsonWebToken); + + return Option.Some(newToken); + } + catch + { + return Option.None; + } + finally + { + jsonDoc?.Dispose(); + } + } + + public static Result FromProductUserId(ProductUserId puid) + => Core.LoadedImplementation.IsInitialized() + ? Core.LoadedImplementation.GetEosIdTokenForProductUserId(puid) + : Result.Failure(GetEosSelfIdTokenError.EosNotInitialized); + + public override string ToString() + { + using var memoryStream = new System.IO.MemoryStream(); + using var jsonWriter = new Utf8JsonWriter(memoryStream); + jsonWriter.WriteStartObject(); + jsonWriter.WriteString(nameof(ProductUserId), ProductUserId.Value); + jsonWriter.WriteString(nameof(JsonWebToken), JsonWebToken.ToString()); + jsonWriter.WriteEndObject(); + jsonWriter.Flush(); + memoryStream.Flush(); + return Encoding.UTF8.GetString(memoryStream.ToArray()); + } + } + + internal abstract partial class Implementation + { + public abstract Task> VerifyEosIdToken(EosIdToken token); + public abstract Result GetEosIdTokenForProductUserId(ProductUserId puid); + } +} \ No newline at end of file diff --git a/Libraries/BarotraumaLibs/EosInterface/IdAndAuth/IdQueries.cs b/Libraries/BarotraumaLibs/EosInterface/IdAndAuth/IdQueries.cs new file mode 100644 index 000000000..fd0195a04 --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterface/IdAndAuth/IdQueries.cs @@ -0,0 +1,64 @@ +using System.Collections.Immutable; +using System.Threading.Tasks; +using Barotrauma.Networking; + +namespace Barotrauma; + +public static partial class EosInterface +{ + public static class IdQueries + { + private static Implementation? LoadedImplementation => Core.LoadedImplementation; + + public static bool IsLoggedIntoEosConnect + => GetLoggedInPuids() is { Length: > 0 }; + + /// + /// Gets all of the s the player has logged in with. + /// For most players, this is expected to return one ID. + /// It may return two IDs if a Steam user has chosen to link their account to an Epic Account. + /// + public static ImmutableArray GetLoggedInPuids() + => LoadedImplementation.IsInitialized() + ? LoadedImplementation.GetLoggedInPuids() + : ImmutableArray.Empty; + + /// + /// Gets all of the s the player has logged in with. + /// This is expected to return at most one ID. + ///

+ /// This should return exactly one ID for any Epic Games Store player. + ///
+ /// Steam players may choose to link their account to only one Epic Games account. + ///
+ public static ImmutableArray GetLoggedInEpicIds() + => LoadedImplementation.IsInitialized() + ? LoadedImplementation.GetLoggedInEpicIds() + : ImmutableArray.Empty; + + public enum GetSelfExternalIdError + { + EosNotInitialized, + Inaccessible, + Timeout, + InvalidUser, + ParseError, + UnhandledErrorCondition + } + + public static async Task, GetSelfExternalIdError>> GetSelfExternalAccountIds( + ProductUserId puid) + => LoadedImplementation.IsInitialized() + ? await LoadedImplementation.GetSelfExternalAccountIds(puid) + : Result.Failure(GetSelfExternalIdError.EosNotInitialized); + } + + internal abstract partial class Implementation + { + public abstract ImmutableArray GetLoggedInPuids(); + public abstract ImmutableArray GetLoggedInEpicIds(); + + public abstract Task, IdQueries.GetSelfExternalIdError>> + GetSelfExternalAccountIds(ProductUserId puid); + } +} \ No newline at end of file diff --git a/Libraries/BarotraumaLibs/EosInterface/IdAndAuth/Login.cs b/Libraries/BarotraumaLibs/EosInterface/IdAndAuth/Login.cs new file mode 100644 index 000000000..df10f35e6 --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterface/IdAndAuth/Login.cs @@ -0,0 +1,206 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Barotrauma.Extensions; +using Barotrauma.Networking; + +namespace Barotrauma; + +public static partial class EosInterface +{ + public static class Login + { + private static Implementation? LoadedImplementation => Core.LoadedImplementation; + + public enum CreateProductAccountError + { + EosNotInitialized, + InvalidContinuanceToken, + Timeout, + UnhandledErrorCondition + } + + public static async Task> CreateProductAccount( + EosConnectContinuanceToken eosContinuanceToken) + => LoadedImplementation.IsInitialized() + ? await LoadedImplementation.CreateProductAccount(eosContinuanceToken) + : Result.Failure(CreateProductAccountError.EosNotInitialized); + + public enum LinkExternalAccountError + { + EosNotInitialized, + InvalidContinuanceToken, + Timeout, + CannotLink, + UnhandledErrorCondition + } + + public static async Task> LinkExternalAccount(ProductUserId puid, + EosConnectContinuanceToken eosContinuanceToken) + => LoadedImplementation.IsInitialized() + ? await LoadedImplementation.LinkExternalAccount(puid, eosContinuanceToken) + : Result.Failure(LinkExternalAccountError.EosNotInitialized); + + public enum UnlinkExternalAccountError + { + EosNotInitialized, + FailedToGetExternalAccounts, + NotLoggedInToGivenAccount, + Timeout, + CannotLink, + InvalidUser, + UnhandledErrorCondition + } + + public static async Task> UnlinkExternalAccount(ProductUserId puid) + => LoadedImplementation.IsInitialized() + ? await LoadedImplementation.UnlinkExternalAccount(puid) + : Result.Failure(UnlinkExternalAccountError.EosNotInitialized); + + public enum LoginError + { + EosNotInitialized, + + SteamNotLoggedIn, + FailedToGetSteamSessionTicket, + + EgsLoginTimeout, + EgsAccountNotFound, + FailedToParseEgsId, + FailedToGetEgsIdToken, + AuthExchangeCodeNotFound, + AuthRequiresOpeningBrowser, + + Timeout, + InvalidUser, + EgsAccessDenied, + EosAccessDenied, + UnexpectedContinuanceToken, + + UnhandledFailureCondition + } + + [Flags] + public enum LoginEpicFlags + { + None = 0x0, + FailWithoutOpeningBrowser = 0x1 + } + + public static async + Task, LoginError>> + LoginEpicWithLinkedSteamAccount(LoginEpicFlags flags) + => LoadedImplementation.IsInitialized() + ? await LoadedImplementation.LoginEpicWithLinkedSteamAccount(flags) + : Result.Failure(LoginError.EosNotInitialized); + + public static async Task, LoginError>> + LoginEpicExchangeCode(string exchangeCode) + => LoadedImplementation.IsInitialized() + ? await LoadedImplementation.LoginEpicExchangeCode(exchangeCode) + : Result.Failure(LoginError.EosNotInitialized); + + public static async Task, LoginError>> + LoginEpicIdToken(EgsIdToken token) + => LoadedImplementation.IsInitialized() + ? await LoadedImplementation.LoginEpicIdToken(token) + : Result.Failure(LoginError.EosNotInitialized); + + public static async Task, LoginError>> LoginSteam() + => LoadedImplementation.IsInitialized() + ? await LoadedImplementation.LoginSteam() + : Result.Failure(LoginError.EosNotInitialized); + + public enum LinkExternalAccountToEpicAccountError + { + EosNotInitialized, + + TimedOut, + FailedToParseEgsAccountId, + + UnhandledErrorCondition + } + + public static async Task> + LinkExternalAccountToEpicAccount(EgsAuthContinuanceToken continuanceToken) + => LoadedImplementation.IsInitialized() + ? await LoadedImplementation.LinkExternalAccountToEpicAccount(continuanceToken) + : Result.Failure(LinkExternalAccountToEpicAccountError.EosNotInitialized); + + public enum LogoutEpicAccountError + { + EosNotInitialized, + TimedOut, + UnhandledErrorCondition + } + + public static async Task> LogoutEpicAccount(EpicAccountId egsId) + => LoadedImplementation.IsInitialized() + ? await LoadedImplementation.LogoutEpicAccount(egsId) + : Result.Failure(LogoutEpicAccountError.EosNotInitialized); + + /// + /// This is essentially a function for logging out, except EOS has no EOS_Connect_Logout function + /// so instead we have this to fake it. Once you use this, no methods should return this PUID + /// until you log into it again. + /// + public static void MarkAsInaccessible(ProductUserId puid) + { + if (LoadedImplementation.IsInitialized()) + { + LoadedImplementation.MarkAsInaccessible(puid); + } + } + + public static Option ParseEgsExchangeCode(IReadOnlyList args) + { + if (args.Contains("-AUTH_TYPE=exchangecode", StringComparer.OrdinalIgnoreCase)) + { + return args.FirstOrNone(arg => + arg.StartsWith("-AUTH_PASSWORD=", StringComparison.OrdinalIgnoreCase)) + .Select(arg => arg["-AUTH_PASSWORD=".Length..]); + } + + return Option.None; + } + + public static void TestEosSessionTimeoutRecovery(ProductUserId puid) + { + if (LoadedImplementation.IsInitialized()) + { + LoadedImplementation.TestEosSessionTimeoutRecovery(puid); + } + } + } + + internal abstract partial class Implementation + { + public abstract Task> CreateProductAccount( + EosConnectContinuanceToken eosContinuanceToken); + + public abstract Task> LinkExternalAccount(ProductUserId puid, + EosConnectContinuanceToken eosContinuanceToken); + + public abstract Task> UnlinkExternalAccount(ProductUserId puid); + + public abstract Task, Login.LoginError>> + LoginEpicExchangeCode(string exchangeCode); + + public abstract + Task, Login.LoginError>> + LoginEpicWithLinkedSteamAccount(Login.LoginEpicFlags flags); + + public abstract Task, Login.LoginError>> + LoginEpicIdToken(EgsIdToken token); + + public abstract Task, Login.LoginError>> LoginSteam(); + + public abstract Task> + LinkExternalAccountToEpicAccount(EgsAuthContinuanceToken continuanceToken); + + public abstract Task> LogoutEpicAccount(EpicAccountId egsId); + public abstract void MarkAsInaccessible(ProductUserId puid); + public abstract void TestEosSessionTimeoutRecovery(ProductUserId puid); + } +} \ No newline at end of file diff --git a/Libraries/BarotraumaLibs/EosInterface/IdAndAuth/Ownership.cs b/Libraries/BarotraumaLibs/EosInterface/IdAndAuth/Ownership.cs new file mode 100644 index 000000000..905198d53 --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterface/IdAndAuth/Ownership.cs @@ -0,0 +1,32 @@ +using System.Threading.Tasks; +using Barotrauma.Networking; + +namespace Barotrauma; + +public static partial class EosInterface +{ + public static class Ownership + { + private static Implementation? LoadedImplementation => Core.LoadedImplementation; + + public static async Task> GetGameOwnershipToken(EpicAccountId selfEpicAccountId) + => LoadedImplementation.IsInitialized() + ? await LoadedImplementation.GetGameOwnershipToken(selfEpicAccountId) + : Option.None; + + public readonly record struct Token(JsonWebToken Jwt) + { + public async Task> Verify() + => LoadedImplementation.IsInitialized() + ? await LoadedImplementation.VerifyGameOwnershipToken(this) + : Option.None; + } + } + + internal abstract partial class Implementation + { + public abstract Task> GetGameOwnershipToken(EpicAccountId selfEpicAccountId); + + public abstract Task> VerifyGameOwnershipToken(Ownership.Token token); + } +} \ No newline at end of file diff --git a/Libraries/BarotraumaLibs/EosInterface/IdAndAuth/ProductUserId.cs b/Libraries/BarotraumaLibs/EosInterface/IdAndAuth/ProductUserId.cs new file mode 100644 index 000000000..49bacb800 --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterface/IdAndAuth/ProductUserId.cs @@ -0,0 +1,13 @@ +namespace Barotrauma; + +public static partial class EosInterface +{ + /// + /// A Product User ID is an EOS-specific ID that's linked to the SteamID or the Epic Account ID of a player. + /// It is used to identify players in many of EOS' interfaces, most notably the P2P networking interface. + ///

+ /// A Product User ID used by Barotrauma is only valid for Barotrauma; other games that use EOS get their + /// own separate set of Product User IDs. + ///
+ public readonly record struct ProductUserId(string Value); +} \ No newline at end of file diff --git a/Libraries/BarotraumaLibs/EosInterface/P2P/P2PSocket.cs b/Libraries/BarotraumaLibs/EosInterface/P2P/P2PSocket.cs new file mode 100644 index 000000000..823689f73 --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterface/P2P/P2PSocket.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using Barotrauma.Networking; + +namespace Barotrauma; + +public static partial class EosInterface +{ + public abstract class P2PSocket : IDisposable + { + public enum CreationError + { + EosNotInitialized, + UserNotLoggedIn, + RequestBindFailed, + CloseBindFailed + } + + public readonly record struct IncomingConnectionRequest( + P2PSocket Socket, + ProductUserId RemoteUserId) + { + public void Accept() + => Socket.AcceptConnectionRequest(this); + } + + public readonly record struct RemoteConnectionClosed( + ProductUserId RemoteUserId, + RemoteConnectionClosed.ConnectionClosedReason Reason) + { + public enum ConnectionClosedReason + { + Unknown, + ClosedByLocalUser, + ClosedByPeer, + TimedOut, + TooManyConnections, + InvalidMessage, + InvalidData, + ConnectionFailed, + ConnectionClosed, + NegotiationFailed, + UnexpectedError, + Unhandled + } + } + + public readonly NamedEvent HandleIncomingConnection + = new NamedEvent(); + + public readonly NamedEvent HandleClosedConnection + = new NamedEvent(); + + public static Result Create(ProductUserId puid, SocketId socketId) + => Core.LoadedImplementation.IsInitialized() + ? Core.LoadedImplementation.CreateP2PSocket(puid, socketId) + : Result.Failure(CreationError.EosNotInitialized); + + public abstract void AcceptConnectionRequest(IncomingConnectionRequest request); + + public abstract void CloseConnection(ProductUserId remoteUserId); + + public readonly record struct IncomingMessage( + byte[] Buffer, + int ByteLength, + ProductUserId Sender); + + public abstract IEnumerable GetMessageBatch(); + + public readonly record struct OutgoingMessage( + byte[] Buffer, + int ByteLength, + ProductUserId Destination, + DeliveryMethod DeliveryMethod); + + public enum SendError + { + EosNotInitialized, + InvalidParameters, + LimitExceeded, + NoConnection, + UnhandledErrorCondition + } + + public abstract Result SendMessage(OutgoingMessage msg); + + public abstract void Dispose(); + } + + internal abstract partial class Implementation + { + public abstract Result CreateP2PSocket(ProductUserId puid, + SocketId socketId); + } +} \ No newline at end of file diff --git a/Libraries/BarotraumaLibs/EosInterface/P2P/SocketId.cs b/Libraries/BarotraumaLibs/EosInterface/P2P/SocketId.cs new file mode 100644 index 000000000..ec11c490b --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterface/P2P/SocketId.cs @@ -0,0 +1,6 @@ +namespace Barotrauma; + +public static partial class EosInterface +{ + public readonly record struct SocketId(string SocketName); +} \ No newline at end of file diff --git a/Libraries/BarotraumaLibs/EosInterface/Sessions/Sessions.cs b/Libraries/BarotraumaLibs/EosInterface/Sessions/Sessions.cs new file mode 100644 index 000000000..3b01d1aae --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterface/Sessions/Sessions.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading.Tasks; + +namespace Barotrauma; + +public static partial class EosInterface +{ + public static class Sessions + { + public const string DefaultBucketName = "BBucket"; + public const int MinBucketIndex = 0; + public const int MaxBucketIndex = 9; + + public sealed record OwnedSession( + string BucketId, + Identifier InternalId, + Identifier GlobalId, + Dictionary Attributes) : IDisposable + { + public Option HostAddress = Option.None; + + public ImmutableDictionary SyncedAttributes = + ImmutableDictionary.Empty; + + public async Task> UpdateAttributes() + => Core.LoadedImplementation is { } implementation + ? await implementation.UpdateOwnedSessionAttributes(this) + : Result.Failure(AttributeUpdateError.EosNotInitialized); + + public async Task> Close() + => Core.LoadedImplementation is { } implementation + ? await implementation.CloseOwnedSession(this) + : Result.Failure(CloseError.EosNotInitialized); + + public void Dispose() + { + if (!Core.IsInitialized) + { + return; + } + + var _ = Close(); + } + } + + public readonly record struct RemoteSession( + string SessionId, + string HostAddress, + int CurrentPlayers, + int MaxPlayers, + ImmutableDictionary Attributes, + string BucketId) + { + public readonly record struct Query( + int BucketIndex, + ProductUserId LocalUserId, + uint MaxResults, + ImmutableDictionary Attributes) + { + public enum Error + { + EosNotInitialized, + + ExceededMaxAllowedResults, + + InvalidParameters, + TimedOut, + NotFound, + + UnhandledErrorCondition + } + + public async Task, Error>> Run() + => Core.LoadedImplementation is { } loadedImplementation + ? await loadedImplementation.RunRemoteSessionQuery(this) + : Result.Failure(Error.EosNotInitialized); + } + } + + public enum CreateError + { + EosNotInitialized, + TimedOut, + + SessionAlreadyExists, + + InvalidParametersForAddAttribute, + IncompatibleVersionForAddAttribute, + UnhandledErrorConditionForAddAttribute, + + InvalidUser, + + UnhandledErrorCondition + } + + public enum AttributeUpdateError + { + EosNotInitialized, + TimedOut, + + FailedToCreateSessionModificationHandle, + + InvalidParametersForRemoveAttribute, + IncompatibleVersionForRemoveAttribute, + UnhandledErrorConditionForRemoveAttribute, + + InvalidParametersForAddAttribute, + IncompatibleVersionForAddAttribute, + UnhandledErrorConditionForAddAttribute, + + InvalidParametersForSessionUpdate, + SessionsOutOfSync, + SessionNotFound, + NoConnection, + + UnhandledErrorCondition + } + + public enum CloseError + { + EosNotInitialized, + TimedOut, + + InvalidParameters, + AlreadyPending, + NotFound, + UnhandledErrorCondition + } + + public enum RegisterError + { + EosNotInitialized, + TimedOut, + UnhandledErrorCondition + } + + public enum UnregisterError + { + EosNotInitialized, + TimedOut, + UnhandledErrorCondition + } + + public static async Task> CreateSession(Option puidOption, + Identifier internalId, int maxPlayers) + => Core.LoadedImplementation.IsInitialized() + ? await Core.LoadedImplementation.CreateSession(puidOption, internalId, maxPlayers) + : Result.Failure(CreateError.EosNotInitialized); + } + + internal abstract partial class Implementation + { + public abstract Task> CreateSession( + Option selfUserIdOption, Identifier internalId, int maxPlayers); + + public abstract Task> UpdateOwnedSessionAttributes( + Sessions.OwnedSession session); + + public abstract Task> CloseOwnedSession(Sessions.OwnedSession session); + public abstract Task CloseAllOwnedSessions(); + + public abstract Task, Sessions.RemoteSession.Query.Error>> + RunRemoteSessionQuery(Sessions.RemoteSession.Query query); + } +} \ No newline at end of file diff --git a/Libraries/BarotraumaLibs/EosInterfacePrivate/.gitignore b/Libraries/BarotraumaLibs/EosInterfacePrivate/.gitignore new file mode 100644 index 000000000..c8cdfedfb --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterfacePrivate/.gitignore @@ -0,0 +1,2 @@ +EOS-SDK/* +**/ExcludeFromPublicRepo/* diff --git a/Libraries/BarotraumaLibs/EosInterfacePrivate/EosInterface.Implementation.Linux.csproj b/Libraries/BarotraumaLibs/EosInterfacePrivate/EosInterface.Implementation.Linux.csproj new file mode 100644 index 000000000..57341009e --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterfacePrivate/EosInterface.Implementation.Linux.csproj @@ -0,0 +1,40 @@ + + + + EosInterface.Implementation.Linux + net6.0 + EosInterfacePrivate + false + disable + EOS_PLATFORM_LINUX + + + + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + x64 + + + + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + x64 + + + + + + + + + PreserveNewest + libEOSSDK-Linux-Shipping.so + + + + + + + + + + + diff --git a/Libraries/BarotraumaLibs/EosInterfacePrivate/EosInterface.Implementation.MacOS.csproj b/Libraries/BarotraumaLibs/EosInterfacePrivate/EosInterface.Implementation.MacOS.csproj new file mode 100644 index 000000000..df8cbade0 --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterfacePrivate/EosInterface.Implementation.MacOS.csproj @@ -0,0 +1,40 @@ + + + + EosInterface.Implementation.MacOS + net6.0 + EosInterfacePrivate + false + disable + EOS_PLATFORM_OSX + + + + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + x64 + + + + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + x64 + + + + + + + + + PreserveNewest + libEOSSDK-Mac-Shipping.dylib + + + + + + + + + + + diff --git a/Libraries/BarotraumaLibs/EosInterfacePrivate/EosInterface.Implementation.Win64.csproj b/Libraries/BarotraumaLibs/EosInterfacePrivate/EosInterface.Implementation.Win64.csproj new file mode 100644 index 000000000..7af67291b --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterfacePrivate/EosInterface.Implementation.Win64.csproj @@ -0,0 +1,39 @@ + + + + EosInterface.Implementation.Win64 + net6.0 + EosInterfacePrivate + false + disable + EOS_PLATFORM_WINDOWS_64 + + + + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + x64 + + + + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + x64 + + + + + + + + PreserveNewest + EOSSDK-Win64-Shipping.dll + + + + + + + + + + + diff --git a/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/Achievements/AchievementsPrivate.cs b/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/Achievements/AchievementsPrivate.cs new file mode 100644 index 000000000..2d8149461 --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/Achievements/AchievementsPrivate.cs @@ -0,0 +1,266 @@ +#nullable enable + +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Barotrauma; + +namespace EosInterfacePrivate; + +public static class AchievementsPrivate +{ + public static async Task> UnlockAchievements(params Identifier[] achievements) + { + if (CorePrivate.AchievementsInterface is not { } achievementsInterface) { return Result.Failure(EosInterface.AchievementUnlockError.EosNotInitialized); } + + var loggedInUsers = IdQueriesPrivate.GetLoggedInPuids(); + + if (loggedInUsers is not { Length: > 0 }) + { + return Result.Failure(EosInterface.AchievementUnlockError.InvalidUser); + } + var loggedInUser = loggedInUsers[0]; + + var achievementUnlockWaiter = new CallbackWaiter(); + var options = new Epic.OnlineServices.Achievements.UnlockAchievementsOptions + { + AchievementIds = achievements.Select(static i => new Epic.OnlineServices.Utf8String(i.Value.ToLowerInvariant())).ToArray(), + UserId = Epic.OnlineServices.ProductUserId.FromString(loggedInUser.Value) + }; + + achievementsInterface.UnlockAchievements(options: ref options, clientData: null, completionDelegate: achievementUnlockWaiter.OnCompletion); + var resultOption = await achievementUnlockWaiter.Task; + + if (!resultOption.TryUnwrap(out var callbackResult)) + { + return Result.Failure(EosInterface.AchievementUnlockError.TimedOut); + } + + return callbackResult.ResultCode switch + { + Epic.OnlineServices.Result.Success => Result.Success(callbackResult.AchievementsCount), + Epic.OnlineServices.Result.InvalidParameters => Result.Failure(EosInterface.AchievementUnlockError.InvalidParameters), + Epic.OnlineServices.Result.InvalidUser => Result.Failure(EosInterface.AchievementUnlockError.InvalidUser), + Epic.OnlineServices.Result.NotFound => Result.Failure(EosInterface.AchievementUnlockError.NotFound), + var unhandled => Result.Failure(unhandled.FailAndLogUnhandledError(EosInterface.AchievementUnlockError.Unknown)) + }; + } + + public static async Task, EosInterface.QueryStatsError>> QueryStats(ImmutableArray stats) + { + if (CorePrivate.StatsInterface is not { } statsInterface) { return Result.Failure(EosInterface.QueryStatsError.EosNotInitialized); } + + var loggedInUsers = IdQueriesPrivate.GetLoggedInPuids(); + + if (loggedInUsers is not { Length: > 0 }) + { + return Result.Failure(EosInterface.QueryStatsError.InvalidUser); + } + var loggedInUser = loggedInUsers[0]; + + var convertedUserId = Epic.OnlineServices.ProductUserId.FromString(loggedInUser.Value); + + var options = new Epic.OnlineServices.Stats.QueryStatsOptions + { + LocalUserId = convertedUserId, + TargetUserId = convertedUserId, + StatNames = stats.Any() + ? stats.Select(static s => new Epic.OnlineServices.Utf8String(s.ToIdentifier().Value.ToLowerInvariant())).ToArray() + : default + }; + + var queryWaiter = new CallbackWaiter(); + statsInterface.QueryStats(options: ref options, clientData: null, completionDelegate: queryWaiter.OnCompletion); + + var resultOption = await queryWaiter.Task; + + if (!resultOption.TryUnwrap(out var callbackResult)) + { + return Result.Failure(EosInterface.QueryStatsError.TimedOut); + } + + if (callbackResult.ResultCode != Epic.OnlineServices.Result.Success) + { + return callbackResult.ResultCode switch + { + Epic.OnlineServices.Result.InvalidParameters => Result.Failure(EosInterface.QueryStatsError.InvalidParameters), + Epic.OnlineServices.Result.InvalidUser => Result.Failure(EosInterface.QueryStatsError.InvalidUser), + Epic.OnlineServices.Result.NotFound => Result.Failure(EosInterface.QueryStatsError.NotFound), + var unhandled => Result.Failure(unhandled.FailAndLogUnhandledError(EosInterface.QueryStatsError.Unknown)) + }; + } + + var builder = ImmutableDictionary.CreateBuilder(); + + if (stats.Length is 0) + { + var countOptions = new Epic.OnlineServices.Stats.GetStatCountOptions + { + TargetUserId = convertedUserId + }; + uint count = statsInterface.GetStatsCount(ref countOptions); + + for (uint i = 0; i < count; i++) + { + var copyIndexOptions = new Epic.OnlineServices.Stats.CopyStatByIndexOptions + { + TargetUserId = convertedUserId, + StatIndex = i + }; + var copyResult = statsInterface.CopyStatByIndex(ref copyIndexOptions, out var statOut); + + if (copyResult is Epic.OnlineServices.Result.Success && statOut is { Name: var name, Value: var value }) + { + builder.Add(AchievementStatExtension.FromIdentifier(new Identifier(name)), value); + } + } + } + else + { + foreach (AchievementStat stat in stats) + { + var copyOptions = new Epic.OnlineServices.Stats.CopyStatByNameOptions + { + TargetUserId = convertedUserId, + Name = new Epic.OnlineServices.Utf8String(stat.ToString().ToLowerInvariant()) + }; + var copyResult = statsInterface.CopyStatByName(ref copyOptions, out var statOut); + + if (copyResult is Epic.OnlineServices.Result.Success && statOut is { Name: var name, Value: var value }) + { + builder.Add(AchievementStatExtension.FromIdentifier(new Identifier(name)), value); + } + } + } + + return Result.Success(builder.ToImmutable()); + } + + public static async Task, EosInterface.QueryAchievementsError>> QueryPlayerAchievements() + { + if (CorePrivate.AchievementsInterface is not { } achievementsInterface) { return Result.Failure(EosInterface.QueryAchievementsError.EosNotInitialized); } + + var loggedInUsers = IdQueriesPrivate.GetLoggedInPuids(); + + if (loggedInUsers is not { Length: > 0 }) + { + return Result.Failure(EosInterface.QueryAchievementsError.InvalidUser); + } + var loggedInUser = loggedInUsers[0]; + + var convertedUserId = Epic.OnlineServices.ProductUserId.FromString(loggedInUser.Value); + + var options = new Epic.OnlineServices.Achievements.QueryPlayerAchievementsOptions + { + LocalUserId = convertedUserId, + TargetUserId = convertedUserId + }; + + var queryWaiter = new CallbackWaiter(); + achievementsInterface.QueryPlayerAchievements(options: ref options, clientData: null, completionDelegate: queryWaiter.OnCompletion); + + var resultOption = await queryWaiter.Task; + + if (!resultOption.TryUnwrap(out var callbackResult)) + { + return Result.Failure(EosInterface.QueryAchievementsError.TimedOut); + } + + if (callbackResult.ResultCode != Epic.OnlineServices.Result.Success) + { + return callbackResult.ResultCode switch + { + Epic.OnlineServices.Result.InvalidParameters => Result.Failure(EosInterface.QueryAchievementsError.InvalidParameters), + Epic.OnlineServices.Result.InvalidUser => Result.Failure(EosInterface.QueryAchievementsError.InvalidUser), + Epic.OnlineServices.Result.InvalidProductUserID => Result.Failure(EosInterface.QueryAchievementsError.InvalidProductUserID), + Epic.OnlineServices.Result.NotFound => Result.Failure(EosInterface.QueryAchievementsError.NotFound), + var unhandled => Result.Failure(unhandled.FailAndLogUnhandledError(EosInterface.QueryAchievementsError.Unknown)) + }; + } + + var countOptions = new Epic.OnlineServices.Achievements.GetPlayerAchievementCountOptions + { + UserId = convertedUserId + }; + uint count = achievementsInterface.GetPlayerAchievementCount(ref countOptions); + + var builder = ImmutableDictionary.CreateBuilder(); + for (uint i = 0; i < count; i++) + { + var copyIndexOptions = new Epic.OnlineServices.Achievements.CopyPlayerAchievementByIndexOptions + { + TargetUserId = convertedUserId, + LocalUserId = convertedUserId, + AchievementIndex = i + }; + var copyResult = achievementsInterface.CopyPlayerAchievementByIndex(ref copyIndexOptions, out var achievementOut); + + if (copyResult is Epic.OnlineServices.Result.Success && achievementOut is { AchievementId: var name, Progress: var value }) + { + builder.Add(new Identifier(name), value); + } + } + + return Result.Success(builder.ToImmutable()); + } + + public static async Task> IngestStats(params (AchievementStat Stat, int IngestAmount)[] stats) + { + if (CorePrivate.StatsInterface is not { } statsInterface) { return Result.Failure(EosInterface.IngestStatError.EosNotInitialized); } + + var loggedInUsers = IdQueriesPrivate.GetLoggedInPuids(); + + if (loggedInUsers is not { Length: > 0 }) + { + return Result.Failure(EosInterface.IngestStatError.InvalidUser); + } + var loggedInUser = loggedInUsers[0]; + + var convertedUserId = Epic.OnlineServices.ProductUserId.FromString(loggedInUser.Value); + + var options = new Epic.OnlineServices.Stats.IngestStatOptions + { + LocalUserId = convertedUserId, + TargetUserId = convertedUserId, + Stats = stats.Select(static s => new Epic.OnlineServices.Stats.IngestData + { + StatName = s.Stat.ToString().ToLowerInvariant(), + IngestAmount = s.IngestAmount + }).ToArray() + }; + + var ingestStatWaiter = new CallbackWaiter(); + statsInterface.IngestStat(options: ref options, clientData: null, completionDelegate: ingestStatWaiter.OnCompletion); + + var resultOption = await ingestStatWaiter.Task; + + if (!resultOption.TryUnwrap(out var callbackResult)) + { + return Result.Failure(EosInterface.IngestStatError.TimedOut); + } + + return callbackResult.ResultCode switch + { + Epic.OnlineServices.Result.Success => Result.Success(Unit.Value), + Epic.OnlineServices.Result.InvalidParameters => Result.Failure(EosInterface.IngestStatError.InvalidParameters), + Epic.OnlineServices.Result.InvalidUser => Result.Failure(EosInterface.IngestStatError.InvalidUser), + Epic.OnlineServices.Result.NotFound => Result.Failure(EosInterface.IngestStatError.NotFound), + var unhandled => Result.Failure(unhandled.FailAndLogUnhandledError(EosInterface.IngestStatError.Unknown)) + }; + } +} + +internal sealed partial class ImplementationPrivate : EosInterface.Implementation +{ + public override Task> UnlockAchievements(params Identifier[] achievementIds) + => TaskScheduler.Schedule(() => AchievementsPrivate.UnlockAchievements(achievementIds)); + + public override Task> IngestStats(params (AchievementStat Stat, int IngestAmount)[] stats) + => TaskScheduler.Schedule(() => AchievementsPrivate.IngestStats(stats)); + + public override Task, EosInterface.QueryStatsError>> QueryStats(ImmutableArray stats) + => TaskScheduler.Schedule(() => AchievementsPrivate.QueryStats(stats)); + + public override Task, EosInterface.QueryAchievementsError>> QueryPlayerAchievements() + => TaskScheduler.Schedule(AchievementsPrivate.QueryPlayerAchievements); +} diff --git a/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/Core/CorePrivate.cs b/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/Core/CorePrivate.cs new file mode 100644 index 000000000..02f3c854a --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/Core/CorePrivate.cs @@ -0,0 +1,176 @@ +#nullable enable +using System; +using Barotrauma.Debugging; +using Microsoft.Xna.Framework; +using Barotrauma; + +namespace EosInterfacePrivate; + +static class CorePrivate +{ + public static EosInterface.Core.Status CurrentStatus + => platformInterface is null + ? EosInterface.Core.Status.NotInitialized + : platformInterface.GetNetworkStatus() == Epic.OnlineServices.Platform.NetworkStatus.Online + ? EosInterface.Core.Status.Online + : EosInterface.Core.Status.InitializedButOffline; + + private static Epic.OnlineServices.Platform.Options platformInterfaceOptions; + public static Epic.OnlineServices.Platform.Options PlatformInterfaceOptions => platformInterfaceOptions; + + private static Epic.OnlineServices.Platform.PlatformInterface? platformInterface; + public static Epic.OnlineServices.Platform.PlatformInterface? PlatformInterface => platformInterface; + + public static Epic.OnlineServices.Connect.ConnectInterface? ConnectInterface + => PlatformInterface?.GetConnectInterface(); + + public static Epic.OnlineServices.Auth.AuthInterface? EgsAuthInterface + => PlatformInterface?.GetAuthInterface(); + + public static Epic.OnlineServices.Friends.FriendsInterface? EgsFriendsInterface + => PlatformInterface?.GetFriendsInterface(); + + public static Epic.OnlineServices.UserInfo.UserInfoInterface? EgsUserInfoInterface + => PlatformInterface?.GetUserInfoInterface(); + + public static Epic.OnlineServices.Presence.PresenceInterface? EgsPresenceInterface + => PlatformInterface?.GetPresenceInterface(); + + public static Epic.OnlineServices.CustomInvites.CustomInvitesInterface? EgsCustomInvitesInterface + => PlatformInterface?.GetCustomInvitesInterface(); + + public static Epic.OnlineServices.UI.UIInterface? EgsUiInterface + => PlatformInterface?.GetUIInterface(); + + public static Epic.OnlineServices.Sessions.SessionsInterface? SessionsInterface + => PlatformInterface?.GetSessionsInterface(); + + public static Epic.OnlineServices.P2P.P2PInterface? P2PInterface + => PlatformInterface?.GetP2PInterface(); + + public static Epic.OnlineServices.Achievements.AchievementsInterface? AchievementsInterface + => PlatformInterface?.GetAchievementsInterface(); + + public static Epic.OnlineServices.Stats.StatsInterface? StatsInterface + => PlatformInterface?.GetStatsInterface(); + + public static Epic.OnlineServices.Ecom.EcomInterface? EcomInterface + => PlatformInterface?.GetEcomInterface(); + + public static Result Init(ImplementationPrivate implementation, EosInterface.ApplicationCredentials applicationCredentials, bool enableOverlay) + { + var initializeOptions = new Epic.OnlineServices.Platform.InitializeOptions + { + ProductName = "Barotrauma", + ProductVersion = GameVersion.CurrentVersion.ToString(), + + SystemInitializeOptions = IntPtr.Zero, + OverrideThreadAffinity = null, + + AllocateMemoryFunction = IntPtr.Zero, + ReallocateMemoryFunction = IntPtr.Zero, + ReleaseMemoryFunction = IntPtr.Zero + }; + + var result = Epic.OnlineServices.Platform.PlatformInterface.Initialize(ref initializeOptions); + Console.WriteLine( + $"{nameof(Epic.OnlineServices.Platform.PlatformInterface)}.{nameof(Epic.OnlineServices.Platform.PlatformInterface.Initialize)} result: {result}"); + + platformInterfaceOptions = PlatformInterfaceOptionsPrivate.PlatformOptions[applicationCredentials]; + if (enableOverlay) + { + // Some caveats: + // - Currently the overlay is not implemented on non-Windows platforms + // - If you try to initialize EOS after the window has already been created, + // enabling the overlay will result in a crash + // - The overlay doesn't do anything if you do not log into an Epic account + platformInterfaceOptions.Flags = Epic.OnlineServices.Platform.PlatformFlags.None; + } + + platformInterface = Epic.OnlineServices.Platform.PlatformInterface.Create(ref platformInterfaceOptions); + + if (ConnectInterface != null) + { + LoginPrivate.Init(); + } + + if (platformInterface is null) { return Result.Failure(EosInterface.Core.InitError.PlatformInterfaceNotCreated); } + + PresencePrivate.Init(implementation); + + var setLogCallbackResult = Epic.OnlineServices.Logging.LoggingInterface.SetCallback(LogCallback); + if (setLogCallbackResult == Epic.OnlineServices.Result.Success) + { + Epic.OnlineServices.Logging.LoggingInterface.SetLogLevel( + Epic.OnlineServices.Logging.LogCategory.AllCategories, + Epic.OnlineServices.Logging.LogLevel.VeryVerbose); + } + + return Result.Success(default(Unit)); + } + + private static void LogCallback(ref Epic.OnlineServices.Logging.LogMessage msg) + { + DebugConsoleCore.Log($"[EOS {msg.Category} {msg.Level}] {msg.Message}"); + } + + public static Result CheckForLauncherAndRestart() + { + if (platformInterface is null) { return Result.Failure(EosInterface.Core.CheckForLauncherAndRestartError.EosNotInitialized); } + var result = platformInterface.CheckForLauncherAndRestart(); + if (result == Epic.OnlineServices.Result.Success) { return Result.Success(EosInterface.Core.WillRestartThroughLauncher.Yes); } + if (result == Epic.OnlineServices.Result.NoChange) { return Result.Success(EosInterface.Core.WillRestartThroughLauncher.No); } + return Result.Failure(result switch + { + Epic.OnlineServices.Result.UnexpectedError + => EosInterface.Core.CheckForLauncherAndRestartError.UnexpectedError, + _ + => result.FailAndLogUnhandledError(EosInterface.Core.CheckForLauncherAndRestartError.UnhandledErrorCondition) + }); + } + + private static EosInterface.Core.Status prevTickStatus = EosInterface.Core.Status.NotInitialized; + public static void Update() + { + platformInterface?.Tick(); + var currentStatus = CurrentStatus; + if (currentStatus == EosInterface.Core.Status.Online && prevTickStatus != currentStatus) + { + // We were offline, but now we are back online so let's update all sessions + OwnedSessionsPrivate.ForceUpdateAllOwnedSessions(); + } + prevTickStatus = currentStatus; + } + + public static void Quit() + { + PresencePrivate.Quit(); + + platformInterface?.Release(); + platformInterface = null; + Epic.OnlineServices.Platform.PlatformInterface.Shutdown(); + } +} + +internal sealed partial class ImplementationPrivate : EosInterface.Implementation +{ + + public override EosInterface.Core.Status CurrentStatus => CorePrivate.CurrentStatus; + + public override string NativeLibraryName => Epic.OnlineServices.Config.LibraryName; + + public override Result Init(EosInterface.ApplicationCredentials applicationCredentials, bool enableOverlay) + => CorePrivate.Init(this, applicationCredentials, enableOverlay); + + public override Result CheckForLauncherAndRestart() + => CorePrivate.CheckForLauncherAndRestart(); + + public override void Quit() + => CorePrivate.Quit(); + + public override void Update() + { + CorePrivate.Update(); + TaskScheduler.RunOnCurrentThread(); + } +} diff --git a/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/Core/CustomTaskScheduler.cs b/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/Core/CustomTaskScheduler.cs new file mode 100644 index 000000000..18ae08313 --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/Core/CustomTaskScheduler.cs @@ -0,0 +1,67 @@ +#nullable enable +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace EosInterfacePrivate; + +internal sealed partial class ImplementationPrivate : Barotrauma.EosInterface.Implementation +{ + /// + /// Custom TaskScheduler to force every EOS-related task to run on the main thread, because + /// the docs say the SDK is not thread-safe even though it's worked fine without this :/ + /// + /// See https://dev.epicgames.com/docs/epic-online-services/eos-get-started/eossdkc-sharp-getting-started#threading + /// + internal sealed class CustomTaskScheduler : TaskScheduler + { + private readonly ConcurrentQueue taskQueue = new ConcurrentQueue(); + + internal Task Schedule(Func> action) + { + return + Task.Factory.StartNew( + function: action, + cancellationToken: CancellationToken.None, + creationOptions: TaskCreationOptions.None, + scheduler: this).Unwrap(); + } + + internal Task Schedule(Func action) + { + return + Task.Factory.StartNew( + function: action, + cancellationToken: CancellationToken.None, + creationOptions: TaskCreationOptions.None, + scheduler: this).Unwrap(); + } + + internal void RunOnCurrentThread() + { + while (taskQueue.TryDequeue(out var task)) + { + TryExecuteTask(task); + } + } + + protected override IEnumerable GetScheduledTasks() + => Enumerable.Empty(); + + protected override void QueueTask(Task task) + { + taskQueue.Enqueue(task); + } + + protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) + { + // Never allow executing inline because that means it's not the main thread + return false; + } + } + + internal readonly CustomTaskScheduler TaskScheduler = new CustomTaskScheduler(); +} diff --git a/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/Friends/FriendsPrivate.cs b/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/Friends/FriendsPrivate.cs new file mode 100644 index 000000000..82040e4a1 --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/Friends/FriendsPrivate.cs @@ -0,0 +1,249 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading.Tasks; +using Barotrauma.Networking; +using Barotrauma; +using Barotrauma.Extensions; + +namespace EosInterfacePrivate; + +static class FriendsPrivate +{ + internal static async Task> GetUserInfoData( + EpicAccountId selfEaid, EpicAccountId friendEaid) + { + if (CorePrivate.EgsUserInfoInterface is not { } egsUserInfoInterface) { return Result.Failure(EosInterface.Friends.GetFriendsError.EosNotInitialized); } + + var selfEaidInternal = Epic.OnlineServices.EpicAccountId.FromString(selfEaid.EosStringRepresentation); + var friendEaidInternal = Epic.OnlineServices.EpicAccountId.FromString(friendEaid.EosStringRepresentation); + + var queryUserInfoOptions = new Epic.OnlineServices.UserInfo.QueryUserInfoOptions + { + LocalUserId = selfEaidInternal, + TargetUserId = friendEaidInternal + }; + var queryUserInfoWaiter = new CallbackWaiter(); + egsUserInfoInterface.QueryUserInfo(options: ref queryUserInfoOptions, clientData: null, completionDelegate: queryUserInfoWaiter.OnCompletion); + var queryUserInfoResult = await queryUserInfoWaiter.Task; + if (!queryUserInfoResult.TryUnwrap(out var queryUserInfo)) + { + return Result.Failure(EosInterface.Friends.GetFriendsError.UserInfoQueryTimedOut); + } + if (queryUserInfo.ResultCode != Epic.OnlineServices.Result.Success) + { + return Result.Failure(EosInterface.Friends.GetFriendsError.UserInfoQueryFailed); + } + + var copyUserInfoOptions = new Epic.OnlineServices.UserInfo.CopyUserInfoOptions + { + LocalUserId = selfEaidInternal, + TargetUserId = friendEaidInternal + }; + var copyUserInfoResult = egsUserInfoInterface.CopyUserInfo(ref copyUserInfoOptions, out var friendInfoNullable); + if (copyUserInfoResult != Epic.OnlineServices.Result.Success) + { + return Result.Failure(EosInterface.Friends.GetFriendsError.CopyUserInfoFailed); + } + if (friendInfoNullable is not { } friendInfo) + { + return Result.Failure(EosInterface.Friends.GetFriendsError.CopyUserInfoFailed); + } + + string displayName = friendInfo.Nickname ?? friendInfo.DisplayName ?? ""; + if (string.IsNullOrEmpty(displayName)) + { + return Result.Failure(EosInterface.Friends.GetFriendsError.DisplayNameIsEmpty); + } + + return Result.Success(friendInfo); + } + + internal static async Task> GetPresenceFromUserInfoData( + EpicAccountId selfEaid, EpicAccountId friendEaid, Epic.OnlineServices.UserInfo.UserInfoData friendInfo) + { + if (CorePrivate.EgsPresenceInterface is not { } egsPresenceInterface) { return Result.Failure(EosInterface.Friends.GetFriendsError.EosNotInitialized); } + + var selfEaidInternal = Epic.OnlineServices.EpicAccountId.FromString(selfEaid.EosStringRepresentation); + var friendEaidInternal = friendInfo.UserId; + + string displayName = friendInfo.Nickname ?? friendInfo.DisplayName ?? ""; + + var queryPresenceOptions = new Epic.OnlineServices.Presence.QueryPresenceOptions + { + LocalUserId = selfEaidInternal, + TargetUserId = friendEaidInternal + }; + var queryPresenceWaiter = new CallbackWaiter(); + egsPresenceInterface.QueryPresence(options: ref queryPresenceOptions, clientData: null, completionDelegate: queryPresenceWaiter.OnCompletion); + var queryPresenceResult = await queryPresenceWaiter.Task; + if (!queryPresenceResult.TryUnwrap(out var queryPresence)) + { + return Result.Failure(EosInterface.Friends.GetFriendsError.EgsPresenceQueryTimedOut); + } + if (queryPresence.ResultCode != Epic.OnlineServices.Result.Success) + { + return Result.Failure(EosInterface.Friends.GetFriendsError.EgsPresenceQueryFailed); + } + + var copyPresenceOptions = new Epic.OnlineServices.Presence.CopyPresenceOptions + { + LocalUserId = selfEaidInternal, + TargetUserId = friendEaidInternal + }; + var copyPresenceResult = egsPresenceInterface.CopyPresence(ref copyPresenceOptions, out var friendPresenceNullable); + if (copyPresenceResult != Epic.OnlineServices.Result.Success) + { + return Result.Failure(EosInterface.Friends.GetFriendsError.CopyPresenceFailed); + } + if (friendPresenceNullable is not { } friendPresence) + { + return Result.Failure(EosInterface.Friends.GetFriendsError.CopyPresenceFailed); + } + + string productId = friendPresence.ProductId ?? ""; + var friendStatus = friendPresence.Status switch + { + Epic.OnlineServices.Presence.Status.Offline + => FriendStatus.Offline, + _ + => productId == PlatformInterfaceOptionsPrivate.BasePlatformInterfaceOptions.ProductId + ? FriendStatus.PlayingBarotrauma + : !string.IsNullOrEmpty(productId) + ? FriendStatus.PlayingAnotherGame + : FriendStatus.NotPlaying + }; + + var records = friendPresence.Records ?? Array.Empty(); + + string getRecordValue(string key) + => records + .FirstOrNone(r => string.Equals(r.Key, key, StringComparison.OrdinalIgnoreCase)) + .Select(r => r.Value) + .Fallback(""); + var connectCommand = getRecordValue("connectcommand"); + var serverName = getRecordValue("servername"); + + return Result.Success(new EosInterface.EgsFriend( + DisplayName: displayName, + EpicAccountId: friendEaid, + Status: friendStatus, + ConnectCommand: connectCommand, + ServerName: serverName)); + } + + public static async Task> GetSelfUserInfo(EpicAccountId epicAccount) + { + var getUserInfoDataResult = await GetUserInfoData(epicAccount, epicAccount); + if (getUserInfoDataResult.TryUnwrapFailure(out var error)) + { + return Result.Failure(error); + } + if (!getUserInfoDataResult.TryUnwrapSuccess(out var friendInfo)) + { + throw new UnreachableCodeException(); + } + + string displayName = friendInfo.Nickname ?? friendInfo.DisplayName ?? ""; + + return Result.Success(new EosInterface.EgsFriend( + DisplayName: displayName, + EpicAccountId: epicAccount, + Status: FriendStatus.PlayingBarotrauma, + ConnectCommand: "", + ServerName: "")); + } + + public static async Task> GetFriend(EpicAccountId selfEaid, EpicAccountId friendEaid) + { + var getUserInfoDataResult = await GetUserInfoData(selfEaid, friendEaid); + if (getUserInfoDataResult.TryUnwrapFailure(out var error)) + { + return Result.Failure(error); + } + if (!getUserInfoDataResult.TryUnwrapSuccess(out var friendInfo)) + { + throw new UnreachableCodeException(); + } + + return await GetPresenceFromUserInfoData(selfEaid, friendEaid, friendInfo); + } + + public static async Task, EosInterface.Friends.GetFriendsError>> GetFriends(EpicAccountId epicAccount) + { + if (CorePrivate.EgsFriendsInterface is not { } egsFriendsInterface) { return Result.Failure(EosInterface.Friends.GetFriendsError.EosNotInitialized); } + + var selfEaidInternal = Epic.OnlineServices.EpicAccountId.FromString(epicAccount.EosStringRepresentation); + + var queryFriendsOptions = new Epic.OnlineServices.Friends.QueryFriendsOptions + { + LocalUserId = selfEaidInternal + }; + var queryFriendsWaiter = new CallbackWaiter(); + egsFriendsInterface.QueryFriends(options: ref queryFriendsOptions, clientData: null, completionDelegate: queryFriendsWaiter.OnCompletion); + var queryFriendsInfoResult = await queryFriendsWaiter.Task; + if (!queryFriendsInfoResult.TryUnwrap(out var queryFriendsInfo)) + { + return Result.Failure(EosInterface.Friends.GetFriendsError.EgsFriendsQueryTimedOut); + } + + if (queryFriendsInfo.ResultCode != Epic.OnlineServices.Result.Success) + { + return Result.Failure(EosInterface.Friends.GetFriendsError.EgsFriendsQueryFailed); + } + + var getFriendsCountOptions = new Epic.OnlineServices.Friends.GetFriendsCountOptions + { + LocalUserId = selfEaidInternal + }; + var friendCount = egsFriendsInterface.GetFriendsCount(ref getFriendsCountOptions); + var friends = new List(); + + for (int i = 0; i < friendCount; i++) + { + var getFriendAtIndexOptions = new Epic.OnlineServices.Friends.GetFriendAtIndexOptions + { + LocalUserId = selfEaidInternal, + Index = i + }; + var friendId = egsFriendsInterface.GetFriendAtIndex(ref getFriendAtIndexOptions); + if (friendId == null) + { + continue; + } + + if (!EpicAccountId.Parse(friendId.ToString()).TryUnwrap(out var friendIdPublic)) + { + continue; + } + + var getUserInfoDataResult = await GetUserInfoData(epicAccount, friendIdPublic); + if (!getUserInfoDataResult.TryUnwrapSuccess(out var friendInfo)) + { + continue; + } + + var egsFriendPublicResult = await GetPresenceFromUserInfoData(epicAccount, friendIdPublic, friendInfo); + if (!egsFriendPublicResult.TryUnwrapSuccess(out var egsFriendPublic)) + { + continue; + } + + friends.Add(egsFriendPublic); + } + return Result.Success(friends.ToImmutableArray()); + } +} + +internal sealed partial class ImplementationPrivate : EosInterface.Implementation +{ + public override Task> GetFriend(EpicAccountId selfEaid, EpicAccountId friendEaid) + => TaskScheduler.Schedule(() => FriendsPrivate.GetFriend(selfEaid, friendEaid)); + + public override Task, EosInterface.Friends.GetFriendsError>> GetFriends(EpicAccountId epicAccountId) + => TaskScheduler.Schedule(() => FriendsPrivate.GetFriends(epicAccountId)); + + public override Task> GetSelfUserInfo(EpicAccountId epicAccountId) + => TaskScheduler.Schedule(() => FriendsPrivate.GetSelfUserInfo(epicAccountId)); +} diff --git a/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/Friends/PresencePrivate.cs b/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/Friends/PresencePrivate.cs new file mode 100644 index 000000000..c0dbde509 --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/Friends/PresencePrivate.cs @@ -0,0 +1,465 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Barotrauma; +using Barotrauma.Extensions; +using EpicAccountId = Barotrauma.Networking.EpicAccountId; +using Result = Barotrauma.Result; + +namespace EosInterfacePrivate; + +static class PresencePrivate +{ + internal static readonly NamedEvent OnJoinGame = new NamedEvent(); + private static ulong joinGameAcceptedNotificationId = Epic.OnlineServices.Common.InvalidNotificationid; + + internal static readonly NamedEvent OnInviteAccepted = new NamedEvent(); + private static ulong inviteAcceptedNotificationId = Epic.OnlineServices.Common.InvalidNotificationid; + private static ulong inviteRejectedNotificationId = Epic.OnlineServices.Common.InvalidNotificationid; + + internal static readonly NamedEvent OnInviteReceived = new NamedEvent(); + private static ulong inviteReceivedNotificationId = Epic.OnlineServices.Common.InvalidNotificationid; + + public static void Init(ImplementationPrivate implementation) + { + var presenceInterface = CorePrivate.EgsPresenceInterface; + var customInvitesInterface = CorePrivate.EgsCustomInvitesInterface; + if (presenceInterface is null + || customInvitesInterface is null) + { + return; + } + + var boilerplate0 = new Epic.OnlineServices.Presence.AddNotifyJoinGameAcceptedOptions(); + joinGameAcceptedNotificationId = presenceInterface.AddNotifyJoinGameAccepted(ref boilerplate0, null, OnJoinGameAcceptedEos); + + var boilerplate1 = new Epic.OnlineServices.CustomInvites.AddNotifyCustomInviteAcceptedOptions(); + inviteAcceptedNotificationId = customInvitesInterface.AddNotifyCustomInviteAccepted(ref boilerplate1, implementation, OnInviteAcceptedEos); + + var boilerplate2 = new Epic.OnlineServices.CustomInvites.AddNotifyCustomInviteRejectedOptions(); + inviteRejectedNotificationId = customInvitesInterface.AddNotifyCustomInviteRejected(ref boilerplate2, null, OnInviteRejectedEos); + + var boilerplate3 = new Epic.OnlineServices.CustomInvites.AddNotifyCustomInviteReceivedOptions(); + inviteReceivedNotificationId = customInvitesInterface.AddNotifyCustomInviteReceived(ref boilerplate3, implementation, OnInviteReceivedEos); + } + + public static void Quit() + { + OnJoinGame.Dispose(); + OnInviteAccepted.Dispose(); + + var presenceInterface = CorePrivate.EgsPresenceInterface; + var customInvitesInterface = CorePrivate.EgsCustomInvitesInterface; + if (presenceInterface is null + || customInvitesInterface is null) + { + return; + } + + static void callRemover(Action remover, ref ulong id) + { + remover(id); + id = Epic.OnlineServices.Common.InvalidNotificationid; + } + + callRemover(presenceInterface.RemoveNotifyJoinGameAccepted, ref joinGameAcceptedNotificationId); + callRemover(customInvitesInterface.RemoveNotifyCustomInviteAccepted, ref inviteAcceptedNotificationId); + callRemover(customInvitesInterface.RemoveNotifyCustomInviteRejected, ref inviteRejectedNotificationId); + callRemover(customInvitesInterface.RemoveNotifyCustomInviteReceived, ref inviteReceivedNotificationId); + } + + private static void OnJoinGameAcceptedEos(ref Epic.OnlineServices.Presence.JoinGameAcceptedCallbackInfo data) + { + if (data.UiEventId != Epic.OnlineServices.UI.UIInterface.EventidInvalid) + { + // What is this for? I have no idea. + // Documentation says it's important tho: + // https://dev.epicgames.com/docs/epic-account-services/social-overlay-overview/sdk-integration#invite-lifecycle-and-caveats + var egsUiInterface = CorePrivate.EgsUiInterface; + if (egsUiInterface != null) + { + var ack = new Epic.OnlineServices.UI.AcknowledgeEventIdOptions + { + UiEventId = data.UiEventId, + Result = Epic.OnlineServices.Result.Success + }; + egsUiInterface.AcknowledgeEventId(ref ack); + } + } + + var selfEpicIdOption = EpicAccountId.Parse(data.LocalUserId.ToString()); + if (!selfEpicIdOption.TryUnwrap(out var selfEpicId)) { return; } + + var joinCommandStr = data.JoinInfo; + + OnJoinGame.Invoke(new EosInterface.Presence.JoinGameInfo(selfEpicId, joinCommandStr)); + } + + private static void OnInviteAcceptedEos(ref Epic.OnlineServices.CustomInvites.OnCustomInviteAcceptedCallbackInfo data) + { + if (data.LocalUserId is null) { return; } + if (data.ClientData is not ImplementationPrivate implementation) { return; } + + RemoveInvite( + recipientPuid: new EosInterface.ProductUserId(data.LocalUserId.ToString()), + senderPuid: new EosInterface.ProductUserId(data.TargetUserId.ToString())); + + var joinCommandStr = data.Payload; + + var selfPuid = new EosInterface.ProductUserId(data.LocalUserId.ToString()); + + async Task> prepareCallbackInfo() + { + var selfExternalAccountIdsTask = IdQueriesPrivate.GetExternalAccountIds(selfPuid, selfPuid); + + await Task.WhenAll(selfExternalAccountIdsTask, selfExternalAccountIdsTask); + + var selfExternalAccountIdsResult = await selfExternalAccountIdsTask; + + if (!selfExternalAccountIdsResult.TryUnwrapSuccess(out var selfExternalAccountIds) + || !selfExternalAccountIds.OfType().FirstOrNone().TryUnwrap(out var selfEpicAccountId)) + { + return Option.None; + } + + return Option.Some(new EosInterface.Presence.AcceptInviteInfo( + selfEpicAccountId, + joinCommandStr)); + } + + TaskPool.Add( + $"AcceptedInviteFor{selfPuid.Value}", + implementation.TaskScheduler.Schedule(prepareCallbackInfo), + t => + { + if (!t.TryGetResult(out Option infoOption)) { return; } + if (!infoOption.TryUnwrap(out var info)) { return; } + + OnInviteAccepted.Invoke(info); + }); + } + + private static void OnInviteRejectedEos(ref Epic.OnlineServices.CustomInvites.CustomInviteRejectedCallbackInfo data) + { + if (data.LocalUserId is null) { return; } + + RemoveInvite( + recipientPuid: new EosInterface.ProductUserId(data.LocalUserId.ToString()), + senderPuid: new EosInterface.ProductUserId(data.TargetUserId.ToString())); + } + + private readonly record struct InviteId( + EpicAccountId RecipientEpicId, + EpicAccountId SenderEpicId, + EosInterface.ProductUserId RecipientPuid, + EosInterface.ProductUserId SenderPuid, + string IdValue); + + private static readonly List ReceivedInviteIds = new List(); + + private static void RemoveInvite(EpicAccountId recipientEpicId, EpicAccountId senderEpicId) + { + RemoveInvites(ReceivedInviteIds.Where(id => id.RecipientEpicId == recipientEpicId && id.SenderEpicId == senderEpicId).ToImmutableArray()); + } + + private static void RemoveInvite(EosInterface.ProductUserId recipientPuid, EosInterface.ProductUserId senderPuid) + { + RemoveInvites(ReceivedInviteIds.Where(id => id.RecipientPuid == recipientPuid && id.SenderPuid == senderPuid).ToImmutableArray()); + } + + private static void RemoveInvites(ImmutableArray invites) + { + var customInvitesInterface = CorePrivate.EgsCustomInvitesInterface; + if (customInvitesInterface == null) { return; } + + foreach (var invite in invites) + { + ReceivedInviteIds.Remove(invite); + var targetUserId = Epic.OnlineServices.ProductUserId.FromString(invite.SenderPuid.Value); + var localUserId = Epic.OnlineServices.ProductUserId.FromString(invite.RecipientPuid.Value); + var finalizeInviteOptions = new Epic.OnlineServices.CustomInvites.FinalizeInviteOptions + { + TargetUserId = targetUserId, + LocalUserId = localUserId, + CustomInviteId = invite.IdValue, + ProcessingResult = Epic.OnlineServices.Result.Success + }; + customInvitesInterface.FinalizeInvite(ref finalizeInviteOptions); + } + } + + private static void OnInviteReceivedEos(ref Epic.OnlineServices.CustomInvites.OnCustomInviteReceivedCallbackInfo data) + { + if (data.ClientData is not ImplementationPrivate implementation) { return; } + var joinCommandStr = data.Payload; + + var selfPuid = new EosInterface.ProductUserId(data.LocalUserId.ToString()); + var senderPuid = new EosInterface.ProductUserId(data.TargetUserId.ToString()); + var inviteIdValue = data.CustomInviteId; + + // We can only have one invite for the same recipient-sender pair + RemoveInvite( + recipientPuid: selfPuid, + senderPuid: senderPuid); + + async Task> prepareCallbackInfo() + { + var selfExternalAccountIdsTask = IdQueriesPrivate.GetExternalAccountIds(selfPuid, selfPuid); + var senderExternalAccountIdsTask = IdQueriesPrivate.GetExternalAccountIds(selfPuid, senderPuid); + + await Task.WhenAll(selfExternalAccountIdsTask, selfExternalAccountIdsTask); + + var selfExternalAccountIdsResult = await selfExternalAccountIdsTask; + var senderExternalAccountIdsResult = await senderExternalAccountIdsTask; + + if (!selfExternalAccountIdsResult.TryUnwrapSuccess(out var selfExternalAccountIds) + || !selfExternalAccountIds.OfType().FirstOrNone().TryUnwrap(out var selfEpicAccountId)) + { + return Option.None; + } + + if (!senderExternalAccountIdsResult.TryUnwrapSuccess(out var senderExternalAccountIds) + || !senderExternalAccountIds.OfType().FirstOrNone().TryUnwrap(out var senderEpicAccountId)) + { + return Option.None; + } + + return Option.Some(new EosInterface.Presence.ReceiveInviteInfo( + selfEpicAccountId, + senderEpicAccountId, + joinCommandStr)); + } + + TaskPool.Add( + $"ReceivedInviteFrom{senderPuid.Value}", + implementation.TaskScheduler.Schedule(prepareCallbackInfo), + t => + { + if (!t.TryGetResult(out Option infoOption)) { return; } + + if (!infoOption.TryUnwrap(out var info)) { return; } + + ReceivedInviteIds.Add(new InviteId( + RecipientEpicId: info.RecipientId, + SenderEpicId: info.SenderId, + RecipientPuid: selfPuid, + SenderPuid: senderPuid, + IdValue: inviteIdValue)); + + OnInviteReceived.Invoke(info); + }); + } + + public static async Task> SetJoinCommand( + EpicAccountId epicAccountId, + string desc, + string serverName, + string joinCommand) + { + if (string.IsNullOrWhiteSpace(joinCommand)) + { + desc = ""; + } + + if (desc.Length > Epic.OnlineServices.Presence.PresenceInterface.RichTextMaxValueLength) + { + return Result.Failure(EosInterface.Presence.SetJoinCommandError.DescTooLong); + } + if (joinCommand.Length > Epic.OnlineServices.Presence.PresenceModification.PresencemodificationJoininfoMaxLength) + { + return Result.Failure(EosInterface.Presence.SetJoinCommandError.JoinCommandTooLong); + } + + if (serverName.Length > Epic.OnlineServices.Presence.PresenceInterface.DataMaxValueLength) + { + return Result.Failure(EosInterface.Presence.SetJoinCommandError.ServerNameTooLong); + } + if (joinCommand.Length > Epic.OnlineServices.Presence.PresenceInterface.DataMaxValueLength) + { + return Result.Failure(EosInterface.Presence.SetJoinCommandError.JoinCommandTooLong); + } + + using var janitor = Janitor.Start(); + + var presenceInterface = CorePrivate.EgsPresenceInterface; + var customInvitesInterface = CorePrivate.EgsCustomInvitesInterface; + if (presenceInterface is null + || customInvitesInterface is null) + { + return Result.Failure(EosInterface.Presence.SetJoinCommandError.EosNotInitialized); + } + + var epicAccountIdInternal = Epic.OnlineServices.EpicAccountId.FromString(epicAccountId.EosStringRepresentation); + + var puidResult = await IdQueriesPrivate.GetPuidForExternalId(epicAccountId); + if (!puidResult.TryUnwrapSuccess(out var puid)) + { + return Result.Failure(EosInterface.Presence.SetJoinCommandError.FailedToGetPuid); + } + + var puidInternal = Epic.OnlineServices.ProductUserId.FromString(puid.Value); + + var setCustomInviteOptions = new Epic.OnlineServices.CustomInvites.SetCustomInviteOptions + { + LocalUserId = puidInternal, + Payload = joinCommand + }; + var setCustomInviteResult = customInvitesInterface.SetCustomInvite(ref setCustomInviteOptions); + if (setCustomInviteResult != Epic.OnlineServices.Result.Success) + { + return Result.Failure(EosInterface.Presence.SetJoinCommandError.FailedToSetCustomInvite); + } + + var createPresenceModificationOptions = new Epic.OnlineServices.Presence.CreatePresenceModificationOptions + { + LocalUserId = epicAccountIdInternal + }; + var createPresenceModificationResult = presenceInterface.CreatePresenceModification(ref createPresenceModificationOptions, out var presenceModification); + janitor.AddAction(presenceModification.Release); + if (createPresenceModificationResult != Epic.OnlineServices.Result.Success) + { + return Result.Failure(EosInterface.Presence.SetJoinCommandError.FailedToCreatePresenceModification); + } + + var setRichTextOptions = new Epic.OnlineServices.Presence.PresenceModificationSetRawRichTextOptions + { + RichText = desc + }; + var setRichTextResult = presenceModification.SetRawRichText(ref setRichTextOptions); + if (setRichTextResult != Epic.OnlineServices.Result.Success) + { + return Result.Failure(EosInterface.Presence.SetJoinCommandError.FailedToSetRichText); + } + + var setDataOptions = new Epic.OnlineServices.Presence.PresenceModificationSetDataOptions + { + Records = new[] + { + new Epic.OnlineServices.Presence.DataRecord + { + Key = "servername", + Value = serverName + }, + new Epic.OnlineServices.Presence.DataRecord + { + Key = "connectcommand", + Value = joinCommand + } + } + }; + var setDataResult = presenceModification.SetData(ref setDataOptions); + if (setDataResult != Epic.OnlineServices.Result.Success) + { + return Result.Failure(EosInterface.Presence.SetJoinCommandError.FailedToSetRecords); + } + + // This is necessary to make the SDK not choke if given an empty, but not null, joinCommand + string? joinCommandNullable = string.IsNullOrWhiteSpace(joinCommand) ? null : joinCommand; + + var setJoinInfoOptions = new Epic.OnlineServices.Presence.PresenceModificationSetJoinInfoOptions + { + JoinInfo = joinCommandNullable + }; + var setJoinInfoResult = presenceModification.SetJoinInfo(ref setJoinInfoOptions); + if (setJoinInfoResult != Epic.OnlineServices.Result.Success) + { + return Result.Failure(EosInterface.Presence.SetJoinCommandError.FailedToSetJoinInfo); + } + + var setPresenceOptions = new Epic.OnlineServices.Presence.SetPresenceOptions + { + LocalUserId = epicAccountIdInternal, + PresenceModificationHandle = presenceModification + }; + var setPresenceWaiter = new CallbackWaiter(); + presenceInterface.SetPresence(options: ref setPresenceOptions, clientData: null, completionDelegate: setPresenceWaiter.OnCompletion); + var setPresenceResultOption = await setPresenceWaiter.Task; + if (!setPresenceResultOption.TryUnwrap(out var setPresenceResult)) + { + return Result.Failure(EosInterface.Presence.SetJoinCommandError.SetPresenceTimedOut); + } + + if (setPresenceResult.ResultCode != Epic.OnlineServices.Result.Success) + { + return Result.Failure(EosInterface.Presence.SetJoinCommandError.FailedToSetPresence); + } + + return Result.Success(Unit.Value); + } + + public static async Task> SendInvite(EpicAccountId selfEpicAccountId, EpicAccountId remoteEpicAccountId) + { + var customInvitesInterface = CorePrivate.EgsCustomInvitesInterface; + if (customInvitesInterface is null) + { + return Result.Failure(EosInterface.Presence.SendInviteError.EosNotInitialized); + } + + var selfPuidResult = await IdQueriesPrivate.GetPuidForExternalId(selfEpicAccountId); + if (!selfPuidResult.TryUnwrapSuccess(out var selfPuid)) + { + return Result.Failure(EosInterface.Presence.SendInviteError.FailedToGetSelfPuid); + } + + var selfPuidInternal = Epic.OnlineServices.ProductUserId.FromString(selfPuid.Value); + + var remotePuidResult = await IdQueriesPrivate.GetPuidForExternalId(remoteEpicAccountId); + if (!remotePuidResult.TryUnwrapSuccess(out var remotePuid)) + { + return Result.Failure(EosInterface.Presence.SendInviteError.FailedToGetRemotePuid); + } + + var remotePuidInternal = Epic.OnlineServices.ProductUserId.FromString(remotePuid.Value); + + var sendCustomInviteOptions = new Epic.OnlineServices.CustomInvites.SendCustomInviteOptions + { + LocalUserId = selfPuidInternal, + TargetUserIds = new[] + { + remotePuidInternal + } + }; + var callbackWaiter = new CallbackWaiter(); + customInvitesInterface.SendCustomInvite(options: ref sendCustomInviteOptions, clientData: null, completionDelegate: callbackWaiter.OnCompletion); + var callbackResultOption = await callbackWaiter.Task; + if (!callbackResultOption.TryUnwrap(out var callbackResult)) + { + return Result.Failure(EosInterface.Presence.SendInviteError.TimedOut); + } + + if (callbackResult.ResultCode != Epic.OnlineServices.Result.Success) + { + return Result.Failure(EosInterface.Presence.SendInviteError.InternalError); + } + + return Result.Success(Unit.Value); + } + + public static void DeclineInvite(EpicAccountId selfEpicAccountId, EpicAccountId senderEpicAccountId) + { + RemoveInvite( + recipientEpicId: selfEpicAccountId, + senderEpicId: senderEpicAccountId); + } +} + +internal sealed partial class ImplementationPrivate : EosInterface.Implementation +{ + public override NamedEvent OnJoinGame => PresencePrivate.OnJoinGame; + public override NamedEvent OnInviteAccepted => PresencePrivate.OnInviteAccepted; + public override NamedEvent OnInviteReceived => PresencePrivate.OnInviteReceived; + + public override Task> SetJoinCommand( + EpicAccountId epicAccountId, string desc, string serverName, string joinCommand) + => TaskScheduler.Schedule(() => PresencePrivate.SetJoinCommand(epicAccountId, desc, serverName, joinCommand)); + + public override Task> SendInvite( + EpicAccountId selfEpicAccountId, EpicAccountId remoteEpicAccountId) + => TaskScheduler.Schedule(() => PresencePrivate.SendInvite(selfEpicAccountId, remoteEpicAccountId)); + + public override void DeclineInvite(EpicAccountId selfEpicAccountId, EpicAccountId senderEpicAccountId) + => PresencePrivate.DeclineInvite(selfEpicAccountId, senderEpicAccountId); +} diff --git a/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/IdAndAuth/EgsIdTokenPrivate.cs b/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/IdAndAuth/EgsIdTokenPrivate.cs new file mode 100644 index 000000000..ebf780b44 --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/IdAndAuth/EgsIdTokenPrivate.cs @@ -0,0 +1,119 @@ +#nullable enable +using System.Text.Json; +using System.Threading.Tasks; +using Barotrauma.Networking; +using Barotrauma; + +namespace EosInterfacePrivate; + +public sealed class EgsIdTokenPrivate : EosInterface.EgsIdToken +{ + public override EpicAccountId AccountId { get; } + + internal static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions + { + IncludeFields = true + }; + + internal readonly record struct TokenStruct( + string AccountId, + string JsonWebToken); + + internal readonly Epic.OnlineServices.Auth.IdToken InternalToken; + internal EgsIdTokenPrivate(EpicAccountId accountId, Epic.OnlineServices.Auth.IdToken internalToken) + { + AccountId = accountId; + InternalToken = internalToken; + } + + public new static Option Parse(string str) + { + try + { + if (JsonSerializer.Deserialize( + str, + returnType: typeof(TokenStruct), + options: JsonSerializerOptions) + is not TokenStruct tokenStruct) + { + return Option.None; + } + + if (!EpicAccountId.Parse(tokenStruct.AccountId).TryUnwrap(out var accountId)) { return Option.None; } + + var internalToken = new Epic.OnlineServices.Auth.IdToken + { + AccountId = Epic.OnlineServices.EpicAccountId.FromString(tokenStruct.AccountId), + JsonWebToken = tokenStruct.JsonWebToken + }; + + return Option.Some(new EgsIdTokenPrivate(accountId, internalToken)); + } + catch + { + return Option.None; + } + } + + public override string ToString() + { + var tokenStruct = new TokenStruct( + AccountId: InternalToken.AccountId.ToString(), + JsonWebToken: InternalToken.JsonWebToken); + return JsonSerializer.Serialize(tokenStruct, options: JsonSerializerOptions); + } + + public static Result GetEgsIdTokenForEpicAccountId(EpicAccountId accountId) + { + var (success, failure) = Result.GetFactoryMethods(); + + if (CorePrivate.EgsAuthInterface is not { } egsAuthInterface) { return failure(EosInterface.GetEgsSelfIdTokenError.EosNotInitialized); } + + var copyIdTokenOptions = new Epic.OnlineServices.Auth.CopyIdTokenOptions + { + AccountId = Epic.OnlineServices.EpicAccountId.FromString(accountId.EosStringRepresentation) + }; + var copyIdTokenResult = egsAuthInterface.CopyIdToken(ref copyIdTokenOptions, out var idTokenNullable); + + if (copyIdTokenResult is Epic.OnlineServices.Result.NotFound) { return failure(EosInterface.GetEgsSelfIdTokenError.InvalidToken); } + if (copyIdTokenResult != Epic.OnlineServices.Result.Success) { return failure(EosInterface.GetEgsSelfIdTokenError.UnhandledErrorCondition); } + if (idTokenNullable is not { } idToken) { return failure(EosInterface.GetEgsSelfIdTokenError.UnhandledErrorCondition); } + + return success(new EgsIdTokenPrivate(accountId, idToken)); + } + + public override async Task Verify(AccountId accountId) + { + if (CorePrivate.EgsAuthInterface is not { } egsAuthInterface) { return EosInterface.VerifyEgsIdTokenResult.Failed; } + + var verifyIdTokenOptions = new Epic.OnlineServices.Auth.VerifyIdTokenOptions + { + IdToken = InternalToken + }; + var verifyIdTokenWaiter = new CallbackWaiter(); + egsAuthInterface.VerifyIdToken(options: ref verifyIdTokenOptions, clientData: null, completionDelegate: verifyIdTokenWaiter.OnCompletion); + var result = await verifyIdTokenWaiter.Task; + if (!result.TryUnwrap(out var callbackInfo)) { return EosInterface.VerifyEgsIdTokenResult.Failed; } + + if (callbackInfo.ResultCode != Epic.OnlineServices.Result.Success + || callbackInfo.ProductId != CorePrivate.PlatformInterfaceOptions.ProductId) + { + return EosInterface.VerifyEgsIdTokenResult.Failed; + } + + var resultAccountId = IdQueriesPrivate.EosStringToAccountId(callbackInfo.ExternalAccountId, callbackInfo.ExternalAccountIdType); + + return resultAccountId.TryUnwrap(out var resultId) && resultId == accountId + ? EosInterface.VerifyEgsIdTokenResult.Verified + : EosInterface.VerifyEgsIdTokenResult.Failed; + } +} + +internal sealed partial class ImplementationPrivate : EosInterface.Implementation +{ + public override Option ParseEgsIdToken(string str) + => EgsIdTokenPrivate.Parse(str).Select(t => (EosInterface.EgsIdToken)t); + + public override Result GetEgsIdTokenForEpicAccountId(EpicAccountId accountId) + => EgsIdTokenPrivate.GetEgsIdTokenForEpicAccountId(accountId); +} diff --git a/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/IdAndAuth/EosIdTokenPrivate.cs b/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/IdAndAuth/EosIdTokenPrivate.cs new file mode 100644 index 000000000..6069eb30c --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/IdAndAuth/EosIdTokenPrivate.cs @@ -0,0 +1,73 @@ +#nullable enable +using System.Threading.Tasks; +using Barotrauma.Networking; +using Barotrauma; + +namespace EosInterfacePrivate; + +static class EosIdTokenPrivate +{ + public static Result GetEosIdTokenForProductUserId(EosInterface.ProductUserId puid) + { + var (success, failure) = Result.GetFactoryMethods(); + + if (CorePrivate.ConnectInterface is not { } connectInterface) { return failure(EosInterface.GetEosSelfIdTokenError.EosNotInitialized); } + + var copyIdTokenOptions = new Epic.OnlineServices.Connect.CopyIdTokenOptions + { + LocalUserId = Epic.OnlineServices.ProductUserId.FromString(puid.Value) + }; + var copyIdTokenResult = connectInterface.CopyIdToken(ref copyIdTokenOptions, out var idTokenNullable); + + if (copyIdTokenResult is Epic.OnlineServices.Result.NotFound) { return failure(EosInterface.GetEosSelfIdTokenError.InvalidToken); } + if (copyIdTokenResult != Epic.OnlineServices.Result.Success) { return failure(EosInterface.GetEosSelfIdTokenError.UnhandledErrorCondition); } + if (idTokenNullable is not { } idToken) { return failure(EosInterface.GetEosSelfIdTokenError.UnhandledErrorCondition); } + + if (!JsonWebToken.Parse(idToken.JsonWebToken).TryUnwrap(out var jsonWebToken)) { return failure(EosInterface.GetEosSelfIdTokenError.CouldNotParseJwt); } + + return success(new EosInterface.EosIdToken(new EosInterface.ProductUserId(idToken.ProductUserId.ToString()), jsonWebToken)); + } + + public static async Task> Verify(EosInterface.EosIdToken token) + { + if (CorePrivate.ConnectInterface is not { } connectInterface) { return Result.Failure(EosInterface.VerifyEosIdTokenError.EosNotInitialized); } + + var verifyIdTokenOptions = new Epic.OnlineServices.Connect.VerifyIdTokenOptions + { + IdToken = new Epic.OnlineServices.Connect.IdToken + { + ProductUserId = Epic.OnlineServices.ProductUserId.FromString(token.ProductUserId.Value), + JsonWebToken = token.JsonWebToken.ToString() + } + }; + var verifyIdTokenWaiter = new CallbackWaiter(); + connectInterface.VerifyIdToken(options: ref verifyIdTokenOptions, clientData: null, completionDelegate: verifyIdTokenWaiter.OnCompletion); + var result = await verifyIdTokenWaiter.Task; + if (!result.TryUnwrap(out var callbackInfo)) { return Result.Failure(EosInterface.VerifyEosIdTokenError.TimedOut); } + + if (callbackInfo.ResultCode != Epic.OnlineServices.Result.Success) + { + return Result.Failure(EosInterface.VerifyEosIdTokenError.UnhandledErrorCondition); + } + + if (callbackInfo.ProductId != CorePrivate.PlatformInterfaceOptions.ProductId) + { + return Result.Failure(EosInterface.VerifyEosIdTokenError.ProductIdDidNotMatch); + } + + var resultAccountId = IdQueriesPrivate.EosStringToAccountId(callbackInfo.AccountId, callbackInfo.AccountIdType); + + return resultAccountId.TryUnwrap(out var resultId) + ? Result.Success(resultId) + : Result.Failure(EosInterface.VerifyEosIdTokenError.CouldNotParseExternalAccountId); + } +} + +internal sealed partial class ImplementationPrivate : EosInterface.Implementation +{ + public override Task> VerifyEosIdToken(EosInterface.EosIdToken token) + => TaskScheduler.Schedule(() => EosIdTokenPrivate.Verify(token)); + + public override Result GetEosIdTokenForProductUserId(EosInterface.ProductUserId puid) + => EosIdTokenPrivate.GetEosIdTokenForProductUserId(puid); +} diff --git a/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/IdAndAuth/IdQueriesPrivate.cs b/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/IdAndAuth/IdQueriesPrivate.cs new file mode 100644 index 000000000..b561ce341 --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/IdAndAuth/IdQueriesPrivate.cs @@ -0,0 +1,222 @@ +#nullable enable +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Barotrauma.Extensions; +using Barotrauma.Networking; +using Barotrauma; + +namespace EosInterfacePrivate; + +static class IdQueriesPrivate +{ + public static ImmutableArray GetLoggedInPuids() + { + if (CorePrivate.ConnectInterface is not { } connectInterface) { return ImmutableArray.Empty; } + + int count = connectInterface.GetLoggedInUsersCount(); + var ids = new List(); + foreach (int i in Enumerable.Range(0, count)) + { + if (connectInterface.GetLoggedInUserByIndex(i) is not { } userId) { return ImmutableArray.Empty; } + var newPuid = new EosInterface.ProductUserId(userId.ToString()); + if (!LoginPrivate.PuidToPrimaryExternalId.ContainsKey(newPuid)) { continue; } + ids.Add(newPuid); + } + + return ids.ToImmutableArray(); + } + + public static ImmutableArray GetLoggedInEpicIds() + { + if (CorePrivate.EgsAuthInterface is not { } egsAuthInterface) { return ImmutableArray.Empty; } + + int count = egsAuthInterface.GetLoggedInAccountsCount(); + var ids = new List(); + foreach (int i in Enumerable.Range(0, count)) + { + if (egsAuthInterface.GetLoggedInAccountByIndex(i) is not { } userId) { return ImmutableArray.Empty; } + var newEpicIdOption = EpicAccountId.Parse(userId.ToString()); + if (!newEpicIdOption.TryUnwrap(out var newEpicId)) { return ImmutableArray.Empty; } + ids.Add(newEpicId); + } + + return ids.ToImmutableArray(); + } + + public static Task, EosInterface.IdQueries.GetSelfExternalIdError>> + GetSelfExternalAccountIds( + EosInterface.ProductUserId productUserId) + => GetExternalAccountIds(productUserId, productUserId); + + internal static async Task, EosInterface.IdQueries.GetSelfExternalIdError>> + GetExternalAccountIds( + EosInterface.ProductUserId selfPuid, + EosInterface.ProductUserId puidToGetIdsFor) + { + // If logged only into an Epic account, you cannot fetch SteamIDs. + // See Epic.OnlineServices.Connect.ExternalAccountInfo.AccountId + + var (success, failure) = Result, EosInterface.IdQueries.GetSelfExternalIdError>.GetFactoryMethods(); + + if (CorePrivate.ConnectInterface is not { } connectInterface) + { + return failure(EosInterface.IdQueries.GetSelfExternalIdError.EosNotInitialized); + } + if (!LoginPrivate.PuidToPrimaryExternalId.ContainsKey(selfPuid)) + { + return failure(EosInterface.IdQueries.GetSelfExternalIdError.Inaccessible); + } + + var selfPuidInternal = Epic.OnlineServices.ProductUserId.FromString(selfPuid.Value); + var otherPuidInternal = Epic.OnlineServices.ProductUserId.FromString(puidToGetIdsFor.Value); + + var queryProductUserIdMappingsOptions = new Epic.OnlineServices.Connect.QueryProductUserIdMappingsOptions + { + LocalUserId = selfPuidInternal, + ProductUserIds = new[] { otherPuidInternal } + }; + + var queryWaiter = new CallbackWaiter(); + connectInterface.QueryProductUserIdMappings(options: ref queryProductUserIdMappingsOptions, clientData: null, completionDelegate: queryWaiter.OnCompletion); + var queryResultOption = await queryWaiter.Task; + if (!queryResultOption.TryUnwrap(out var queryResult)) + { + return failure(EosInterface.IdQueries.GetSelfExternalIdError.Timeout); + } + + if (queryResult.ResultCode != Epic.OnlineServices.Result.Success) + { + return failure(queryResult.ResultCode switch + { + Epic.OnlineServices.Result.NotFound => EosInterface.IdQueries.GetSelfExternalIdError.InvalidUser, + Epic.OnlineServices.Result.InvalidUser => EosInterface.IdQueries.GetSelfExternalIdError.InvalidUser, + var unhandled => unhandled.FailAndLogUnhandledError(EosInterface.IdQueries.GetSelfExternalIdError.UnhandledErrorCondition) + }); + } + + var getProductUserExternalAccountCountOptions = new Epic.OnlineServices.Connect.GetProductUserExternalAccountCountOptions + { + TargetUserId = otherPuidInternal + }; + + uint count = connectInterface.GetProductUserExternalAccountCount(ref getProductUserExternalAccountCountOptions); + var accountIds = new AccountId[count]; + + foreach (int i in Enumerable.Range(0, (int)count)) + { + var copyProductUserExternalAccountByIndexOptions = new Epic.OnlineServices.Connect.CopyProductUserExternalAccountByIndexOptions + { + TargetUserId = otherPuidInternal, + ExternalAccountInfoIndex = (uint)i + }; + + connectInterface.CopyProductUserExternalAccountByIndex( + ref copyProductUserExternalAccountByIndexOptions, + out var externalAccountInfoNullable); + if (!externalAccountInfoNullable.TryGetValue(out var externalAccountInfo)) + { + return failure(EosInterface.IdQueries.GetSelfExternalIdError.InvalidUser); + } + + var accountIdOption = + EosStringToAccountId(externalAccountInfo.AccountId, externalAccountInfo.AccountIdType); + if (!accountIdOption.TryUnwrap(out var accountId)) + { + return failure(EosInterface.IdQueries.GetSelfExternalIdError.ParseError); + } + + accountIds[i] = accountId; + } + + return success(accountIds.ToImmutableArray()); + } + + internal static async Task> GetPuidForExternalId(AccountId externalId) + { + var connectInterface = CorePrivate.ConnectInterface; + if (connectInterface is null) + { + return Result.Failure(Epic.OnlineServices.Result.NotConfigured); + } + + var externalAccountType = externalId is EpicAccountId + ? Epic.OnlineServices.ExternalAccountType.Epic + : Epic.OnlineServices.ExternalAccountType.Steam; + string externalAccountEosRepresentation = externalId.EosStringRepresentation; + + Result lastError + = Result.Failure(Epic.OnlineServices.Result.UnexpectedError); + foreach (var selfPuid in GetLoggedInPuids() + .OrderByDescending(id => LoginPrivate.PuidToPrimaryExternalId[id].GetType() == externalId.GetType())) + { + var selfPuidInternal = Epic.OnlineServices.ProductUserId.FromString(selfPuid.Value); + + // See https://dev.epicgames.com/docs/en-US/api-ref/functions/eos-connect-query-external-account-mappings + // to learn why we need to call this function before we call GetExternalAccountMapping + var queryExternalAccountMappingsOptions = new Epic.OnlineServices.Connect.QueryExternalAccountMappingsOptions + { + LocalUserId = selfPuidInternal, + AccountIdType = externalAccountType, + ExternalAccountIds = new Epic.OnlineServices.Utf8String[] + { + externalAccountEosRepresentation + } + }; + + var queryExternalAccountMappingsWaiter = new CallbackWaiter(); + connectInterface.QueryExternalAccountMappings(options: ref queryExternalAccountMappingsOptions, clientData: null, completionDelegate: queryExternalAccountMappingsWaiter.OnCompletion); + var resultOption = await queryExternalAccountMappingsWaiter.Task; + if (!resultOption.TryUnwrap(out var result)) + { + lastError = Result.Failure(Epic.OnlineServices.Result.TimedOut); + continue; + } + + if (result.ResultCode != Epic.OnlineServices.Result.Success) + { + lastError = Result.Failure(result.ResultCode); + continue; + } + + var getExternalAccountMappingsOptions = new Epic.OnlineServices.Connect.GetExternalAccountMappingsOptions + { + LocalUserId = selfPuidInternal, + AccountIdType = externalAccountType, + TargetExternalUserId = externalAccountEosRepresentation + }; + var otherPuid = connectInterface.GetExternalAccountMapping(ref getExternalAccountMappingsOptions); + if (otherPuid is null) + { + lastError = Result.Failure(Epic.OnlineServices.Result.NotFound); + continue; + } + return Result.Success(new EosInterface.ProductUserId(otherPuid.ToString())); + } + + return lastError; + } + + public static Option EosStringToAccountId( + string stringRepresentation, + Epic.OnlineServices.ExternalAccountType accountType) + => accountType switch + { + Epic.OnlineServices.ExternalAccountType.Steam => SteamId.Parse(stringRepresentation).Select(id => (AccountId)id), + Epic.OnlineServices.ExternalAccountType.Epic => EpicAccountId.Parse(stringRepresentation).Select(id => (AccountId)id), + _ => Option.None + }; +} + +internal sealed partial class ImplementationPrivate : EosInterface.Implementation +{ + public override ImmutableArray GetLoggedInPuids() + => IdQueriesPrivate.GetLoggedInPuids(); + + public override ImmutableArray GetLoggedInEpicIds() + => IdQueriesPrivate.GetLoggedInEpicIds(); + + public override Task, EosInterface.IdQueries.GetSelfExternalIdError>> GetSelfExternalAccountIds(EosInterface.ProductUserId puid) + => TaskScheduler.Schedule(() => IdQueriesPrivate.GetSelfExternalAccountIds(puid)); +} diff --git a/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/IdAndAuth/LoginPrivate.cs b/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/IdAndAuth/LoginPrivate.cs new file mode 100644 index 000000000..689ff998a --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/IdAndAuth/LoginPrivate.cs @@ -0,0 +1,708 @@ +#nullable enable +using System; +using System.Collections.Concurrent; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Barotrauma.Debugging; +using Barotrauma.Networking; +using Barotrauma; + +namespace EosInterfacePrivate; + +static class LoginPrivate +{ + private const string EosLoginSteamIdentity = "BarotraumaEosLogin"; + private static Option steamworksAuthTicket; + + private static Option eosConnectExpirationNotifyId, eosConnectStatusChangedNotifyId; + private static Option egsAuthExpirationNotifyId; + + internal static void Init() + { + if (CorePrivate.ConnectInterface is not { } connectInterface) { return; } + if (CorePrivate.EgsAuthInterface is not { } egsAuthInterface) { return; } + + ClearNotificationId(ref egsAuthExpirationNotifyId, egsAuthInterface.RemoveNotifyLoginStatusChanged); + var authExpirationOptions = new Epic.OnlineServices.Auth.AddNotifyLoginStatusChangedOptions(); + ulong authExpirationNotifyId = egsAuthInterface.AddNotifyLoginStatusChanged(ref authExpirationOptions, null, OnEgsAuthStatusChanged); + StoreNotificationId(out egsAuthExpirationNotifyId, authExpirationNotifyId); + + ClearNotificationId(ref eosConnectExpirationNotifyId, connectInterface.RemoveNotifyAuthExpiration); + var connectExpirationOptions = new Epic.OnlineServices.Connect.AddNotifyAuthExpirationOptions(); + ulong connectExpirationNotifyId = connectInterface.AddNotifyAuthExpiration(ref connectExpirationOptions, null, OnConnectExpiration); + StoreNotificationId(out eosConnectExpirationNotifyId, connectExpirationNotifyId); + + ClearNotificationId(ref eosConnectStatusChangedNotifyId, connectInterface.RemoveNotifyLoginStatusChanged); + var addNotifyConnectStatusChangedOptions = new Epic.OnlineServices.Connect.AddNotifyLoginStatusChangedOptions(); + var connectChangedNotifyId = connectInterface.AddNotifyLoginStatusChanged(ref addNotifyConnectStatusChangedOptions, null, OnConnectStatusChanged); + StoreNotificationId(out eosConnectStatusChangedNotifyId, connectChangedNotifyId); + + static void ClearNotificationId(ref Option field, Action clearAction) + { + if (field.TryUnwrap(out var notificationId)) + { + clearAction(notificationId); + } + field = Option.None; + } + + static void StoreNotificationId(out Option field, ulong value) + { + bool isValid = value is not Epic.OnlineServices.Common.InvalidNotificationid; + field = isValid + ? Option.Some(value) + : Option.None; + } + } + + internal static readonly ConcurrentDictionary PuidToPrimaryExternalId = new(); + + private readonly record struct LoginParams( + Epic.OnlineServices.Connect.Credentials Credentials, + AccountId ExternalAccountId); + + private static async Task> GenCredentialsSteam() + { + if (!Steamworks.SteamClient.IsValid || !Steamworks.SteamClient.IsLoggedOn) { return Result.Failure(EosInterface.Login.LoginError.SteamNotLoggedIn); } + if (steamworksAuthTicket.TryUnwrap(out var oldTicket)) { oldTicket.Cancel(); } + var newTicketNullable = await Steamworks.SteamUser.GetAuthTicketForWebApi(EosLoginSteamIdentity); + if (newTicketNullable is not { Data: not null } ticket) + { + return Result.Failure(EosInterface.Login.LoginError.FailedToGetSteamSessionTicket); + } + return Result.Success( + new LoginParams( + Credentials: new Epic.OnlineServices.Connect.Credentials + { + Token = ToolBoxCore.ByteArrayToHexString(ticket.Data), + Type = Epic.OnlineServices.ExternalCredentialType.SteamSessionTicket + }, + ExternalAccountId: new SteamId(Steamworks.SteamClient.SteamId))); + } + + private static async Task, EosInterface.Login.LoginError>> GenCredentialsEpic( + Epic.OnlineServices.Auth.LoginCredentialType credentialsType, + string? credentialsId, + string? credentialsToken, + Epic.OnlineServices.ExternalCredentialType credentialsExternalType, + EosInterface.Login.LoginEpicFlags flags) + { + if (CorePrivate.EgsAuthInterface is not { } egsAuthInterface) { return Result.Failure(EosInterface.Login.LoginError.EosNotInitialized); } + + if (credentialsType is not ( + Epic.OnlineServices.Auth.LoginCredentialType.ExternalAuth + or Epic.OnlineServices.Auth.LoginCredentialType.Developer + or Epic.OnlineServices.Auth.LoginCredentialType.ExchangeCode)) + { + return Result.Failure(EosInterface.Login.LoginError.InvalidUser); + } + + var authLoginOptions = new Epic.OnlineServices.Auth.LoginOptions + { + Credentials = new Epic.OnlineServices.Auth.Credentials + { + Id = credentialsId, + Token = credentialsToken, + Type = credentialsType, + SystemAuthCredentialsOptions = default, + ExternalType = credentialsExternalType + }, + ScopeFlags = + Epic.OnlineServices.Auth.AuthScopeFlags.BasicProfile + | Epic.OnlineServices.Auth.AuthScopeFlags.Presence + | Epic.OnlineServices.Auth.AuthScopeFlags.FriendsList, + LoginFlags = flags.HasFlag(EosInterface.Login.LoginEpicFlags.FailWithoutOpeningBrowser) + ? Epic.OnlineServices.Auth.LoginFlags.NoUserInterface + : Epic.OnlineServices.Auth.LoginFlags.None + }; + + var authLoginWaiter = new CallbackWaiter(); + egsAuthInterface.Login(options: ref authLoginOptions, clientData: null, completionDelegate: authLoginWaiter.OnCompletion); + + // This can time out if authLoginOptions.ScopeFlags is set incorrectly, + // because the docs lied and this callback isn't guaranteed to be called + var authLoginCallbackInfoOption = await authLoginWaiter.Task; + + if (!authLoginCallbackInfoOption.TryUnwrap(out var authLoginCallbackInfo)) + { + return Result.Failure(EosInterface.Login.LoginError.EgsLoginTimeout); + } + if (authLoginCallbackInfo is { ResultCode: Epic.OnlineServices.Result.InvalidUser, ContinuanceToken: { } continuanceToken }) + { + return Result.Success((Either)continuanceToken); + } + + if (authLoginCallbackInfo.ResultCode != Epic.OnlineServices.Result.Success) + { + return Result.Failure(authLoginCallbackInfo.ResultCode switch { + Epic.OnlineServices.Result.NotFound + => EosInterface.Login.LoginError.EgsAccountNotFound, + Epic.OnlineServices.Result.AuthExchangeCodeNotFound + => EosInterface.Login.LoginError.AuthExchangeCodeNotFound, + Epic.OnlineServices.Result.AuthUserInterfaceRequired + => EosInterface.Login.LoginError.AuthRequiresOpeningBrowser, + Epic.OnlineServices.Result.AccessDenied + => EosInterface.Login.LoginError.EgsAccessDenied, + _ + => EosInterface.Login.LoginError.UnhandledFailureCondition + }); + } + if (!EpicAccountId.Parse(authLoginCallbackInfo.LocalUserId.ToString()).TryUnwrap(out var externalAccountId)) + { + return Result.Failure(EosInterface.Login.LoginError.FailedToParseEgsId); + } + + var copyIdTokenOptions = new Epic.OnlineServices.Auth.CopyIdTokenOptions + { + AccountId = authLoginCallbackInfo.LocalUserId + }; + + var tokenCopyResult = egsAuthInterface.CopyIdToken(ref copyIdTokenOptions, out var tokenNullable); + if (tokenCopyResult != Epic.OnlineServices.Result.Success) + { + Result.Failure(EosInterface.Login.LoginError.FailedToGetEgsIdToken); + } + if (tokenNullable is not { } token) { return Result.Failure(EosInterface.Login.LoginError.FailedToGetEgsIdToken); } + + return Result.Success( + (Either)new LoginParams( + Credentials: new Epic.OnlineServices.Connect.Credentials + { + Token = token.JsonWebToken, + Type = Epic.OnlineServices.ExternalCredentialType.EpicIdToken + }, + ExternalAccountId: externalAccountId)); + } + + public static async Task, EosInterface.Login.LoginError>> LoginSteam() + { + var credentialsSteamResult = await GenCredentialsSteam(); + if (credentialsSteamResult.TryUnwrapFailure(out var error)) + { + return Result.Failure(error); + } + if (!credentialsSteamResult.TryUnwrapSuccess(out var loginParams)) + { + return Result.Failure(EosInterface.Login.LoginError.InvalidUser); + } + + var result = await Login(loginParams); + if (steamworksAuthTicket.TryUnwrap(out var ticket)) { ticket.Cancel(); } + steamworksAuthTicket = Option.None; + return result; + } + + public static async Task, EosInterface.Login.LoginError>> LoginEpicWithLinkedSteamAccount(EosInterface.Login.LoginEpicFlags flags) + { + if (steamworksAuthTicket.TryUnwrap(out var oldTicket)) { oldTicket.Cancel(); } + var newTicketNullable = await Steamworks.SteamUser.GetAuthTicketForWebApi(EosLoginSteamIdentity); + if (newTicketNullable is not { Data: not null } ticket) + { + return Result.Failure(EosInterface.Login.LoginError.FailedToGetSteamSessionTicket); + } + var epicCredentialsOption = await GenCredentialsEpic( + credentialsType: Epic.OnlineServices.Auth.LoginCredentialType.ExternalAuth, + credentialsId: null, + credentialsToken: ToolBoxCore.ByteArrayToHexString(ticket.Data), + credentialsExternalType: Epic.OnlineServices.ExternalCredentialType.SteamSessionTicket, + flags: flags); + if (epicCredentialsOption.TryUnwrapFailure(out var epicCredentialsFail)) + { + return Result.Failure(epicCredentialsFail); + } + if (!epicCredentialsOption.TryUnwrapSuccess(out var loginParamsOrContinuanceToken)) + { + return Result.Failure(EosInterface.Login.LoginError.UnhandledFailureCondition); + } + + if (loginParamsOrContinuanceToken.TryGet(out Epic.OnlineServices.ContinuanceToken continuanceToken)) + { + return Result.Success((OneOf) + new EosInterface.EgsAuthContinuanceToken(continuanceToken.InnerHandle, ExtractExpiryTimeFromContinuanceToken(continuanceToken, EosInterface.EgsAuthContinuanceToken.Duration))); + } + if (!loginParamsOrContinuanceToken.TryGet(out LoginParams loginParams)) + { + return Result.Failure(EosInterface.Login.LoginError.UnexpectedContinuanceToken); + } + + var loginResult = await Login(loginParams); + if (loginResult.TryUnwrapSuccess(out var loginSuccess)) + { + return loginSuccess.TryGet(out EosInterface.EosConnectContinuanceToken eosContinuanceToken) + ? Result.Success((OneOf)eosContinuanceToken) + : loginSuccess.TryGet(out EosInterface.ProductUserId puid) + ? Result.Success((OneOf)puid) + : Result.Failure(EosInterface.Login.LoginError.UnhandledFailureCondition); + } + return loginResult.TryUnwrapFailure(out var loginFailure) + ? Result.Failure(loginFailure) + : Result.Failure(EosInterface.Login.LoginError.UnhandledFailureCondition); + } + + public static async Task, EosInterface.Login.LoginError>> LoginEpicExchangeCode(string exchangeCode) + { + var epicCredentialsOption = await GenCredentialsEpic( + credentialsType: Epic.OnlineServices.Auth.LoginCredentialType.ExchangeCode, + credentialsId: "", + credentialsToken: exchangeCode, + credentialsExternalType: Epic.OnlineServices.ExternalCredentialType.Epic, + flags: EosInterface.Login.LoginEpicFlags.None); + if (epicCredentialsOption.TryUnwrapFailure(out var epicCredentialsFail)) + { + return Result.Failure(epicCredentialsFail); + } + if (!epicCredentialsOption.TryUnwrapSuccess(out var loginParamsOrContinuanceToken)) + { + return Result.Failure(EosInterface.Login.LoginError.UnhandledFailureCondition); + } + if (!loginParamsOrContinuanceToken.TryGet(out LoginParams loginParams)) + { + return Result.Failure(EosInterface.Login.LoginError.UnexpectedContinuanceToken); + } + + var result = await Login(loginParams); + return result; + } + + public static async Task, EosInterface.Login.LoginError>> LoginEpicIdToken(EosInterface.EgsIdToken egsIdToken) + { + if (egsIdToken is not EgsIdTokenPrivate privateEgsIdToken) { return Result.Failure(EosInterface.Login.LoginError.InvalidUser); } + var credentials = new Epic.OnlineServices.Connect.Credentials + { + Token = privateEgsIdToken.InternalToken.JsonWebToken, + Type = Epic.OnlineServices.ExternalCredentialType.EpicIdToken + }; + + return await Login(new LoginParams(credentials, privateEgsIdToken.AccountId)); + } + + private static DateTime ExtractExpiryTimeFromContinuanceToken(Epic.OnlineServices.ContinuanceToken continuanceToken, TimeSpan fallbackDuration) + { + // Not the exact expiry time, but it's a pretty close guess should we fail to decode the continuance token + var expiryTime = DateTime.Now + fallbackDuration; + + // This method exists to replace Epic.OnlineServices.ContinuanceToken.ToString because + // the generated code is broken, and I don't want to modify it because we risk undoing + // a fix when we update the SDK. + static string continuanceTokenToString(Epic.OnlineServices.ContinuanceToken continuanceToken) + { + int inOutBufferLength = 1024; + System.IntPtr outBufferAddress = Epic.OnlineServices.Helper.AddAllocation(inOutBufferLength); + + var funcResult = Epic.OnlineServices.Bindings.EOS_ContinuanceToken_ToString(continuanceToken.InnerHandle, outBufferAddress, ref inOutBufferLength); + if (funcResult == Epic.OnlineServices.Result.LimitExceeded) + { + // Buffer wasn't large enough to copy the string. + // inOutBufferLength was updated by the last call to be the actual length required. + // Generate a new buffer and try again. + Epic.OnlineServices.Helper.Dispose(ref outBufferAddress); + outBufferAddress = Epic.OnlineServices.Helper.AddAllocation(inOutBufferLength); + funcResult = Epic.OnlineServices.Bindings.EOS_ContinuanceToken_ToString(continuanceToken.InnerHandle, outBufferAddress, ref inOutBufferLength); + if (funcResult != Epic.OnlineServices.Result.Success) + { + DebugConsoleCore.Log($"EOS_ContinuanceToken_ToString failed with result {funcResult}"); + } + } + + Epic.OnlineServices.Utf8String outBuffer = "EOS_ContinuanceToken_ToString failed"; + if (funcResult == Epic.OnlineServices.Result.Success) + { + Epic.OnlineServices.Helper.Get(outBufferAddress, out outBuffer); + } + Epic.OnlineServices.Helper.Dispose(ref outBufferAddress); + + return outBuffer; + } + + var ctDecode = JsonWebToken.Parse(continuanceTokenToString(continuanceToken)); + if (ctDecode.TryUnwrap(out var jwt)) + { + string decodedPayload = jwt.PayloadDecoded; + try + { + // Ugly regex hack to get expiry time. The right thing to do would be to parse the payload as JSON, + // but I don't really care because we're extracting one field out of this whole thing. + string expiryTimeUnix = Regex.Match(decodedPayload, @"""exp""\s*:\s*([0-9]+)").Groups[1].Value; + expiryTime = UnixTime.ParseUtc(expiryTimeUnix).Fallback(UnixTime.UtcEpoch).ToLocalTime(); + } + catch + { + // could not extract expiry time, oh well! + } + } + + return expiryTime; + } + + private static async Task, EosInterface.Login.LoginError>> Login(LoginParams loginParams) + { + static Result, EosInterface.Login.LoginError> success(EosInterface.ProductUserId id) + => Result, EosInterface.Login.LoginError>.Success(id); + static Result, EosInterface.Login.LoginError> continuance(EosInterface.EosConnectContinuanceToken token) + => Result, EosInterface.Login.LoginError>.Success(token); + static Result, EosInterface.Login.LoginError> failure(EosInterface.Login.LoginError error) + => Result, EosInterface.Login.LoginError>.Failure(error); + + if (CorePrivate.ConnectInterface is not { } connectInterface) { return failure(EosInterface.Login.LoginError.EosNotInitialized); } + + var loginOptions = new Epic.OnlineServices.Connect.LoginOptions + { + Credentials = loginParams.Credentials, + UserLoginInfo = null + }; + AccountId primaryExternalId = loginParams.ExternalAccountId; + + var loginWaiter = new CallbackWaiter(); + connectInterface.Login(options: ref loginOptions, clientData: null, completionDelegate: loginWaiter.OnCompletion); + var callbackResultOption = await loginWaiter.Task; + if (!callbackResultOption.TryUnwrap(out var callbackResult)) + { + return failure(EosInterface.Login.LoginError.Timeout); + } + + if (callbackResult.ResultCode == Epic.OnlineServices.Result.Success) + { + var retVal = new EosInterface.ProductUserId(callbackResult.LocalUserId.ToString()); + PuidToPrimaryExternalId[retVal] = primaryExternalId; + return success(retVal); + } + + if (callbackResult is { ResultCode: Epic.OnlineServices.Result.InvalidUser, ContinuanceToken: { } continuanceToken }) + { + var expiryTime = ExtractExpiryTimeFromContinuanceToken(continuanceToken, EosInterface.EosConnectContinuanceToken.Duration); + + return continuance(new EosInterface.EosConnectContinuanceToken(callbackResult.ContinuanceToken.InnerHandle, primaryExternalId, expiryTime)); + } + + return callbackResult.ResultCode switch + { + Epic.OnlineServices.Result.InvalidUser + => failure(EosInterface.Login.LoginError.InvalidUser), + Epic.OnlineServices.Result.AccessDenied + => failure(EosInterface.Login.LoginError.EosAccessDenied), + var unhandled + => failure(unhandled.FailAndLogUnhandledError(EosInterface.Login.LoginError.UnhandledFailureCondition)) + }; + } + + private static void OnEgsAuthStatusChanged(ref Epic.OnlineServices.Auth.LoginStatusChangedCallbackInfo info) + { + var eaidOption = EpicAccountId.Parse(info.LocalUserId.ToString()); + if (!eaidOption.TryUnwrap(out var eaid)) { return; } + + if (info.CurrentStatus == Epic.OnlineServices.LoginStatus.NotLoggedIn) + { + TaskPool.Add( + "UnlogPuidLinkedToEaid", + IdQueriesPrivate.GetPuidForExternalId(eaid), + t => + { + if (!t.TryGetResult(out Result? result)) { return; } + if (!result.TryUnwrapSuccess(out var puid)) { return; } + + MarkAsInaccessible(puid); + }); + } + } + + public static void OnConnectExpiration(ref Epic.OnlineServices.Connect.AuthExpirationCallbackInfo info) + { + var puid = new EosInterface.ProductUserId(info.LocalUserId.ToString()); + DebugConsoleCore.Log($"OnAuthExpirationNotification {puid}"); + if (!PuidToPrimaryExternalId.TryGetValue(puid, out var externalId)) { return; } + + switch (externalId) + { + case SteamId: + { + static async Task RelogSteam() + { + var steamCredentialsResult = await GenCredentialsSteam(); + if (!steamCredentialsResult.TryUnwrapSuccess(out var loginParams)) { return; } + await Relog(loginParams); + } + + TaskPool.Add( + "EosReLoginSteam", + RelogSteam(), + TaskPool.IgnoredCallback); + break; + } + case EpicAccountId epicAccountId: + { + if (CopyEpicIdToken(epicAccountId).TryUnwrap(out var token)) + { + var epicLoginCredentials = new Epic.OnlineServices.Connect.Credentials + { + Token = token.JsonWebToken, + Type = Epic.OnlineServices.ExternalCredentialType.EpicIdToken + }; + var reLogParams = new LoginParams(Credentials: epicLoginCredentials, ExternalAccountId: externalId); + TaskPool.Add("OnAuthExpirationNotification", Relog(reLogParams), onCompletion: TaskPool.IgnoredCallback); + } + + break; + } + } + + static async Task Relog(LoginParams loginParams) + { + var loginOptions = new Epic.OnlineServices.Connect.LoginOptions + { + Credentials = loginParams.Credentials, + UserLoginInfo = null + }; + + var connectLoginWaiter = new CallbackWaiter(); + CorePrivate.ConnectInterface?.Login(options: ref loginOptions, clientData: null, completionDelegate: connectLoginWaiter.OnCompletion); + var resultOption = await connectLoginWaiter.Task; + if (resultOption.TryUnwrap(out var result)) + { + string s = $"EOS relog result: {result.ResultCode}"; + if (result.LocalUserId != null) + { + s += " : " + result.LocalUserId; + } + + if (result.ContinuanceToken != null) + { + s += " ; " + result.ContinuanceToken; + } + DebugConsoleCore.Log(s); + } + else + { + DebugConsoleCore.Log("EOS relog timed out"); + } + } + } + + private static void OnConnectStatusChanged(ref Epic.OnlineServices.Connect.LoginStatusChangedCallbackInfo info) + { + var puid = new EosInterface.ProductUserId(info.LocalUserId.ToString()); + DebugConsoleCore.Log($"OnLoginStatusChangedNotification {puid} {info.CurrentStatus}"); + if (info.CurrentStatus == Epic.OnlineServices.LoginStatus.NotLoggedIn) + { + PuidToPrimaryExternalId.TryRemove(puid, out _); + } + } + + public static async Task> LinkExternalAccountToEpicAccount(EosInterface.EgsAuthContinuanceToken continuanceToken) + { + if (CorePrivate.EgsAuthInterface is not { } egsAuthInterface) { return Result.Failure(EosInterface.Login.LinkExternalAccountToEpicAccountError.EosNotInitialized); } + + var linkOptions = new Epic.OnlineServices.Auth.LinkAccountOptions + { + LinkAccountFlags = Epic.OnlineServices.Auth.LinkAccountFlags.NoFlags, + ContinuanceToken = new Epic.OnlineServices.ContinuanceToken(continuanceToken.Spend()), + LocalUserId = null + }; + + var callbackWaiter = new CallbackWaiter(timeout: TimeSpan.FromMinutes(5)); + egsAuthInterface.LinkAccount(options: ref linkOptions, clientData: null, completionDelegate: callbackWaiter.OnCompletion); + var resultOption = await callbackWaiter.Task; + + if (!resultOption.TryUnwrap(out var result)) + { + return Result.Failure(EosInterface.Login.LinkExternalAccountToEpicAccountError.TimedOut); + } + + if (result.ResultCode == Epic.OnlineServices.Result.Success) + { + if (!EpicAccountId.Parse(result.SelectedAccountId.ToString()).TryUnwrap(out var epicAccountId)) + { + return Result.Failure(EosInterface.Login.LinkExternalAccountToEpicAccountError.FailedToParseEgsAccountId); + } + return Result.Success(epicAccountId); + } + return Result.Failure(EosInterface.Login.LinkExternalAccountToEpicAccountError.UnhandledErrorCondition); + } + + public static async Task> LogoutEpicAccount(EpicAccountId egsId) + { + if (CorePrivate.EgsAuthInterface is not { } egsAuthInterface) { return Result.Failure(EosInterface.Login.LogoutEpicAccountError.EosNotInitialized); } + + var logoutOptions = new Epic.OnlineServices.Auth.LogoutOptions + { + LocalUserId = Epic.OnlineServices.EpicAccountId.FromString(egsId.EosStringRepresentation) + }; + + var callbackWaiter = new CallbackWaiter(); + egsAuthInterface.Logout(options: ref logoutOptions, clientData: null, completionDelegate: callbackWaiter.OnCompletion); + var logoutResultOption = await callbackWaiter.Task; + if (!logoutResultOption.TryUnwrap(out var logoutResult)) + { + return Result.Failure(EosInterface.Login.LogoutEpicAccountError.TimedOut); + } + if (logoutResult.ResultCode == Epic.OnlineServices.Result.Success) { return Result.Success(Unit.Value); } + + return Result.Failure(logoutResult.ResultCode switch + { + _ + => EosInterface.Login.LogoutEpicAccountError.UnhandledErrorCondition + }); + } + + public static void MarkAsInaccessible(EosInterface.ProductUserId puid) + { + PuidToPrimaryExternalId.TryRemove(puid, out _); + } + + private static Option CopyEpicIdToken(EpicAccountId epicAccountId) + { + if (CorePrivate.EgsAuthInterface is not { } egsAuthInterface) { return Option.None; } + + var copyIdTokenOptions = new Epic.OnlineServices.Auth.CopyIdTokenOptions + { + AccountId = Epic.OnlineServices.EpicAccountId.FromString(epicAccountId.EosStringRepresentation) + }; + var result = egsAuthInterface.CopyIdToken(ref copyIdTokenOptions, out var tokenNullable); + + if (result is Epic.OnlineServices.Result.Success && tokenNullable is { } token) + { + return Option.Some(token); + } + + return Option.None; + } + + public static async Task> CreateProductAccount(EosInterface.EosConnectContinuanceToken eosContinuanceToken) + { + if (CorePrivate.ConnectInterface is not { } connectInterface) { return Result.Failure(EosInterface.Login.CreateProductAccountError.EosNotInitialized); } + if (eosContinuanceToken is not { IsValid: true }) { return Result.Failure(EosInterface.Login.CreateProductAccountError.InvalidContinuanceToken); } + + var internalContinuanceToken = new Epic.OnlineServices.ContinuanceToken(eosContinuanceToken.Spend()); + var options = new Epic.OnlineServices.Connect.CreateUserOptions + { + ContinuanceToken = internalContinuanceToken + }; + + var createUserWaiter = new CallbackWaiter(); + connectInterface.CreateUser(options: ref options, clientData: null, completionDelegate: createUserWaiter.OnCompletion); + var callbackResultOption = await createUserWaiter.Task; + if (!callbackResultOption.TryUnwrap(out var callbackResult)) + { + return Result.Failure(EosInterface.Login.CreateProductAccountError.Timeout); + } + + if (callbackResult.ResultCode == Epic.OnlineServices.Result.Success) + { + var retVal = new EosInterface.ProductUserId(callbackResult.LocalUserId.ToString()); + PuidToPrimaryExternalId[retVal] = eosContinuanceToken.ExternalAccountId; + return Result.Success(retVal); + } + + return Result.Failure(EosInterface.Login.CreateProductAccountError.UnhandledErrorCondition); + } + + public static async Task> LinkExternalAccount(EosInterface.ProductUserId puid, EosInterface.EosConnectContinuanceToken eosContinuanceToken) + { + if (CorePrivate.ConnectInterface is not { } connectInterface) { return Result.Failure(EosInterface.Login.LinkExternalAccountError.EosNotInitialized); } + if (eosContinuanceToken is not { IsValid: true }) { return Result.Failure(EosInterface.Login.LinkExternalAccountError.InvalidContinuanceToken); } + + var internalContinuanceToken = new Epic.OnlineServices.ContinuanceToken(eosContinuanceToken.Spend()); + var internalPuid = Epic.OnlineServices.ProductUserId.FromString(puid.Value); + var options = new Epic.OnlineServices.Connect.LinkAccountOptions + { + LocalUserId = internalPuid, + ContinuanceToken = internalContinuanceToken + }; + + var linkAccountAwaiter = new CallbackWaiter(); + connectInterface.LinkAccount(options: ref options, clientData: null, completionDelegate: linkAccountAwaiter.OnCompletion); + var callbackResultOption = await linkAccountAwaiter.Task; + if (!callbackResultOption.TryUnwrap(out var callbackResult)) + { + return Result.Failure(EosInterface.Login.LinkExternalAccountError.Timeout); + } + + if (callbackResult.ResultCode == Epic.OnlineServices.Result.Success) + { + return Result.Success(Unit.Value); + } + + return Result.Failure(callbackResult.ResultCode switch + { + Epic.OnlineServices.Result.ConnectLinkAccountFailed + => EosInterface.Login.LinkExternalAccountError.CannotLink, + _ + => EosInterface.Login.LinkExternalAccountError.UnhandledErrorCondition + }); + } + + public static async Task> UnlinkExternalAccount(EosInterface.ProductUserId puid) + { + if (CorePrivate.ConnectInterface is not { } connectInterface) { return Result.Failure(EosInterface.Login.UnlinkExternalAccountError.EosNotInitialized); } + + var internalPuid = Epic.OnlineServices.ProductUserId.FromString(puid.Value); + var options = new Epic.OnlineServices.Connect.UnlinkAccountOptions + { + LocalUserId = internalPuid + }; + + var unlinkAccountAwaiter = new CallbackWaiter(); + connectInterface.UnlinkAccount(options: ref options, clientData: null, completionDelegate: unlinkAccountAwaiter.OnCompletion); + var callbackResultOption = await unlinkAccountAwaiter.Task; + if (!callbackResultOption.TryUnwrap(out var callbackResult)) + { + return Result.Failure(EosInterface.Login.UnlinkExternalAccountError.Timeout); + } + + if (callbackResult.ResultCode == Epic.OnlineServices.Result.Success) + { + PuidToPrimaryExternalId.TryRemove(puid, out _); + return Result.Success(Unit.Value); + } + + return Result.Failure(callbackResult.ResultCode switch + { + Epic.OnlineServices.Result.InvalidUser + => EosInterface.Login.UnlinkExternalAccountError.InvalidUser, + _ + => EosInterface.Login.UnlinkExternalAccountError.UnhandledErrorCondition + }); + } +} + +internal sealed partial class ImplementationPrivate : EosInterface.Implementation +{ + public override Task> CreateProductAccount(EosInterface.EosConnectContinuanceToken eosContinuanceToken) + => TaskScheduler.Schedule(() => LoginPrivate.CreateProductAccount(eosContinuanceToken)); + + public override Task> LinkExternalAccount(EosInterface.ProductUserId puid, EosInterface.EosConnectContinuanceToken eosContinuanceToken) + => TaskScheduler.Schedule(() => LoginPrivate.LinkExternalAccount(puid, eosContinuanceToken)); + + public override Task> UnlinkExternalAccount(EosInterface.ProductUserId puid) + => TaskScheduler.Schedule(() => LoginPrivate.UnlinkExternalAccount(puid)); + + public override Task, EosInterface.Login.LoginError>> LoginEpicWithLinkedSteamAccount(EosInterface.Login.LoginEpicFlags flags) + => TaskScheduler.Schedule(() => LoginPrivate.LoginEpicWithLinkedSteamAccount(flags)); + + public override Task, EosInterface.Login.LoginError>> LoginEpicExchangeCode(string exchangeCode) + => TaskScheduler.Schedule(() => LoginPrivate.LoginEpicExchangeCode(exchangeCode)); + + public override Task, EosInterface.Login.LoginError>> LoginEpicIdToken(EosInterface.EgsIdToken token) + => TaskScheduler.Schedule(() => LoginPrivate.LoginEpicIdToken(token)); + + public override Task, EosInterface.Login.LoginError>> LoginSteam() + => TaskScheduler.Schedule(LoginPrivate.LoginSteam); + + public override Task> LinkExternalAccountToEpicAccount(EosInterface.EgsAuthContinuanceToken continuanceToken) + => TaskScheduler.Schedule(() => LoginPrivate.LinkExternalAccountToEpicAccount(continuanceToken)); + + public override Task> LogoutEpicAccount(EpicAccountId egsId) + => LoginPrivate.LogoutEpicAccount(egsId); + + public override void MarkAsInaccessible(EosInterface.ProductUserId puid) + => LoginPrivate.MarkAsInaccessible(puid); + + public override void TestEosSessionTimeoutRecovery(EosInterface.ProductUserId puid) + { + var info = new Epic.OnlineServices.Connect.AuthExpirationCallbackInfo + { + ClientData = null, + LocalUserId = Epic.OnlineServices.ProductUserId.FromString(puid.Value) + }; + LoginPrivate.OnConnectExpiration(ref info); + } +} diff --git a/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/IdAndAuth/OwnershipPrivate.cs b/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/IdAndAuth/OwnershipPrivate.cs new file mode 100644 index 000000000..9217754e0 --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/IdAndAuth/OwnershipPrivate.cs @@ -0,0 +1,158 @@ +#nullable enable + +using System; +using System.Linq; +using System.Net.Http; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Barotrauma.Networking; +using Barotrauma; + +namespace EosInterfacePrivate; + +static partial class OwnershipPrivate +{ + internal static async Task> GetGameOwnershipToken(EpicAccountId selfEpicAccountId) + { + if (CorePrivate.EcomInterface is not { } ecomInterface) { return Option.None; } + + var epicAccountIdInternal = + Epic.OnlineServices.EpicAccountId.FromString(selfEpicAccountId.EosStringRepresentation); + + var queryOwnershipTokenOptions = new Epic.OnlineServices.Ecom.QueryOwnershipTokenOptions + { + LocalUserId = epicAccountIdInternal, + CatalogItemIds = new Epic.OnlineServices.Utf8String[] + { + AudienceItemId, + //"Completely arbitrary string!" + + // IDEA: + // As of 2023-06-21, QueryOwnershipToken will succeed even if given obviously fake catalog item IDs. + // This could be useful to us! We could use this to add an audience parameter to this method and fix + // the impersonation exploit without requiring our own persistent service. + // We should ask Epic about this before actually trying it, this is certainly a hack and might get patched. + } + }; + var callbackWaiter = new CallbackWaiter(); + ecomInterface.QueryOwnershipToken(options: ref queryOwnershipTokenOptions, clientData: null, completionDelegate: callbackWaiter.OnCompletion); + var queryOwnershipTokenResultOption = await callbackWaiter.Task; + if (!queryOwnershipTokenResultOption.TryUnwrap(out var queryOwnershipTokenResult)) { return Option.None; } + if (queryOwnershipTokenResult.ResultCode != Epic.OnlineServices.Result.Success) { return Option.None; } + + var jwtOption = JsonWebToken.Parse(queryOwnershipTokenResult.OwnershipToken); + return jwtOption.Select(jwt => new EosInterface.Ownership.Token(jwt)); + } + + internal static async Task> VerifyGameOwnershipToken(EosInterface.Ownership.Token token) + { + JsonWebToken jwt = token.Jwt; + + // Decode header + string kidProperty; + string algProperty; + try + { + var jsonDoc = JsonDocument.Parse(jwt.HeaderDecoded); + kidProperty = jsonDoc.RootElement.GetProperty("kid").GetString() ?? ""; + algProperty = jsonDoc.RootElement.GetProperty("alg").GetString() ?? ""; + } + catch + { + // Header JSON decode failed, can't verify token + return Option.None; + } + + // Basic header sanity checks + if (algProperty != "RS512") { return Option.None; } + if (!kidProperty.IsBase64Url()) { return Option.None; } + + // Decode payload + string epicAccountIdStr; + string catalogItemId; + Option expirationOption; + bool owned; + try + { + var jsonDoc = JsonDocument.Parse(jwt.PayloadDecoded); + epicAccountIdStr = jsonDoc.RootElement.GetProperty("sub").GetString() ?? ""; + var entProperty = jsonDoc.RootElement.GetProperty("ent").EnumerateArray().First(); + catalogItemId = entProperty.GetProperty("catalogItemId").GetString() ?? ""; + expirationOption = UnixTime.ParseUtc(jsonDoc.RootElement.GetProperty("exp").GetUInt64().ToString()); + owned = entProperty.GetProperty("owned").GetBoolean(); + } + catch + { + // Payload JSON decode failed, can't verify token + return Option.None; + } + + // Check that the payload is actually what we want + if (catalogItemId != AudienceItemId) { return Option.None; } + if (!owned) { return Option.None; } + if (!expirationOption.TryUnwrap(out var expiration)) { return Option.None; } + if (DateTime.UtcNow >= expiration) { return Option.None; } + + // Get the public key required to verify this token + string modulus; + string exponent; + try + { + string url = + "https://ecommerceintegration-public-service-ecomprod02.ol.epicgames.com/ecommerceintegration/api/public/publickeys/" + + kidProperty; + using var httpClient = new HttpClient(); + var response = await httpClient.SendAsync(new HttpRequestMessage( + HttpMethod.Get, + new Uri(url))); + if (!response.IsSuccessStatusCode) { return Option.None; } + var responseStr = await response.Content.ReadAsStringAsync(); + + var responseJsonDoc = JsonDocument.Parse(responseStr); + if (kidProperty != responseJsonDoc.RootElement.GetProperty("kid").GetString()) { return Option.None; } + + modulus = responseJsonDoc.RootElement.GetProperty("n").GetString() ?? ""; + exponent = responseJsonDoc.RootElement.GetProperty("e").GetString() ?? ""; + } + catch + { + // Failed to query EG Ecom web API, can't verify token + return Option.None; + } + + // Prepare RSA-SHA512 and verify token + var modulusBytesOption = Base64Url.DecodeBytes(modulus); + if (!modulusBytesOption.TryUnwrap(out var modulusBytes)) { return Option.None; } + var exponentBytesOption = Base64Url.DecodeBytes(exponent); + if (!exponentBytesOption.TryUnwrap(out var exponentBytes)) { return Option.None; } + + var signatureBytesOption = Base64Url.DecodeBytes(jwt.Signature); + if (!signatureBytesOption.TryUnwrap(out var signatureBytes)) { return Option.None; } + + using var rsa = RSA.Create(); + using var sha = SHA512.Create(); + rsa.ImportParameters(new RSAParameters + { + Exponent = exponentBytes.ToArray(), + Modulus = modulusBytes.ToArray(), + }); + byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(jwt.Header + "." + jwt.Payload)); + var deformatter = new RSAPKCS1SignatureDeformatter(rsa); + deformatter.SetHashAlgorithm("SHA512"); + bool verified = deformatter.VerifySignature(hash, signatureBytes.ToArray()); + if (!verified) { return Option.None; } + + return EpicAccountId.Parse(epicAccountIdStr); + } +} + +internal sealed partial class ImplementationPrivate : EosInterface.Implementation +{ + public override Task> GetGameOwnershipToken(EpicAccountId selfEpicAccountId) + => TaskScheduler.Schedule(() => OwnershipPrivate.GetGameOwnershipToken(selfEpicAccountId)); + + public override Task> VerifyGameOwnershipToken(EosInterface.Ownership.Token token) + => TaskScheduler.Schedule(() => OwnershipPrivate.VerifyGameOwnershipToken(token)); +} diff --git a/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/P2P/P2PSocketPrivate.cs b/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/P2P/P2PSocketPrivate.cs new file mode 100644 index 000000000..4ba5c1322 --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/P2P/P2PSocketPrivate.cs @@ -0,0 +1,252 @@ +#nullable enable +using Barotrauma.Networking; +using System; +using System.Collections.Generic; +using Barotrauma; + +namespace EosInterfacePrivate; + +public sealed class P2PSocketPrivate : EosInterface.P2PSocket +{ + private readonly record struct CallbackIds( + ulong OnConnectionRequested, + ulong OnConnectionClosed); + private CallbackIds callbackIds; + + private readonly Epic.OnlineServices.P2P.SocketId socketIdInternal; + private readonly Epic.OnlineServices.ProductUserId selfPuid; + private P2PSocketPrivate(Epic.OnlineServices.P2P.SocketId socketIdInternal, Epic.OnlineServices.ProductUserId selfPuid) + { + this.socketIdInternal = socketIdInternal; + this.selfPuid = selfPuid; + } + + internal static Result CreatePrivate(EosInterface.ProductUserId selfPuid, EosInterface.SocketId socketId) + { + var p2pInterface = CorePrivate.P2PInterface; + if (p2pInterface is null) { return Result.Failure(CreationError.EosNotInitialized); } + + var socketIdInternal = new Epic.OnlineServices.P2P.SocketId { SocketName = socketId.SocketName }; + var selfPuidInternal = Epic.OnlineServices.ProductUserId.FromString(selfPuid.Value); + + using var janitor = Janitor.Start(); + + var socket = new P2PSocketPrivate(socketIdInternal, selfPuidInternal); + + var addNotifyPeerConnectionRequestOptions = new Epic.OnlineServices.P2P.AddNotifyPeerConnectionRequestOptions + { + LocalUserId = selfPuidInternal, + SocketId = socketIdInternal + }; + + var onConnectionRequestCallbackId = p2pInterface.AddNotifyPeerConnectionRequest( + ref addNotifyPeerConnectionRequestOptions, + socket, + ConnectionRequestHandler); + + if (onConnectionRequestCallbackId == Epic.OnlineServices.Common.InvalidNotificationid) + { + return Result.Failure(CreationError.RequestBindFailed); + } + + janitor.AddAction(() => p2pInterface.RemoveNotifyPeerConnectionRequest(onConnectionRequestCallbackId)); + + var addNotifyPeerConnectionClosedOptions = new Epic.OnlineServices.P2P.AddNotifyPeerConnectionClosedOptions + { + LocalUserId = selfPuidInternal, + SocketId = socketIdInternal + }; + + var onConnectionClosedCallbackId = p2pInterface.AddNotifyPeerConnectionClosed( + ref addNotifyPeerConnectionClosedOptions, + socket, + ConnectionClosedHandler); + + if (onConnectionClosedCallbackId == Epic.OnlineServices.Common.InvalidNotificationid) + { + return Result.Failure(CreationError.CloseBindFailed); + } + + janitor.AddAction(() => p2pInterface.RemoveNotifyPeerConnectionClosed(onConnectionClosedCallbackId)); + + socket.callbackIds = new CallbackIds( + OnConnectionRequested: onConnectionRequestCallbackId, + OnConnectionClosed: onConnectionClosedCallbackId); + + janitor.Dismiss(); + + return Result.Success(socket); + } + + private static void ConnectionRequestHandler(ref Epic.OnlineServices.P2P.OnIncomingConnectionRequestInfo info) + { + if (info.ClientData is P2PSocketPrivate p2pSocket + && string.Equals(info.SocketId?.SocketName, p2pSocket.socketIdInternal.SocketName)) + { + p2pSocket.HandleIncomingConnection.Invoke(new IncomingConnectionRequest( + Socket: p2pSocket, + RemoteUserId: new EosInterface.ProductUserId(info.RemoteUserId.ToString()))); + } + } + + private static void ConnectionClosedHandler(ref Epic.OnlineServices.P2P.OnRemoteConnectionClosedInfo info) + { + if (info.ClientData is P2PSocketPrivate p2pSocket + && string.Equals(info.SocketId?.SocketName, p2pSocket.socketIdInternal.SocketName)) + { + p2pSocket.HandleClosedConnection.Invoke(new RemoteConnectionClosed( + RemoteUserId: new EosInterface.ProductUserId(info.RemoteUserId.ToString()), + Reason: info.Reason switch + { + Epic.OnlineServices.P2P.ConnectionClosedReason.Unknown + => RemoteConnectionClosed.ConnectionClosedReason.Unknown, + Epic.OnlineServices.P2P.ConnectionClosedReason.ClosedByLocalUser + => RemoteConnectionClosed.ConnectionClosedReason.ClosedByLocalUser, + Epic.OnlineServices.P2P.ConnectionClosedReason.ClosedByPeer + => RemoteConnectionClosed.ConnectionClosedReason.ClosedByPeer, + Epic.OnlineServices.P2P.ConnectionClosedReason.TimedOut + => RemoteConnectionClosed.ConnectionClosedReason.TimedOut, + Epic.OnlineServices.P2P.ConnectionClosedReason.TooManyConnections + => RemoteConnectionClosed.ConnectionClosedReason.TooManyConnections, + Epic.OnlineServices.P2P.ConnectionClosedReason.InvalidMessage + => RemoteConnectionClosed.ConnectionClosedReason.InvalidMessage, + Epic.OnlineServices.P2P.ConnectionClosedReason.InvalidData + => RemoteConnectionClosed.ConnectionClosedReason.InvalidData, + Epic.OnlineServices.P2P.ConnectionClosedReason.ConnectionFailed + => RemoteConnectionClosed.ConnectionClosedReason.ConnectionFailed, + Epic.OnlineServices.P2P.ConnectionClosedReason.ConnectionClosed + => RemoteConnectionClosed.ConnectionClosedReason.ConnectionClosed, + Epic.OnlineServices.P2P.ConnectionClosedReason.NegotiationFailed + => RemoteConnectionClosed.ConnectionClosedReason.NegotiationFailed, + Epic.OnlineServices.P2P.ConnectionClosedReason.UnexpectedError + => RemoteConnectionClosed.ConnectionClosedReason.UnexpectedError, + _ + => RemoteConnectionClosed.ConnectionClosedReason.Unhandled + })); + } + } + + public override void AcceptConnectionRequest(IncomingConnectionRequest request) + { + var remoteUserIdInternal = Epic.OnlineServices.ProductUserId.FromString(request.RemoteUserId.Value); + + var acceptConnectionOptions = new Epic.OnlineServices.P2P.AcceptConnectionOptions + { + LocalUserId = selfPuid, + RemoteUserId = remoteUserIdInternal, + SocketId = socketIdInternal + }; + CorePrivate.P2PInterface?.AcceptConnection(ref acceptConnectionOptions); + } + + public override void CloseConnection(EosInterface.ProductUserId remoteUserId) + { + var remoteUserIdInternal = Epic.OnlineServices.ProductUserId.FromString(remoteUserId.Value); + + var closeConnectionOptions = new Epic.OnlineServices.P2P.CloseConnectionOptions + { + LocalUserId = selfPuid, + RemoteUserId = remoteUserIdInternal, + SocketId = socketIdInternal + }; + CorePrivate.P2PInterface?.CloseConnection(ref closeConnectionOptions); + } + + public override IEnumerable GetMessageBatch() + { + var p2pInterface = CorePrivate.P2PInterface; + if (p2pInterface is null) { yield break; } + + var packetQueueOptions = new Epic.OnlineServices.P2P.GetPacketQueueInfoOptions(); + p2pInterface.GetPacketQueueInfo(ref packetQueueOptions, out var packetQueueInfo); + + byte[] buf = new byte[Epic.OnlineServices.P2P.P2PInterface.MaxPacketSize]; + + for (ulong i = 0; i < packetQueueInfo.IncomingPacketQueueCurrentPacketCount; i++) + { + var receivePacketOptions = new Epic.OnlineServices.P2P.ReceivePacketOptions + { + LocalUserId = selfPuid, + MaxDataSizeBytes = (uint)buf.Length, + RequestedChannel = null + }; + + var result = p2pInterface.ReceivePacket( + ref receivePacketOptions, + out var senderId, + out var senderSocketId, + out _, + buf, + out uint bytesWritten); + + if (result != Epic.OnlineServices.Result.Success) { continue; } + if (senderSocketId.SocketName != socketIdInternal.SocketName) { continue; } + + yield return new IncomingMessage( + buf, (int)bytesWritten, new EosInterface.ProductUserId(senderId.ToString())); + } + } + + public override Result SendMessage(OutgoingMessage msg) + { + var p2pInterface = CorePrivate.P2PInterface; + if (p2pInterface is null) { return Result.Failure(SendError.EosNotInitialized); } + + var reliability = msg.DeliveryMethod switch + { + DeliveryMethod.Reliable + => Epic.OnlineServices.P2P.PacketReliability.ReliableOrdered, + _ + => Epic.OnlineServices.P2P.PacketReliability.UnreliableUnordered + }; + + var sendPacketOptions = new Epic.OnlineServices.P2P.SendPacketOptions + { + LocalUserId = selfPuid, + RemoteUserId = Epic.OnlineServices.ProductUserId.FromString(msg.Destination.Value), + SocketId = socketIdInternal, + Channel = 0, + Data = new ArraySegment(array: msg.Buffer, offset: 0, count: msg.ByteLength), + AllowDelayedDelivery = true, + Reliability = reliability, + DisableAutoAcceptConnection = false + }; + var result = p2pInterface.SendPacket(ref sendPacketOptions); + + return result switch + { + Epic.OnlineServices.Result.Success + => Result.Success(Unit.Value), + Epic.OnlineServices.Result.InvalidParameters + => Result.Failure(SendError.InvalidParameters), + Epic.OnlineServices.Result.LimitExceeded + => Result.Failure(SendError.LimitExceeded), + Epic.OnlineServices.Result.NoConnection + => Result.Failure(SendError.NoConnection), + _ + => Result.Failure(SendError.UnhandledErrorCondition) + }; + } + + public override void Dispose() + { + var p2pInterface = CorePrivate.P2PInterface; + if (p2pInterface is null) { return; } + + var closeConnectionsOptions = new Epic.OnlineServices.P2P.CloseConnectionsOptions + { + LocalUserId = selfPuid, + SocketId = socketIdInternal + }; + p2pInterface.RemoveNotifyPeerConnectionRequest(callbackIds.OnConnectionRequested); + p2pInterface.RemoveNotifyPeerConnectionClosed(callbackIds.OnConnectionClosed); + p2pInterface.CloseConnections(ref closeConnectionsOptions); + callbackIds = default; + } +} + +internal sealed partial class ImplementationPrivate : EosInterface.Implementation +{ + public override Result CreateP2PSocket(EosInterface.ProductUserId puid, EosInterface.SocketId socketId) + => P2PSocketPrivate.CreatePrivate(puid, socketId); +} diff --git a/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/Sessions/OwnedSessionsPrivate.cs b/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/Sessions/OwnedSessionsPrivate.cs new file mode 100644 index 000000000..fbb20cbce --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/Sessions/OwnedSessionsPrivate.cs @@ -0,0 +1,319 @@ +#nullable enable +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Barotrauma; + +namespace EosInterfacePrivate; + +static class OwnedSessionsPrivate +{ + private static readonly Random rng = new Random(); + private static readonly ConcurrentDictionary liveOwnedSessions = new ConcurrentDictionary(); + + private static Epic.OnlineServices.Utf8String IdentifierToAttributeKey(Identifier id) + { + // Attribute keys are always uppercase in the EOS developer page, + // so to minimize surprises let's match that here + return id.Value.ToUpperInvariant(); + } + + public static async Task> Create(Option selfUserIdOption, Identifier internalId, int maxPlayers) + { + var (success, failure) = Result.GetFactoryMethods(); + + if (CorePrivate.SessionsInterface is not { } sessionsInterface) { return failure(EosInterface.Sessions.CreateError.EosNotInitialized); } + + if (liveOwnedSessions.ContainsKey(internalId)) { return failure(EosInterface.Sessions.CreateError.SessionAlreadyExists); } + + using var janitor = Janitor.Start(); + + var bucketIndex = rng.Next(EosInterface.Sessions.MinBucketIndex, EosInterface.Sessions.MaxBucketIndex + 1); + string bucketName = EosInterface.Sessions.DefaultBucketName + bucketIndex; + var createSessionModificationOptions = new Epic.OnlineServices.Sessions.CreateSessionModificationOptions + { + SessionName = internalId.Value.ToUpperInvariant(), + BucketId = bucketName, + MaxPlayers = (uint)maxPlayers, + LocalUserId = selfUserIdOption.TryUnwrap(out var selfUserId) + ? Epic.OnlineServices.ProductUserId.FromString(selfUserId.Value) + : null, + PresenceEnabled = false, + SessionId = null, + SanctionsEnabled = false + }; + var sessionCreateResult = sessionsInterface.CreateSessionModification(ref createSessionModificationOptions, out var sessionModificationHandle); + if (sessionCreateResult != Epic.OnlineServices.Result.Success) + { + return failure(sessionCreateResult switch + { + Epic.OnlineServices.Result.InvalidUser => EosInterface.Sessions.CreateError.InvalidUser, + Epic.OnlineServices.Result.SessionsSessionAlreadyExists => EosInterface.Sessions.CreateError.SessionAlreadyExists, + _ => EosInterface.Sessions.CreateError.UnhandledErrorCondition + }); + } + janitor.AddAction(sessionModificationHandle.Release); + + var updateSessionOptions = new Epic.OnlineServices.Sessions.UpdateSessionOptions + { + SessionModificationHandle = sessionModificationHandle + }; + + var updateSessionWaiter = new CallbackWaiter(); + sessionsInterface.UpdateSession(options: ref updateSessionOptions, clientData: null, completionDelegate: updateSessionWaiter.OnCompletion); + var updateSessionResultOption = await updateSessionWaiter.Task; + + if (!updateSessionResultOption.TryUnwrap(out var updateSessionResult)) { return failure(EosInterface.Sessions.CreateError.TimedOut); } + + if (updateSessionResult.ResultCode == Epic.OnlineServices.Result.Success) + { + var newSession = new EosInterface.Sessions.OwnedSession( + BucketId: bucketName, + InternalId: updateSessionResult.SessionName.ToIdentifier(), + GlobalId: updateSessionResult.SessionId.ToIdentifier(), + Attributes: new Dictionary()); + liveOwnedSessions.TryAdd(internalId, newSession); + return success(newSession); + } + return failure(updateSessionResult.ResultCode.FailAndLogUnhandledError(EosInterface.Sessions.CreateError.UnhandledErrorCondition)); + } + + public static async Task> UpdateOwnedSessionAttributes(EosInterface.Sessions.OwnedSession session) + { + if (CorePrivate.SessionsInterface is not { } sessionsInterface) { return Result.Failure(EosInterface.Sessions.AttributeUpdateError.EosNotInitialized); } + + using var janitor = Janitor.Start(); + + var updateSessionModificationOptions = new Epic.OnlineServices.Sessions.UpdateSessionModificationOptions + { + SessionName = session.InternalId.Value.ToUpperInvariant() + }; + var sessionCreateResult = sessionsInterface.UpdateSessionModification(ref updateSessionModificationOptions, out var sessionModificationHandle); + if (sessionCreateResult != Epic.OnlineServices.Result.Success) + { + return Result.Failure(EosInterface.Sessions.AttributeUpdateError.FailedToCreateSessionModificationHandle); + } + janitor.AddAction(() => sessionModificationHandle.Release()); + + var keysToRemove = session.SyncedAttributes + .Except(session.Attributes) + .Select(kvp => kvp.Key) + .ToArray(); + + var attributesToAdd = session.Attributes + .Except(session.SyncedAttributes) + .ToArray(); + + var setBucketIdOptions = new Epic.OnlineServices.Sessions.SessionModificationSetBucketIdOptions + { + BucketId = session.BucketId + }; + sessionModificationHandle.SetBucketId(ref setBucketIdOptions); + + if (session.HostAddress.TryUnwrap(out var hostAddress)) + { + var setHostAddressOptions = new Epic.OnlineServices.Sessions.SessionModificationSetHostAddressOptions + { + HostAddress = hostAddress + }; + sessionModificationHandle.SetHostAddress(ref setHostAddressOptions); + } + + foreach (Identifier key in keysToRemove) + { + var removeAttributeOptions = new Epic.OnlineServices.Sessions.SessionModificationRemoveAttributeOptions + { + Key = IdentifierToAttributeKey(key) + }; + var removeResult = sessionModificationHandle.RemoveAttribute(ref removeAttributeOptions); + if (removeResult != Epic.OnlineServices.Result.Success) + { + return Result.Failure( + removeResult switch + { + Epic.OnlineServices.Result.InvalidParameters + => EosInterface.Sessions.AttributeUpdateError.InvalidParametersForRemoveAttribute, + Epic.OnlineServices.Result.IncompatibleVersion + => EosInterface.Sessions.AttributeUpdateError.IncompatibleVersionForRemoveAttribute, + _ + => EosInterface.Sessions.AttributeUpdateError.UnhandledErrorConditionForRemoveAttribute + }); + } + } + + foreach (var kvp in attributesToAdd) + { + // EOS doesn't like empty values so let's skip those + if (kvp.Value.IsNullOrEmpty()) { continue; } + + var addAttributeOptions = new Epic.OnlineServices.Sessions.SessionModificationAddAttributeOptions + { + SessionAttribute = new Epic.OnlineServices.Sessions.AttributeData + { + Key = IdentifierToAttributeKey(kvp.Key), + Value = kvp.Value + }, + AdvertisementType = Epic.OnlineServices.Sessions.SessionAttributeAdvertisementType.Advertise + }; + var addResult = sessionModificationHandle.AddAttribute(ref addAttributeOptions); + if (addResult != Epic.OnlineServices.Result.Success) + { + return Result.Failure( + addResult switch + { + Epic.OnlineServices.Result.InvalidParameters + => EosInterface.Sessions.AttributeUpdateError.InvalidParametersForAddAttribute, + Epic.OnlineServices.Result.IncompatibleVersion + => EosInterface.Sessions.AttributeUpdateError.IncompatibleVersionForAddAttribute, + _ + => EosInterface.Sessions.AttributeUpdateError.UnhandledErrorConditionForAddAttribute + }); + } + } + + var updateSessionOptions = new Epic.OnlineServices.Sessions.UpdateSessionOptions + { + SessionModificationHandle = sessionModificationHandle + }; + + var updateSessionWaiter = new CallbackWaiter(); + sessionsInterface.UpdateSession(options: ref updateSessionOptions, clientData: null, completionDelegate: updateSessionWaiter.OnCompletion); + var updateSessionResultOption = await updateSessionWaiter.Task; + + if (!updateSessionResultOption.TryUnwrap(out var updateSessionResult)) { Result.Failure(EosInterface.Sessions.AttributeUpdateError.TimedOut); } + + if (updateSessionResult.ResultCode != Epic.OnlineServices.Result.Success) + { + return updateSessionResult.ResultCode switch + { + Epic.OnlineServices.Result.InvalidParameters + => Result.Failure(EosInterface.Sessions.AttributeUpdateError.InvalidParametersForSessionUpdate), + Epic.OnlineServices.Result.SessionsOutOfSync + => Result.Failure(EosInterface.Sessions.AttributeUpdateError.SessionsOutOfSync), + Epic.OnlineServices.Result.NotFound + => Result.Failure(EosInterface.Sessions.AttributeUpdateError.SessionNotFound), + Epic.OnlineServices.Result.NoConnection + => Result.Failure(EosInterface.Sessions.AttributeUpdateError.NoConnection), + var unhandled + => Result.Failure(unhandled.FailAndLogUnhandledError(EosInterface.Sessions.AttributeUpdateError.UnhandledErrorCondition)) + }; + } + + session.SyncedAttributes = session.Attributes.ToImmutableDictionary(); + return Result.Success(Unit.Value); + } + + public static async Task> CloseOwnedSession(EosInterface.Sessions.OwnedSession session) + { + if (CorePrivate.SessionsInterface is not { } sessionsInterface) { return Result.Failure(EosInterface.Sessions.CloseError.EosNotInitialized); } + + liveOwnedSessions.TryRemove(session.InternalId, out _); + + var options = new Epic.OnlineServices.Sessions.DestroySessionOptions + { + SessionName = session.InternalId.Value.ToUpperInvariant() + }; + + var callbackWaiter = new CallbackWaiter(); + sessionsInterface.DestroySession(options: ref options, clientData: null, completionDelegate: callbackWaiter.OnCompletion); + var resultOption = await callbackWaiter.Task; + + if (!resultOption.TryUnwrap(out var result)) { return Result.Failure(EosInterface.Sessions.CloseError.TimedOut); } + + return result.ResultCode switch + { + Epic.OnlineServices.Result.Success + => Result.Success(Unit.Value), + Epic.OnlineServices.Result.InvalidParameters + => Result.Failure(EosInterface.Sessions.CloseError.InvalidParameters), + Epic.OnlineServices.Result.AlreadyPending + => Result.Failure(EosInterface.Sessions.CloseError.AlreadyPending), + Epic.OnlineServices.Result.NotFound + => Result.Failure(EosInterface.Sessions.CloseError.NotFound), + var unhandled + => Result.Failure(unhandled.FailAndLogUnhandledError(EosInterface.Sessions.CloseError.UnhandledErrorCondition)) + }; + } + + public static Task CloseAllOwnedSessions() + { + return Task.WhenAll(liveOwnedSessions.Values + .ToArray() + .Select(CloseOwnedSession)); + } + + public static Task ForceUpdateAllOwnedSessions() + { + var sessionsToUpdate = liveOwnedSessions.Values.ToArray(); + foreach (var session in sessionsToUpdate) + { + session.SyncedAttributes = ImmutableDictionary.Empty; + } + return Task.WhenAll(sessionsToUpdate + .Select(UpdateOwnedSessionAttributes)); + } + + public static async Task, EosInterface.Sessions.RegisterError>> RegisterPlayers(EosInterface.Sessions.OwnedSession session, params EosInterface.ProductUserId[] puids) + { + if (CorePrivate.SessionsInterface is not { } sessionsInterface) { return Result.Failure(EosInterface.Sessions.RegisterError.EosNotInitialized); } + + var registerPlayersOptions = new Epic.OnlineServices.Sessions.RegisterPlayersOptions + { + SessionName = session.InternalId.Value.ToUpperInvariant(), + PlayersToRegister = puids.Select(puid => Epic.OnlineServices.ProductUserId.FromString(puid.Value)).ToArray() + }; + var registerPlayersWaiter = new CallbackWaiter(); + sessionsInterface.RegisterPlayers(options: ref registerPlayersOptions, clientData: null, completionDelegate: registerPlayersWaiter.OnCompletion); + var registerResultOption = await registerPlayersWaiter.Task; + + if (!registerResultOption.TryUnwrap(out var registerResult)) { return Result.Failure(EosInterface.Sessions.RegisterError.TimedOut); } + + if (registerResult.ResultCode != Epic.OnlineServices.Result.Success) + { + return Result.Failure(registerResult.ResultCode.FailAndLogUnhandledError(EosInterface.Sessions.RegisterError.UnhandledErrorCondition)); + } + + return Result.Success(registerResult.RegisteredPlayers.Select(puid => new EosInterface.ProductUserId(puid.ToString())).ToImmutableArray()); + } + + public static async Task, EosInterface.Sessions.UnregisterError>> UnregisterPlayers(EosInterface.Sessions.OwnedSession session, params EosInterface.ProductUserId[] puids) + { + if (CorePrivate.SessionsInterface is not { } sessionsInterface) { return Result.Failure(EosInterface.Sessions.UnregisterError.EosNotInitialized); } + + var unregisterPlayersOptions = new Epic.OnlineServices.Sessions.UnregisterPlayersOptions + { + SessionName = session.InternalId.Value.ToUpperInvariant(), + PlayersToUnregister = puids.Select(puid => Epic.OnlineServices.ProductUserId.FromString(puid.Value)).ToArray() + }; + var unregisterPlayersWaiter = new CallbackWaiter(); + sessionsInterface.UnregisterPlayers(options: ref unregisterPlayersOptions, clientData: null, completionDelegate: unregisterPlayersWaiter.OnCompletion); + var unregisterResultOption = await unregisterPlayersWaiter.Task; + + if (!unregisterResultOption.TryUnwrap(out var unregisterResult)) { return Result.Failure(EosInterface.Sessions.UnregisterError.TimedOut); } + + if (unregisterResult.ResultCode != Epic.OnlineServices.Result.Success) + { + return Result.Failure(unregisterResult.ResultCode.FailAndLogUnhandledError(EosInterface.Sessions.UnregisterError.UnhandledErrorCondition)); + } + + return Result.Success(unregisterResult.UnregisteredPlayers.Select(puid => new EosInterface.ProductUserId(puid.ToString())).ToImmutableArray()); + } +} + +internal sealed partial class ImplementationPrivate : EosInterface.Implementation +{ + public override Task> CreateSession(Option selfUserIdOption, Identifier internalId, int maxPlayers) + => TaskScheduler.Schedule(() => OwnedSessionsPrivate.Create(selfUserIdOption, internalId, maxPlayers)); + + public override Task> UpdateOwnedSessionAttributes(EosInterface.Sessions.OwnedSession session) + => TaskScheduler.Schedule(() => OwnedSessionsPrivate.UpdateOwnedSessionAttributes(session)); + + public override Task> CloseOwnedSession(EosInterface.Sessions.OwnedSession session) + => TaskScheduler.Schedule(() => OwnedSessionsPrivate.CloseOwnedSession(session)); + + public override Task CloseAllOwnedSessions() + => TaskScheduler.Schedule(OwnedSessionsPrivate.CloseAllOwnedSessions); +} diff --git a/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/Sessions/RemoteSessionsPrivate.cs b/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/Sessions/RemoteSessionsPrivate.cs new file mode 100644 index 000000000..307d75b7e --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/Sessions/RemoteSessionsPrivate.cs @@ -0,0 +1,159 @@ +#nullable enable +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Barotrauma; + +namespace EosInterfacePrivate; + +static class RemoteSessionsPrivate +{ + /// + /// Largest number that can be passed to CreateSessionSearchOptions.MaxSearchResults + /// before it will immediately result in an InvalidParameters error. + /// + private const uint MaxResultsUpperBound = Epic.OnlineServices.Sessions.SessionsInterface.MaxSearchResults; + + public static async Task, EosInterface.Sessions.RemoteSession.Query.Error>> RunQuery(EosInterface.Sessions.RemoteSession.Query query) + { + if (CorePrivate.SessionsInterface is not { } sessionsInterface) + { + return Result.Failure(EosInterface.Sessions.RemoteSession.Query.Error.EosNotInitialized); + } + + using var janitor = Janitor.Start(); + + var createSessionSearchOptions = new Epic.OnlineServices.Sessions.CreateSessionSearchOptions + { + MaxSearchResults = query.MaxResults + }; + var createSessionSearchResult = sessionsInterface.CreateSessionSearch(ref createSessionSearchOptions, out var sessionSearchHandle); + if (createSessionSearchResult != Epic.OnlineServices.Result.Success) + { + return Result.Failure( + createSessionSearchResult switch + { + Epic.OnlineServices.Result.InvalidParameters when query.MaxResults > MaxResultsUpperBound + => EosInterface.Sessions.RemoteSession.Query.Error.ExceededMaxAllowedResults, + Epic.OnlineServices.Result.InvalidParameters + => EosInterface.Sessions.RemoteSession.Query.Error.InvalidParameters, + _ + => createSessionSearchResult.FailAndLogUnhandledError(EosInterface.Sessions.RemoteSession.Query.Error.UnhandledErrorCondition) + }); + } + janitor.AddAction(sessionSearchHandle.Release); + + var setParameterOptions = new Epic.OnlineServices.Sessions.SessionSearchSetParameterOptions + { + Parameter = new Epic.OnlineServices.Sessions.AttributeData + { + Key = Epic.OnlineServices.Sessions.SessionsInterface.SearchBucketId, + Value = new Epic.OnlineServices.Sessions.AttributeDataValue + { + AsUtf8 = EosInterface.Sessions.DefaultBucketName + query.BucketIndex + } + }, + ComparisonOp = Epic.OnlineServices.ComparisonOp.Equal + }; + sessionSearchHandle.SetParameter(ref setParameterOptions); + + var findOptions = new Epic.OnlineServices.Sessions.SessionSearchFindOptions + { + LocalUserId = Epic.OnlineServices.ProductUserId.FromString(query.LocalUserId.Value) + }; + + var findCallbackWaiter = new CallbackWaiter(); + sessionSearchHandle.Find(options: ref findOptions, clientData: null, completionDelegate: findCallbackWaiter.OnCompletion); + var findResultOption = await findCallbackWaiter.Task; + if (!findResultOption.TryUnwrap(out var findResult)) + { + return Result.Failure(EosInterface.Sessions.RemoteSession.Query.Error.TimedOut); + } + if (findResult.ResultCode != Epic.OnlineServices.Result.Success) + { + return Result.Failure( + findResult.ResultCode switch + { + Epic.OnlineServices.Result.NotFound + => EosInterface.Sessions.RemoteSession.Query.Error.NotFound, + Epic.OnlineServices.Result.InvalidParameters + => EosInterface.Sessions.RemoteSession.Query.Error.InvalidParameters, + _ + => EosInterface.Sessions.RemoteSession.Query.Error.EosNotInitialized + }); + } + + var boilerplate1 = new Epic.OnlineServices.Sessions.SessionSearchGetSearchResultCountOptions(); + uint resultCount = sessionSearchHandle.GetSearchResultCount(ref boilerplate1); + + var sessions = new List(); + foreach (int sessionIndex in Enumerable.Range(0, (int)resultCount)) + { + var attributes = new Dictionary(); + + var copySessionDetailsOptions = new Epic.OnlineServices.Sessions.SessionSearchCopySearchResultByIndexOptions + { + SessionIndex = (uint)sessionIndex + }; + var detailsCopyResult = sessionSearchHandle.CopySearchResultByIndex(ref copySessionDetailsOptions, out var sessionDetails); + if (detailsCopyResult != Epic.OnlineServices.Result.Success) { break; } + janitor.AddAction(sessionDetails.Release); + + var copyInfoOptions = new Epic.OnlineServices.Sessions.SessionDetailsCopyInfoOptions(); + var infoCopyResult = sessionDetails.CopyInfo(ref copyInfoOptions, out var sessionInfo); + if (infoCopyResult != Epic.OnlineServices.Result.Success) { break; } + + if (sessionInfo is not + { + Settings: + { + BucketId: { } bucketId, + NumPublicConnections: var numPublicConnections + }, + NumOpenPublicConnections: var numOpenPublicConnections, + SessionId: { } sessionId, + HostAddress: { } hostAddress + }) + { + break; + } + + var boilerplate2 = new Epic.OnlineServices.Sessions.SessionDetailsGetSessionAttributeCountOptions(); + var attributeCount = sessionDetails.GetSessionAttributeCount(ref boilerplate2); + + foreach (var attributeIndex in Enumerable.Range(0, (int)attributeCount)) + { + var copyAttributeOptions = + new Epic.OnlineServices.Sessions.SessionDetailsCopySessionAttributeByIndexOptions + { + AttrIndex = (uint)attributeIndex + }; + + var attributeCopyResult = sessionDetails.CopySessionAttributeByIndex(ref copyAttributeOptions, out var attributeNullable); + if (attributeCopyResult != Epic.OnlineServices.Result.Success) { break; } + if (attributeNullable?.Data is not { } attributeData + || attributeData.Value.ValueType != Epic.OnlineServices.AttributeType.String) + { + break; + } + attributes.Add(attributeData.Key.ToIdentifier(), attributeData.Value.AsUtf8); + } + sessions.Add(new EosInterface.Sessions.RemoteSession( + SessionId: sessionId, + HostAddress: hostAddress, + CurrentPlayers: (int)(numPublicConnections - numOpenPublicConnections), + MaxPlayers: (int)numPublicConnections, + Attributes: attributes.ToImmutableDictionary(), + BucketId: bucketId)); + } + + return Result.Success(sessions.ToImmutableArray()); + } +} + +internal sealed partial class ImplementationPrivate : EosInterface.Implementation +{ + public override Task, EosInterface.Sessions.RemoteSession.Query.Error>> RunRemoteSessionQuery(EosInterface.Sessions.RemoteSession.Query query) + => TaskScheduler.Schedule(() => RemoteSessionsPrivate.RunQuery(query)); +} \ No newline at end of file diff --git a/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/Util/CallbackWaiter.cs b/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/Util/CallbackWaiter.cs new file mode 100644 index 000000000..677ab874a --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/Util/CallbackWaiter.cs @@ -0,0 +1,50 @@ +#nullable enable + +using System; +using System.Threading.Tasks; +using Barotrauma; + +namespace EosInterfacePrivate; + +/// +/// Creates a task that returns the result of a callback. +/// This is meant to be used with EOS' asynchronous methods, +/// which are all callback-based because this is a C library. +/// +internal class CallbackWaiter where T : notnull +{ + private readonly object mutex = new object(); + private Option result = Option.None; + private readonly DateTime timeout; + + public readonly Task> Task; + + public CallbackWaiter(TimeSpan timeout = default) + { + this.timeout = DateTime.Now + (timeout == default + ? TimeSpan.FromSeconds(60) + : timeout); + this.Task = System.Threading.Tasks.Task.Run(RunTask); + } + + public void OnCompletion(ref T result) + { + lock (mutex) + { + this.result = Option.Some(result); + } + } + + private async Task> RunTask() + { + while (DateTime.Now < timeout) + { + lock (mutex) + { + if (result.IsSome()) { return result; } + } + await System.Threading.Tasks.Task.Delay(32); + } + return Option.None; + } +} diff --git a/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/Util/ResultExtension.cs b/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/Util/ResultExtension.cs new file mode 100644 index 000000000..a37e80169 --- /dev/null +++ b/Libraries/BarotraumaLibs/EosInterfacePrivate/InterfaceImpl/Util/ResultExtension.cs @@ -0,0 +1,14 @@ +using System.Runtime.CompilerServices; +using Barotrauma.Debugging; +using Microsoft.Xna.Framework; + +namespace EosInterfacePrivate; + +public static class ResultExtension +{ + public static T FailAndLogUnhandledError(this Epic.OnlineServices.Result result, T unknown, [CallerMemberName] string caller = null) + { + DebugConsoleCore.NewMessage($"Result \"{result}\" was not handled by \"{caller}\".", Color.Red); + return unknown; + } +} \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/SteamServer.cs b/Libraries/Facepunch.Steamworks/SteamServer.cs index d2a264969..e4543dd1e 100644 --- a/Libraries/Facepunch.Steamworks/SteamServer.cs +++ b/Libraries/Facepunch.Steamworks/SteamServer.cs @@ -403,7 +403,7 @@ namespace Steamworks /// /// Forget this guy. They're no longer in the game. /// - public static void EndSession( SteamId steamid ) + public static void EndAuthSession( SteamId steamid ) { Internal?.EndAuthSession( steamid ); } diff --git a/Libraries/Facepunch.Steamworks/SteamUserStats.cs b/Libraries/Facepunch.Steamworks/SteamUserStats.cs index bf0a267a0..b4a4aa4e7 100644 --- a/Libraries/Facepunch.Steamworks/SteamUserStats.cs +++ b/Libraries/Facepunch.Steamworks/SteamUserStats.cs @@ -200,11 +200,11 @@ namespace Steamworks /// to that value. Steam doesn't provide a mechanism for atomically increasing /// stats like this, this functionality is added here as a convenience. ///
- public static bool AddStat( string name, int amount = 1 ) + public static bool AddStatInt( string name, int amount = 1 ) { var val = GetStatInt( name ); val += amount; - return SetStat( name, val ); + return SetStatInt( name, val ); } /// @@ -212,17 +212,17 @@ namespace Steamworks /// to that value. Steam doesn't provide a mechanism for atomically increasing /// stats like this, this functionality is added here as a convenience. /// - public static bool AddStat( string name, float amount = 1.0f ) + public static bool AddStatFloat( string name, float amount = 1.0f ) { var val = GetStatFloat( name ); val += amount; - return SetStat( name, val ); + return SetStatFloat(name, val) || SetStatInt( name, (int)val ); } /// /// Set a stat value. This will automatically call after a successful call. /// - public static bool SetStat( string name, int value ) + public static bool SetStatInt( string name, int value ) { return Internal != null && Internal.SetStat( name, value ); } @@ -230,7 +230,7 @@ namespace Steamworks /// /// Set a stat value. This will automatically call after a successful call. /// - public static bool SetStat( string name, float value ) + public static bool SetStatFloat( string name, float value ) { return Internal != null && Internal.SetStat( name, value ); } @@ -250,9 +250,12 @@ namespace Steamworks ///
public static float GetStatFloat( string name ) { - float data = 0; - Internal?.GetStat( name, ref data ); - return data; + float dataFloat = 0; + if (Internal?.GetStat(name, ref dataFloat) is true) + { + return dataFloat; + } + return GetStatInt(name); } /// diff --git a/LinuxSolution.sln b/LinuxSolution.sln index 8c10904cd..80f20e9e5 100644 --- a/LinuxSolution.sln +++ b/LinuxSolution.sln @@ -41,7 +41,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MonoGame.Framework.Linux.Ne EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LinuxTest", "Barotrauma\BarotraumaTest\LinuxTest.csproj", "{F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DeployAll", "Deploy\DeployAll\DeployAll.csproj", "{60B82E13-2CDD-4C74-8373-FD7264D6C80B}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeployAll", "Deploy\DeployAll\DeployAll.csproj", "{60B82E13-2CDD-4C74-8373-FD7264D6C80B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BarotraumaLibs", "BarotraumaLibs", "{E63B6919-54B8-40BA-8FFF-FCCB9642358A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BarotraumaCore", "Libraries\BarotraumaLibs\BarotraumaCore\BarotraumaCore.csproj", "{A1686660-B920-407C-BBB9-C9C49543FBDD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EosInterface", "Libraries\BarotraumaLibs\EosInterface\EosInterface.csproj", "{AA83FAA6-12EF-4EC8-A10C-CF92C78706CA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EosInterface.Implementation.Linux", "Libraries\BarotraumaLibs\EosInterfacePrivate\EosInterface.Implementation.Linux.csproj", "{4C00CD9A-4241-43E4-BB62-9356324FE81C}" EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution @@ -212,6 +220,42 @@ Global {60B82E13-2CDD-4C74-8373-FD7264D6C80B}.Unstable|Any CPU.Build.0 = Debug|Any CPU {60B82E13-2CDD-4C74-8373-FD7264D6C80B}.Unstable|x64.ActiveCfg = Debug|Any CPU {60B82E13-2CDD-4C74-8373-FD7264D6C80B}.Unstable|x64.Build.0 = Debug|Any CPU + {A1686660-B920-407C-BBB9-C9C49543FBDD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1686660-B920-407C-BBB9-C9C49543FBDD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1686660-B920-407C-BBB9-C9C49543FBDD}.Debug|x64.ActiveCfg = Debug|Any CPU + {A1686660-B920-407C-BBB9-C9C49543FBDD}.Debug|x64.Build.0 = Debug|Any CPU + {A1686660-B920-407C-BBB9-C9C49543FBDD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1686660-B920-407C-BBB9-C9C49543FBDD}.Release|Any CPU.Build.0 = Release|Any CPU + {A1686660-B920-407C-BBB9-C9C49543FBDD}.Release|x64.ActiveCfg = Release|Any CPU + {A1686660-B920-407C-BBB9-C9C49543FBDD}.Release|x64.Build.0 = Release|Any CPU + {A1686660-B920-407C-BBB9-C9C49543FBDD}.Unstable|Any CPU.ActiveCfg = Debug|Any CPU + {A1686660-B920-407C-BBB9-C9C49543FBDD}.Unstable|Any CPU.Build.0 = Debug|Any CPU + {A1686660-B920-407C-BBB9-C9C49543FBDD}.Unstable|x64.ActiveCfg = Debug|Any CPU + {A1686660-B920-407C-BBB9-C9C49543FBDD}.Unstable|x64.Build.0 = Debug|Any CPU + {AA83FAA6-12EF-4EC8-A10C-CF92C78706CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AA83FAA6-12EF-4EC8-A10C-CF92C78706CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AA83FAA6-12EF-4EC8-A10C-CF92C78706CA}.Debug|x64.ActiveCfg = Debug|Any CPU + {AA83FAA6-12EF-4EC8-A10C-CF92C78706CA}.Debug|x64.Build.0 = Debug|Any CPU + {AA83FAA6-12EF-4EC8-A10C-CF92C78706CA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AA83FAA6-12EF-4EC8-A10C-CF92C78706CA}.Release|Any CPU.Build.0 = Release|Any CPU + {AA83FAA6-12EF-4EC8-A10C-CF92C78706CA}.Release|x64.ActiveCfg = Release|Any CPU + {AA83FAA6-12EF-4EC8-A10C-CF92C78706CA}.Release|x64.Build.0 = Release|Any CPU + {AA83FAA6-12EF-4EC8-A10C-CF92C78706CA}.Unstable|Any CPU.ActiveCfg = Debug|Any CPU + {AA83FAA6-12EF-4EC8-A10C-CF92C78706CA}.Unstable|Any CPU.Build.0 = Debug|Any CPU + {AA83FAA6-12EF-4EC8-A10C-CF92C78706CA}.Unstable|x64.ActiveCfg = Debug|Any CPU + {AA83FAA6-12EF-4EC8-A10C-CF92C78706CA}.Unstable|x64.Build.0 = Debug|Any CPU + {4C00CD9A-4241-43E4-BB62-9356324FE81C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4C00CD9A-4241-43E4-BB62-9356324FE81C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4C00CD9A-4241-43E4-BB62-9356324FE81C}.Debug|x64.ActiveCfg = Debug|Any CPU + {4C00CD9A-4241-43E4-BB62-9356324FE81C}.Debug|x64.Build.0 = Debug|Any CPU + {4C00CD9A-4241-43E4-BB62-9356324FE81C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4C00CD9A-4241-43E4-BB62-9356324FE81C}.Release|Any CPU.Build.0 = Release|Any CPU + {4C00CD9A-4241-43E4-BB62-9356324FE81C}.Release|x64.ActiveCfg = Release|Any CPU + {4C00CD9A-4241-43E4-BB62-9356324FE81C}.Release|x64.Build.0 = Release|Any CPU + {4C00CD9A-4241-43E4-BB62-9356324FE81C}.Unstable|Any CPU.ActiveCfg = Debug|Any CPU + {4C00CD9A-4241-43E4-BB62-9356324FE81C}.Unstable|Any CPU.Build.0 = Debug|Any CPU + {4C00CD9A-4241-43E4-BB62-9356324FE81C}.Unstable|x64.ActiveCfg = Debug|Any CPU + {4C00CD9A-4241-43E4-BB62-9356324FE81C}.Unstable|x64.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -231,6 +275,10 @@ Global {33E95A21-E071-4432-819F-AA64CF3EF3F1} = {DE36F45F-F09E-4719-B953-00D148F7722A} {F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3} = {68B18BE6-9EE0-49DA-AE3A-4C7326F768F9} {60B82E13-2CDD-4C74-8373-FD7264D6C80B} = {F35DF9BF-0BED-4FEF-A51C-DD83C531882F} + {E63B6919-54B8-40BA-8FFF-FCCB9642358A} = {DE36F45F-F09E-4719-B953-00D148F7722A} + {A1686660-B920-407C-BBB9-C9C49543FBDD} = {E63B6919-54B8-40BA-8FFF-FCCB9642358A} + {AA83FAA6-12EF-4EC8-A10C-CF92C78706CA} = {E63B6919-54B8-40BA-8FFF-FCCB9642358A} + {4C00CD9A-4241-43E4-BB62-9356324FE81C} = {E63B6919-54B8-40BA-8FFF-FCCB9642358A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {17032EAB-554B-4B44-A4F6-EFB177ACAB7A} diff --git a/MacSolution.sln b/MacSolution.sln index ee5cddf3f..9d317fedb 100644 --- a/MacSolution.sln +++ b/MacSolution.sln @@ -43,6 +43,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MacTest", "Barotrauma\Barot EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DeployAll", "Deploy\DeployAll\DeployAll.csproj", "{36B38D18-3574-4B67-A89C-FD3C2D39F1D6}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BarotraumaLibs", "BarotraumaLibs", "{02FB212A-F4D5-4AE7-9158-B0E9CEF51200}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BarotraumaCore", "Libraries\BarotraumaLibs\BarotraumaCore\BarotraumaCore.csproj", "{53137227-66ED-48B5-A14F-BABD18D9C797}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EosInterface", "Libraries\BarotraumaLibs\EosInterface\EosInterface.csproj", "{AB7F7CD6-7985-4FA6-A398-B104498401E5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EosInterface.Implementation.MacOS", "Libraries\BarotraumaLibs\EosInterfacePrivate\EosInterface.Implementation.MacOS.csproj", "{F0AD4BF3-D7CB-4FE7-8013-5715A0E6B2EC}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution Libraries\GameAnalytics\GA-SDK-MONO-SHARED\GA-SDK-MONO-SHARED.projitems*{c54f0dfe-add3-4767-8cbc-101859218d66}*SharedItemsImports = 5 @@ -212,6 +220,42 @@ Global {36B38D18-3574-4B67-A89C-FD3C2D39F1D6}.Unstable|Any CPU.Build.0 = Debug|Any CPU {36B38D18-3574-4B67-A89C-FD3C2D39F1D6}.Unstable|x64.ActiveCfg = Debug|Any CPU {36B38D18-3574-4B67-A89C-FD3C2D39F1D6}.Unstable|x64.Build.0 = Debug|Any CPU + {53137227-66ED-48B5-A14F-BABD18D9C797}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {53137227-66ED-48B5-A14F-BABD18D9C797}.Debug|Any CPU.Build.0 = Debug|Any CPU + {53137227-66ED-48B5-A14F-BABD18D9C797}.Debug|x64.ActiveCfg = Debug|Any CPU + {53137227-66ED-48B5-A14F-BABD18D9C797}.Debug|x64.Build.0 = Debug|Any CPU + {53137227-66ED-48B5-A14F-BABD18D9C797}.Release|Any CPU.ActiveCfg = Release|Any CPU + {53137227-66ED-48B5-A14F-BABD18D9C797}.Release|Any CPU.Build.0 = Release|Any CPU + {53137227-66ED-48B5-A14F-BABD18D9C797}.Release|x64.ActiveCfg = Release|Any CPU + {53137227-66ED-48B5-A14F-BABD18D9C797}.Release|x64.Build.0 = Release|Any CPU + {53137227-66ED-48B5-A14F-BABD18D9C797}.Unstable|Any CPU.ActiveCfg = Debug|Any CPU + {53137227-66ED-48B5-A14F-BABD18D9C797}.Unstable|Any CPU.Build.0 = Debug|Any CPU + {53137227-66ED-48B5-A14F-BABD18D9C797}.Unstable|x64.ActiveCfg = Debug|Any CPU + {53137227-66ED-48B5-A14F-BABD18D9C797}.Unstable|x64.Build.0 = Debug|Any CPU + {AB7F7CD6-7985-4FA6-A398-B104498401E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB7F7CD6-7985-4FA6-A398-B104498401E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB7F7CD6-7985-4FA6-A398-B104498401E5}.Debug|x64.ActiveCfg = Debug|Any CPU + {AB7F7CD6-7985-4FA6-A398-B104498401E5}.Debug|x64.Build.0 = Debug|Any CPU + {AB7F7CD6-7985-4FA6-A398-B104498401E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB7F7CD6-7985-4FA6-A398-B104498401E5}.Release|Any CPU.Build.0 = Release|Any CPU + {AB7F7CD6-7985-4FA6-A398-B104498401E5}.Release|x64.ActiveCfg = Release|Any CPU + {AB7F7CD6-7985-4FA6-A398-B104498401E5}.Release|x64.Build.0 = Release|Any CPU + {AB7F7CD6-7985-4FA6-A398-B104498401E5}.Unstable|Any CPU.ActiveCfg = Debug|Any CPU + {AB7F7CD6-7985-4FA6-A398-B104498401E5}.Unstable|Any CPU.Build.0 = Debug|Any CPU + {AB7F7CD6-7985-4FA6-A398-B104498401E5}.Unstable|x64.ActiveCfg = Debug|Any CPU + {AB7F7CD6-7985-4FA6-A398-B104498401E5}.Unstable|x64.Build.0 = Debug|Any CPU + {F0AD4BF3-D7CB-4FE7-8013-5715A0E6B2EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F0AD4BF3-D7CB-4FE7-8013-5715A0E6B2EC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F0AD4BF3-D7CB-4FE7-8013-5715A0E6B2EC}.Debug|x64.ActiveCfg = Debug|Any CPU + {F0AD4BF3-D7CB-4FE7-8013-5715A0E6B2EC}.Debug|x64.Build.0 = Debug|Any CPU + {F0AD4BF3-D7CB-4FE7-8013-5715A0E6B2EC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F0AD4BF3-D7CB-4FE7-8013-5715A0E6B2EC}.Release|Any CPU.Build.0 = Release|Any CPU + {F0AD4BF3-D7CB-4FE7-8013-5715A0E6B2EC}.Release|x64.ActiveCfg = Release|Any CPU + {F0AD4BF3-D7CB-4FE7-8013-5715A0E6B2EC}.Release|x64.Build.0 = Release|Any CPU + {F0AD4BF3-D7CB-4FE7-8013-5715A0E6B2EC}.Unstable|Any CPU.ActiveCfg = Debug|Any CPU + {F0AD4BF3-D7CB-4FE7-8013-5715A0E6B2EC}.Unstable|Any CPU.Build.0 = Debug|Any CPU + {F0AD4BF3-D7CB-4FE7-8013-5715A0E6B2EC}.Unstable|x64.ActiveCfg = Debug|Any CPU + {F0AD4BF3-D7CB-4FE7-8013-5715A0E6B2EC}.Unstable|x64.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -231,6 +275,10 @@ Global {F10CE3BB-26B8-446E-84D2-86D25E850F61} = {DE36F45F-F09E-4719-B953-00D148F7722A} {20BC9336-B439-4BF1-8B65-D587DBF421D1} = {DFD82BBD-8D05-403D-BEBC-F4C1CF783E18} {36B38D18-3574-4B67-A89C-FD3C2D39F1D6} = {F35DF9BF-0BED-4FEF-A51C-DD83C531882F} + {02FB212A-F4D5-4AE7-9158-B0E9CEF51200} = {DE36F45F-F09E-4719-B953-00D148F7722A} + {53137227-66ED-48B5-A14F-BABD18D9C797} = {02FB212A-F4D5-4AE7-9158-B0E9CEF51200} + {AB7F7CD6-7985-4FA6-A398-B104498401E5} = {02FB212A-F4D5-4AE7-9158-B0E9CEF51200} + {F0AD4BF3-D7CB-4FE7-8013-5715A0E6B2EC} = {02FB212A-F4D5-4AE7-9158-B0E9CEF51200} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {17032EAB-554B-4B44-A4F6-EFB177ACAB7A} diff --git a/WindowsSolution.sln b/WindowsSolution.sln index cff6db096..f38dff03a 100644 --- a/WindowsSolution.sln +++ b/WindowsSolution.sln @@ -43,6 +43,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsTest", "Barotrauma\B EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DeployAll", "Deploy\DeployAll\DeployAll.csproj", "{C98FE0D0-BC7D-4806-B592-734B53016FD8}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BarotraumaLibs", "BarotraumaLibs", "{29DF600C-C2EB-48F5-9CCB-7E3B1409D95F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BarotraumaCore", "Libraries\BarotraumaLibs\BarotraumaCore\BarotraumaCore.csproj", "{FA273D62-455C-4BF7-B020-D0EBDE9EB565}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EosInterface", "Libraries\BarotraumaLibs\EosInterface\EosInterface.csproj", "{38C5D23D-0858-4254-B7B7-145221A8AB75}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EosInterface.Implementation.Win64", "Libraries\BarotraumaLibs\EosInterfacePrivate\EosInterface.Implementation.Win64.csproj", "{B411A619-1643-4C5F-A95D-9427D59BE010}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution Libraries\GameAnalytics\GA-SDK-MONO-SHARED\GA-SDK-MONO-SHARED.projitems*{95c4d59d-9be4-4278-b4f8-46c0ba1a3916}*SharedItemsImports = 5 @@ -123,20 +131,32 @@ Global {C7212AE2-A925-4225-A639-AE0653EF65B0}.Debug|x64.Build.0 = Debug|Any CPU {C7212AE2-A925-4225-A639-AE0653EF65B0}.Release|x64.ActiveCfg = Release|Any CPU {C7212AE2-A925-4225-A639-AE0653EF65B0}.Release|x64.Build.0 = Release|Any CPU - {C7212AE2-A925-4225-A639-AE0653EF65B0}.Unstable|x64.ActiveCfg = Debug|Any CPU - {C7212AE2-A925-4225-A639-AE0653EF65B0}.Unstable|x64.Build.0 = Debug|Any CPU + {C7212AE2-A925-4225-A639-AE0653EF65B0}.Unstable|x64.ActiveCfg = Release|Any CPU + {C7212AE2-A925-4225-A639-AE0653EF65B0}.Unstable|x64.Build.0 = Release|Any CPU {C98FE0D0-BC7D-4806-B592-734B53016FD8}.Debug|x64.ActiveCfg = Debug|Any CPU {C98FE0D0-BC7D-4806-B592-734B53016FD8}.Debug|x64.Build.0 = Debug|Any CPU {C98FE0D0-BC7D-4806-B592-734B53016FD8}.Release|x64.ActiveCfg = Release|Any CPU {C98FE0D0-BC7D-4806-B592-734B53016FD8}.Release|x64.Build.0 = Release|Any CPU - {C98FE0D0-BC7D-4806-B592-734B53016FD8}.Unstable|x64.ActiveCfg = Debug|Any CPU - {C98FE0D0-BC7D-4806-B592-734B53016FD8}.Unstable|x64.Build.0 = Debug|Any CPU - {D42D8E48-A687-445D-AAF1-9E7E6EBF1DE6}.Debug|x64.ActiveCfg = Debug|Any CPU - {D42D8E48-A687-445D-AAF1-9E7E6EBF1DE6}.Debug|x64.Build.0 = Debug|Any CPU - {D42D8E48-A687-445D-AAF1-9E7E6EBF1DE6}.Release|x64.ActiveCfg = Release|Any CPU - {D42D8E48-A687-445D-AAF1-9E7E6EBF1DE6}.Release|x64.Build.0 = Release|Any CPU - {D42D8E48-A687-445D-AAF1-9E7E6EBF1DE6}.Unstable|x64.ActiveCfg = Debug|Any CPU - {D42D8E48-A687-445D-AAF1-9E7E6EBF1DE6}.Unstable|x64.Build.0 = Debug|Any CPU + {C98FE0D0-BC7D-4806-B592-734B53016FD8}.Unstable|x64.ActiveCfg = Release|Any CPU + {C98FE0D0-BC7D-4806-B592-734B53016FD8}.Unstable|x64.Build.0 = Release|Any CPU + {FA273D62-455C-4BF7-B020-D0EBDE9EB565}.Debug|x64.ActiveCfg = Debug|Any CPU + {FA273D62-455C-4BF7-B020-D0EBDE9EB565}.Debug|x64.Build.0 = Debug|Any CPU + {FA273D62-455C-4BF7-B020-D0EBDE9EB565}.Release|x64.ActiveCfg = Release|Any CPU + {FA273D62-455C-4BF7-B020-D0EBDE9EB565}.Release|x64.Build.0 = Release|Any CPU + {FA273D62-455C-4BF7-B020-D0EBDE9EB565}.Unstable|x64.ActiveCfg = Release|Any CPU + {FA273D62-455C-4BF7-B020-D0EBDE9EB565}.Unstable|x64.Build.0 = Release|Any CPU + {38C5D23D-0858-4254-B7B7-145221A8AB75}.Debug|x64.ActiveCfg = Debug|Any CPU + {38C5D23D-0858-4254-B7B7-145221A8AB75}.Debug|x64.Build.0 = Debug|Any CPU + {38C5D23D-0858-4254-B7B7-145221A8AB75}.Release|x64.ActiveCfg = Release|Any CPU + {38C5D23D-0858-4254-B7B7-145221A8AB75}.Release|x64.Build.0 = Release|Any CPU + {38C5D23D-0858-4254-B7B7-145221A8AB75}.Unstable|x64.ActiveCfg = Release|Any CPU + {38C5D23D-0858-4254-B7B7-145221A8AB75}.Unstable|x64.Build.0 = Release|Any CPU + {B411A619-1643-4C5F-A95D-9427D59BE010}.Debug|x64.ActiveCfg = Debug|Any CPU + {B411A619-1643-4C5F-A95D-9427D59BE010}.Debug|x64.Build.0 = Debug|Any CPU + {B411A619-1643-4C5F-A95D-9427D59BE010}.Release|x64.ActiveCfg = Release|Any CPU + {B411A619-1643-4C5F-A95D-9427D59BE010}.Release|x64.Build.0 = Release|Any CPU + {B411A619-1643-4C5F-A95D-9427D59BE010}.Unstable|x64.ActiveCfg = Release|Any CPU + {B411A619-1643-4C5F-A95D-9427D59BE010}.Unstable|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -156,6 +176,10 @@ Global {6911872D-40EF-400C-B0A1-9985A19ED488} = {DE36F45F-F09E-4719-B953-00D148F7722A} {C7212AE2-A925-4225-A639-AE0653EF65B0} = {78A9F0AA-5519-407A-9B72-2A09F5DF7068} {C98FE0D0-BC7D-4806-B592-734B53016FD8} = {F35DF9BF-0BED-4FEF-A51C-DD83C531882F} + {29DF600C-C2EB-48F5-9CCB-7E3B1409D95F} = {DE36F45F-F09E-4719-B953-00D148F7722A} + {FA273D62-455C-4BF7-B020-D0EBDE9EB565} = {29DF600C-C2EB-48F5-9CCB-7E3B1409D95F} + {38C5D23D-0858-4254-B7B7-145221A8AB75} = {29DF600C-C2EB-48F5-9CCB-7E3B1409D95F} + {B411A619-1643-4C5F-A95D-9427D59BE010} = {29DF600C-C2EB-48F5-9CCB-7E3B1409D95F} {D42D8E48-A687-445D-AAF1-9E7E6EBF1DE6} = {DE36F45F-F09E-4719-B953-00D148F7722A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution From 0ab5180b7d9974cd54b517fd3eacbd2e19d39041 Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Thu, 28 Mar 2024 20:12:53 +0200 Subject: [PATCH 46/53] Update .gitignore (so the Debugging folder doesn't get ignored) --- .gitignore | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index f8f684fb7..130c8f9af 100644 --- a/.gitignore +++ b/.gitignore @@ -9,8 +9,12 @@ build/ bld/ [Bb]in/ [Oo]bj/ -[Dd]ebug*/ -[Rr]elease*/ +[Dd]ebugWindows/ +[Rr]eleaseWindows/ +[Dd]ebugMac/ +[Rr]eleaseMac/ +[Dd]ebugLinux/ +[Rr]eleaseLinux/ *.o # Misc vs crap From 945051708e1584e0683ed2e6bcf5defcc88af2ea Mon Sep 17 00:00:00 2001 From: Regalis11 Date: Thu, 28 Mar 2024 20:14:02 +0200 Subject: [PATCH 47/53] Added missing DebugConsoleCore file --- .../Debugging/DebugConsoleCore.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 Libraries/BarotraumaLibs/BarotraumaCore/Debugging/DebugConsoleCore.cs diff --git a/Libraries/BarotraumaLibs/BarotraumaCore/Debugging/DebugConsoleCore.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Debugging/DebugConsoleCore.cs new file mode 100644 index 000000000..4d3fe892a --- /dev/null +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Debugging/DebugConsoleCore.cs @@ -0,0 +1,26 @@ +using System; +using Microsoft.Xna.Framework; + +namespace Barotrauma.Debugging; + +public static class DebugConsoleCore +{ + private static Action? newMessage; + private static Action? log; + + public static void Init(Action newMessage, Action log) + { + DebugConsoleCore.newMessage ??= newMessage; + DebugConsoleCore.log ??= log; + } + + public static void NewMessage(string msg, Color? color = null) + { + newMessage?.Invoke(msg, color ?? Color.White); + } + + public static void Log(string msg) + { + log?.Invoke(msg); + } +} \ No newline at end of file From 47badaf760c216d5bb807fd26de87b7e284683bb Mon Sep 17 00:00:00 2001 From: Regalis11 Date: Thu, 28 Mar 2024 20:20:06 +0200 Subject: [PATCH 48/53] Updated bug report template --- .github/DISCUSSION_TEMPLATE/bug-reports.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/DISCUSSION_TEMPLATE/bug-reports.yml b/.github/DISCUSSION_TEMPLATE/bug-reports.yml index 1e05751fa..8a3433f8e 100644 --- a/.github/DISCUSSION_TEMPLATE/bug-reports.yml +++ b/.github/DISCUSSION_TEMPLATE/bug-reports.yml @@ -73,9 +73,8 @@ 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.2.8.0 (Winter Update hotfix 2) - - v1.2.13.0 (unstable) - - v1.2.8.0 (EOS test build) + - v1.3.0.0/v1.3.0.1 + - v1.4.0.0 (unstable) - Other validations: required: true From 95a8b89e2bb84a0f928ca63efc7adcc087373128 Mon Sep 17 00:00:00 2001 From: Regalis11 Date: Tue, 2 Apr 2024 18:15:50 +0300 Subject: [PATCH 49/53] v1.3.0.2 --- .../ClientSource/Steam/BulkDownloader.cs | 20 +++++++++++-------- Barotrauma/BarotraumaShared/changelog.txt | 6 ++++++ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs index 5d8783855..ae973d560 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs @@ -48,15 +48,19 @@ namespace Barotrauma.Steam t => { msgBox.Close(); - if (!t.TryGetResult(out Steamworks.Ugc.Item?[]? itemsNullable)) { return; } + if (!t.TryGetResult(out Option[]? itemOptions)) { return; } - var items = itemsNullable - .Where(it => it.HasValue) - .Select(it => it ?? default) - .ToArray(); - - items.ForEach(it => it.Subscribe()); - InitiateDownloads(items, onComplete: () => + List itemsToDownload = new List(); + foreach (Option itemOption in itemOptions) + { + if (itemOption.TryUnwrap(out var item)) + { + itemsToDownload.Add(item); + } + } + + itemsToDownload.ForEach(it => it.Subscribe()); + InitiateDownloads(itemsToDownload, onComplete: () => { ContentPackageManager.UpdateContentPackageList(); GameMain.Instance.ConnectCommand = Option.Some(rejoinCommand); diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 6c6ee9198..2768565a0 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,9 @@ +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.3.0.2 +------------------------------------------------------------------------------------------------------------------------------------------------- + +- Fixed inability to download missing mods from the Steam Workshop when you're joining a server. + ------------------------------------------------------------------------------------------------------------------------------------------------- v1.3.0.1 ------------------------------------------------------------------------------------------------------------------------------------------------- From 332be4c9d2c1dd6e728826505b1220c3a9047941 Mon Sep 17 00:00:00 2001 From: Regalis11 Date: Tue, 2 Apr 2024 18:16:17 +0300 Subject: [PATCH 50/53] Updated bug report template --- .github/DISCUSSION_TEMPLATE/bug-reports.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/DISCUSSION_TEMPLATE/bug-reports.yml b/.github/DISCUSSION_TEMPLATE/bug-reports.yml index 8a3433f8e..45472fffd 100644 --- a/.github/DISCUSSION_TEMPLATE/bug-reports.yml +++ b/.github/DISCUSSION_TEMPLATE/bug-reports.yml @@ -73,7 +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.3.0.0/v1.3.0.1 + - v1.3.0.2 - v1.4.0.0 (unstable) - Other validations: From 18af2754db64b922b0fc59b7d7678118b9c25ca9 Mon Sep 17 00:00:00 2001 From: Regalis11 Date: Fri, 12 Apr 2024 16:37:51 +0300 Subject: [PATCH 51/53] v1.3.0.3 --- .github/DISCUSSION_TEMPLATE/bug-reports.yml | 2 +- .../ClientSource/DebugConsole.cs | 31 ++++++++++ .../Networking/Primitives/Peers/ClientPeer.cs | 2 + .../Primitives/Peers/LidgrenClientPeer.cs | 3 + .../Primitives/Peers/P2PClientPeer.cs | 3 + .../Primitives/Peers/P2POwnerPeer.cs | 26 ++++++--- .../ClientSource/Utils/ToolBox.cs | 27 +++++++++ .../BarotraumaClient/LinuxClient.csproj | 2 +- Barotrauma/BarotraumaClient/MacClient.csproj | 2 +- .../BarotraumaClient/WindowsClient.csproj | 2 +- .../BarotraumaServer/LinuxServer.csproj | 2 +- Barotrauma/BarotraumaServer/MacServer.csproj | 2 +- .../Peers/Server/LidgrenServerPeer.cs | 11 +++- .../Primitives/Peers/Server/P2PServerPeer.cs | 9 +-- .../Primitives/Peers/Server/ServerPeer.cs | 27 ++++++++- .../BarotraumaServer/WindowsServer.csproj | 2 +- .../SharedSource/DebugConsole.cs | 11 ++++ .../Networking/INetSerializableStruct.cs | 58 ++++++++++++++++++- .../SharedSource/Networking/NetworkMember.cs | 1 + Barotrauma/BarotraumaShared/changelog.txt | 6 ++ 20 files changed, 204 insertions(+), 25 deletions(-) diff --git a/.github/DISCUSSION_TEMPLATE/bug-reports.yml b/.github/DISCUSSION_TEMPLATE/bug-reports.yml index 45472fffd..7f8dfb6e5 100644 --- a/.github/DISCUSSION_TEMPLATE/bug-reports.yml +++ b/.github/DISCUSSION_TEMPLATE/bug-reports.yml @@ -73,7 +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.3.0.2 + - v1.3.0.3 - v1.4.0.0 (unstable) - Other validations: diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index fa1990bcb..8c47bb289 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -775,6 +775,7 @@ namespace Barotrauma AssignRelayToServer("simulatedduplicateschance", false); AssignRelayToServer("simulatedlongloadingtime", false); AssignRelayToServer("storeinfo", false); + AssignRelayToServer("sendrawpacket", false); #endif commands.Add(new Command("clientlist", "", (string[] args) => { })); @@ -3263,6 +3264,36 @@ namespace Barotrauma LocationType.Prefabs.Select(lt => lt.Identifier.Value).ToArray() }; })); + + commands.Add(new Command("sendrawpacket", "sendrawpacket [data]: Send a string of hex values as raw binary data to the server", (string[] args) => + { + if (GameMain.NetworkMember is null) + { + ThrowError("Not connected to a server"); + return; + } + + if (args.Length == 0) + { + ThrowError("No data provided"); + return; + } + + string dataString = string.Join(" ", args); + + try + { + byte[] bytes = ToolBox.HexStringToBytes(dataString); + IWriteMessage msg = new WriteOnlyMessage(); + foreach (byte b in bytes) { msg.WriteByte(b); } + GameMain.Client?.ClientPeer?.DebugSendRawMessage(msg); + NewMessage($"Sent {bytes.Length} byte(s)", Color.Green); + } + catch (Exception e) + { + ThrowError("Failed to parse the data", e); + } + })); #endif commands.Add(new Command("limbscale", "Define the limbscale for the controlled character. Provide id or name if you want to target another character. Note: the changes are not saved!", (string[] args) => diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs index ef0b76332..f357f1a2b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs @@ -231,6 +231,8 @@ namespace Barotrauma.Networking #if DEBUG public abstract void ForceTimeOut(); + + public abstract void DebugSendRawMessage(IWriteMessage msg); #endif } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs index a2a1b7120..563c5ecd1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs @@ -287,6 +287,9 @@ namespace Barotrauma.Networking { netClient?.ServerConnection?.ForceTimeOut(); } + + public override void DebugSendRawMessage(IWriteMessage msg) + => ForwardToLidgren(msg, DeliveryMethod.Reliable); #endif } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/P2PClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/P2PClientPeer.cs index d6624b4cf..9e0e1e3c3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/P2PClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/P2PClientPeer.cs @@ -423,6 +423,9 @@ namespace Barotrauma.Networking { timeout = 0.0f; } + + public override void DebugSendRawMessage(IWriteMessage msg) + => ForwardToRemotePeer(msg, DeliveryMethod.Reliable); #endif } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/P2POwnerPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/P2POwnerPeer.cs index 0b2a340da..537345b0c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/P2POwnerPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/P2POwnerPeer.cs @@ -142,8 +142,12 @@ namespace Barotrauma.Networking if (remotePeer is null) { return; } if (remotePeer.PendingDisconnect.IsSome()) { return; } - var peerPacketHeaders = INetSerializableStruct.Read(inc); - + if (!INetSerializableStruct.TryRead(inc, remotePeer.AccountInfo, out PeerPacketHeaders peerPacketHeaders)) + { + CommunicateDisconnectToRemotePeer(remotePeer, PeerDisconnectPacket.WithReason(DisconnectReason.MalformedData)); + return; + } + PacketHeader packetHeader = peerPacketHeaders.PacketHeader; if (packetHeader.IsConnectionInitializationStep()) @@ -178,13 +182,11 @@ namespace Barotrauma.Networking { remotePeer.AuthStatus = RemotePeer.AuthenticationStatus.AuthenticationPending; - var packet = INetSerializableStruct.Read(inc); - - void failAuth() + if (!INetSerializableStruct.TryRead(inc, remotePeer.AccountInfo, out ClientAuthTicketAndVersionPacket packet)) { - CommunicateDisconnectToRemotePeer(remotePeer, PeerDisconnectPacket.WithReason(DisconnectReason.AuthenticationFailed)); + failAuth(); + return; } - if (!packet.AuthTicket.TryUnwrap(out var authenticationTicket)) { failAuth(); @@ -221,6 +223,11 @@ namespace Barotrauma.Networking } remotePeer.UnauthedMessages.Clear(); }); + + void failAuth() + { + CommunicateDisconnectToRemotePeer(remotePeer, PeerDisconnectPacket.WithReason(DisconnectReason.AuthenticationFailed)); + } } public override void Update(float deltaTime) @@ -381,7 +388,7 @@ namespace Barotrauma.Networking { OnInitializationComplete(); - PeerPacketMessage packet = INetSerializableStruct.Read(inc); + var packet = INetSerializableStruct.Read(inc); IReadMessage msg = new ReadOnlyMessage(packet.Buffer, packetHeader.IsCompressed(), 0, packet.Length, ServerConnection); callbacks.OnMessageReceived.Invoke(msg); } @@ -552,6 +559,9 @@ namespace Barotrauma.Networking { //TODO: reimplement? } + + public override void DebugSendRawMessage(IWriteMessage msg) + => ForwardToServerProcess(msg); #endif } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs index 626db0eb9..047dce5cd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs @@ -520,5 +520,32 @@ namespace Barotrauma static string ColorString(string text, Color color) => $"‖color:{color.ToStringHex()}‖{text}‖end‖"; } + + /// + /// Converts a string of hex values to a byte array. + /// + /// + /// 04 03 4b 50 -> { 4, 3, 75, 80 } + /// + /// + /// + public static byte[] HexStringToBytes(string raw) + { + string value = string.Join(string.Empty, raw.Split(" ")); + List bytes = new List(); + for (int i = 0; i < value.Length; i += 2) + { + string hex = value.Substring(i, 2); + byte b = Convert.ToByte(hex, 16); + bytes.Add(b); + + static bool IsHexChar(char c) => c is + >= '0' and <= '9' or + >= 'A' and <= 'F' or + >= 'a' and <= 'f'; + } + + return bytes.ToArray(); + } } } diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 64c52d9e9..d49b2ab21 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.3.0.1 + 1.3.0.3 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 8effce6b9..3d3148698 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.3.0.1 + 1.3.0.3 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 3574cc843..f5f537757 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.3.0.1 + 1.3.0.3 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 20678b562..5ce39bac1 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.3.0.1 + 1.3.0.3 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 4d80f2da0..6fd38119a 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.3.0.1 + 1.3.0.3 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs index 1f5c680b0..ceb81bd03 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs @@ -226,7 +226,7 @@ namespace Barotrauma.Networking } else if (!packetHeader.IsConnectionInitializationStep()) { - if (connectedClients.Find(c => c.Connection.NetConnection == lidgrenMsg.SenderConnection) is not { Connection: LidgrenConnection conn }) + if (FindConnection(lidgrenMsg.SenderConnection) is not { } conn) { if (pendingClient != null) { @@ -254,6 +254,15 @@ namespace Barotrauma.Networking var packet = INetSerializableStruct.Read(inc); callbacks.OnMessageReceived.Invoke(conn, packet.GetReadMessage(packetHeader.IsCompressed(), conn)); } + + LidgrenConnection? FindConnection(NetConnection ligdrenConn) + { + if (connectedClients.Find(c => c.Connection.NetConnection == ligdrenConn) is { Connection: LidgrenConnection conn }) + { + return conn; + } + return null; + } } private void HandleStatusChanged(NetIncomingMessage inc) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/P2PServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/P2PServerPeer.cs index 69dc977a6..4781c5e0a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/P2PServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/P2PServerPeer.cs @@ -105,11 +105,7 @@ namespace Barotrauma.Networking if (!started) { return; } var senderInfo = INetSerializableStruct.Read(inc); - if (!senderInfo.Endpoint.TryUnwrap(out var senderEndpoint)) - { - return; - } - + if (!senderInfo.Endpoint.TryUnwrap(out var senderEndpoint)) { return; } var (_, packetHeader, initialization) = INetSerializableStruct.Read(inc); if (packetHeader.IsServerMessage()) @@ -179,7 +175,8 @@ namespace Barotrauma.Networking { if (packetHeader.IsDataFragment()) { - var completeMessageOption = connectedClient.Defragmenter.ProcessIncomingFragment(INetSerializableStruct.Read(inc)); + var fragment = INetSerializableStruct.Read(inc); + var completeMessageOption = connectedClient.Defragmenter.ProcessIncomingFragment(fragment); if (!completeMessageOption.TryUnwrap(out var completeMessage)) { return; } IReadMessage msg = new ReadOnlyMessage(completeMessage.ToArray(), false, 0, completeMessage.Length, connectedClient.Connection); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs index 1c5d06389..fd8d39273 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs @@ -97,7 +97,11 @@ namespace Barotrauma.Networking switch (initializationStep) { case ConnectionInitialization.AuthInfoAndVersion: - var authPacket = INetSerializableStruct.Read(inc); + if (!INetSerializableStruct.TryRead(inc, pendingClient.AccountInfo, out ClientAuthTicketAndVersionPacket authPacket)) + { + RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.MalformedData)); + return; + } if (!Client.IsValidName(authPacket.Name, serverSettings)) { @@ -134,7 +138,11 @@ namespace Barotrauma.Networking break; case ConnectionInitialization.Password: - var passwordPacket = INetSerializableStruct.Read(inc); + if (!INetSerializableStruct.TryRead(inc, pendingClient.AccountInfo, out ClientPeerPasswordPacket passwordPacket)) + { + RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.MalformedData)); + return; + } if (pendingClient.PasswordSalt is null) { @@ -335,5 +343,20 @@ namespace Barotrauma.Networking public abstract void Send(IWriteMessage msg, NetworkConnection conn, DeliveryMethod deliveryMethod, bool compressPastThreshold = true); public abstract void Disconnect(NetworkConnection conn, PeerDisconnectPacket peerDisconnectPacket); + + private void LogMalformedMessage(NetworkConnection conn) + { + foreach (Client c in GameMain.Server.ConnectedClients) + { + if (c.Connection == conn) + { + DebugConsole.ThrowError($"Received malformed message from {c.Name}."); + return; + } + } + DebugConsole.ThrowError("Received malformed message from remote peer."); + } + protected static void LogMalformedMessage() + => DebugConsole.ThrowError("Received malformed message from remote peer."); } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index d7aaef3d6..2fe048e3d 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.3.0.1 + 1.3.0.3 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index a013fabf8..dd6b41281 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -2611,6 +2611,17 @@ namespace Barotrauma errorMsg); } + private static readonly HashSet loggedErrorIdentifiers = new HashSet(); + /// + /// Log the error message, but only if an error with the same identifier hasn't been thrown yet during this session. + /// + public static void ThrowErrorOnce(string identifier, string errorMsg, Exception e) + { + if (loggedErrorIdentifiers.Contains(identifier)) { return; } + ThrowError(errorMsg, e); + loggedErrorIdentifiers.Add(identifier); + } + public static void AddWarning(string warning, ContentPackage contentPackage = null) { warning = AddContentPackageInfoToMessage($"WARNING: {warning}", contentPackage); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs index cba0112e6..e2fab1f74 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System; using System.Collections.Generic; @@ -7,6 +7,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; +using System.Text; using Barotrauma.Networking; using Microsoft.Xna.Framework; @@ -792,5 +793,60 @@ namespace Barotrauma property.Behavior.WriteAction(value!, property.Attribute, msg, bitField); } } + + public static bool TryRead(IReadMessage inc, AccountInfo sender, [NotNullWhen(true)] out T? data) where T : INetSerializableStruct + { + try + { + data = Read(inc); + return true; + } + catch (Exception e) + { + LogError(e); + data = default; + return false; + } + + void LogError(Exception e) + { + int prevPos = inc.BitPosition; + + StringBuilder hexData = new(); + inc.BitPosition = 0; + while (inc.BitPosition < inc.LengthBits) + { + byte b = inc.ReadByte(); + hexData.Append($"{b:X2} "); + } + // trim the last space if there is one + if (hexData.Length > 0) { hexData.Length--; } + + inc.BitPosition = prevPos; + + //only log the error once per sender, so this can't be abused by spamming the server with malformed data to fill up the console with errors + //note that the name is "Unknown" if the client hasn't properly joined yet, so errors when first joining are only logged once + string accountInfoName = AccountInfoToName(sender); + DebugConsole.ThrowErrorOnce( + identifier: $"INetSerializableStruct.TryRead:{accountInfoName}", + errorMsg: $"Failed to read a message by {accountInfoName}. Data: \"{hexData}\"", e); + + static string AccountInfoToName(AccountInfo info) + { + var connectedClients = + GameMain.NetworkMember?.ConnectedClients ?? Array.Empty(); + + foreach (Client c in connectedClients) + { + if (c.AccountInfo == info) + { + return c.Name; + } + } + + return info.AccountId.TryUnwrap(out var accountId) ? accountId.StringRepresentation : "Unknown"; + } + } + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs index 7432c97cf..ef48e3162 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -155,6 +155,7 @@ namespace Barotrauma.Networking NameTaken, InvalidVersion, SteamP2PError, + MalformedData, //attempt reconnecting with these reasons Timeout, diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 2768565a0..9b3a8ee2f 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,9 @@ +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.3.0.3 +------------------------------------------------------------------------------------------------------------------------------------------------- + +- Fixed an exploit that allowed crashing servers by sending them specifically crafted malformed data. + ------------------------------------------------------------------------------------------------------------------------------------------------- v1.3.0.2 ------------------------------------------------------------------------------------------------------------------------------------------------- From 536346da119a382657ffbe5cfdfeb8af5b6a4aa0 Mon Sep 17 00:00:00 2001 From: Regalis11 Date: Tue, 16 Apr 2024 18:00:23 +0300 Subject: [PATCH 52/53] v1.3.0.4 --- .../Networking/Primitives/Peers/P2POwnerPeer.cs | 13 +++++++++++-- Barotrauma/BarotraumaClient/LinuxClient.csproj | 2 +- Barotrauma/BarotraumaClient/MacClient.csproj | 2 +- Barotrauma/BarotraumaClient/WindowsClient.csproj | 2 +- Barotrauma/BarotraumaServer/LinuxServer.csproj | 2 +- Barotrauma/BarotraumaServer/MacServer.csproj | 2 +- Barotrauma/BarotraumaServer/WindowsServer.csproj | 2 +- .../BarotraumaShared/SharedSource/DebugConsole.cs | 2 +- Barotrauma/BarotraumaShared/changelog.txt | 6 ++++++ 9 files changed, 24 insertions(+), 9 deletions(-) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/P2POwnerPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/P2POwnerPeer.cs index 537345b0c..f9b0d0081 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/P2POwnerPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/P2POwnerPeer.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using Barotrauma.Extensions; using Barotrauma.Steam; using System; @@ -152,7 +152,16 @@ namespace Barotrauma.Networking if (packetHeader.IsConnectionInitializationStep()) { - ConnectionInitialization initialization = peerPacketHeaders.Initialization ?? throw new Exception("Initialization step missing"); + if (peerPacketHeaders.Initialization == null) + { + //can happen if the packet is crafted in a way to leave the Initialization value as null + DebugConsole.ThrowErrorOnce( + $"P2POwnerPeer.OnP2PData:{remotePeer.Endpoint.StringRepresentation}", + $"Failed to initialize remote peer {remotePeer.Endpoint.StringRepresentation}: initialization step missing."); + CommunicateDisconnectToRemotePeer(remotePeer, PeerDisconnectPacket.WithReason(DisconnectReason.MalformedData)); + return; + } + ConnectionInitialization initialization = peerPacketHeaders.Initialization.Value; if (initialization == ConnectionInitialization.AuthInfoAndVersion && remotePeer.AuthStatus == RemotePeer.AuthenticationStatus.NotAuthenticated) { diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index d49b2ab21..f685198c3 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.3.0.3 + 1.3.0.4 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 3d3148698..c71af10d6 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.3.0.3 + 1.3.0.4 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index f5f537757..0608b7d9d 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.3.0.3 + 1.3.0.4 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 5ce39bac1..a39539d07 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.3.0.3 + 1.3.0.4 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 6fd38119a..dd5b22619 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.3.0.3 + 1.3.0.4 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 2fe048e3d..b02ff43fb 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.3.0.3 + 1.3.0.4 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index dd6b41281..c30424c23 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -2615,7 +2615,7 @@ namespace Barotrauma /// /// Log the error message, but only if an error with the same identifier hasn't been thrown yet during this session. /// - public static void ThrowErrorOnce(string identifier, string errorMsg, Exception e) + public static void ThrowErrorOnce(string identifier, string errorMsg, Exception e = null) { if (loggedErrorIdentifiers.Contains(identifier)) { return; } ThrowError(errorMsg, e); diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 9b3a8ee2f..3178f482c 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,9 @@ +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.3.0.4 +------------------------------------------------------------------------------------------------------------------------------------------------- + +- Fixed another exploit that allowed crashing servers by sending them specifically crafted malformed data. + ------------------------------------------------------------------------------------------------------------------------------------------------- v1.3.0.3 ------------------------------------------------------------------------------------------------------------------------------------------------- From 8face2f344a1f53e6637451a7750221b17c5e7cf Mon Sep 17 00:00:00 2001 From: Regalis11 Date: Tue, 16 Apr 2024 18:01:42 +0300 Subject: [PATCH 53/53] Updated issue template --- .github/DISCUSSION_TEMPLATE/bug-reports.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/DISCUSSION_TEMPLATE/bug-reports.yml b/.github/DISCUSSION_TEMPLATE/bug-reports.yml index 7f8dfb6e5..3a3c429d1 100644 --- a/.github/DISCUSSION_TEMPLATE/bug-reports.yml +++ b/.github/DISCUSSION_TEMPLATE/bug-reports.yml @@ -73,8 +73,8 @@ 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.3.0.3 - - v1.4.0.0 (unstable) + - v1.3.0.4 + - v1.4.3.0 (unstable) - Other validations: required: true