From fb5ea537bf906adf956ea96b76d01d6b8bbedd75 Mon Sep 17 00:00:00 2001 From: Markus Isberg Date: Thu, 30 Nov 2023 13:53:00 +0200 Subject: [PATCH] Unstable 1.2.4.0 --- .../BarotraumaClient/ClientSource/Camera.cs | 19 +- .../ClientSource/Characters/Character.cs | 12 +- .../ClientSource/Characters/CharacterHUD.cs | 2 +- .../ClientSource/Characters/CharacterInfo.cs | 14 +- .../Characters/Health/CharacterHealth.cs | 2 + .../ClientSource/Characters/Limb.cs | 2 +- .../ClientSource/DebugConsole.cs | 27 +- .../EventActions/EventObjectiveAction.cs | 10 +- ...lHighlightAction.cs => HighlightAction.cs} | 17 +- .../ClientSource/Events/EventManager.cs | 9 +- .../ClientSource/Events/Missions/Mission.cs | 8 +- .../ClientSource/GUI/CrewManagement.cs | 56 +-- .../BarotraumaClient/ClientSource/GUI/GUI.cs | 18 +- .../ClientSource/GUI/GUIComponent.cs | 21 +- .../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 | 3 +- .../ClientSource/GUI/SubmarineSelection.cs | 2 +- .../ClientSource/GUI/TabMenu.cs | 9 +- .../BarotraumaClient/ClientSource/GameMain.cs | 6 +- .../GameSession/GameModes/CampaignMode.cs | 8 +- .../GameModes/MultiPlayerCampaign.cs | 17 +- .../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/ItemComponent.cs | 19 +- .../Items/Components/ItemContainer.cs | 13 +- .../Items/Components/ItemLabel.cs | 2 +- .../Items/Components/LightComponent.cs | 2 +- .../Items/Components/Machines/Engine.cs | 6 +- .../Items/Components/Machines/Fabricator.cs | 88 ++++- .../Items/Components/Machines/Sonar.cs | 4 +- .../Items/Components/Machines/Steering.cs | 10 +- .../ClientSource/Items/Components/Turret.cs | 16 + .../ClientSource/Items/Inventory.cs | 65 +++- .../ClientSource/Items/Item.cs | 156 ++++++--- .../ClientSource/Items/ItemPrefab.cs | 18 +- .../ClientSource/Map/ItemAssemblyPrefab.cs | 2 +- .../ClientSource/Map/Levels/Level.cs | 3 +- .../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 | 128 +++++-- .../ClientSource/Map/Map/Map.cs | 10 +- .../ClientSource/Map/MapEntity.cs | 23 +- .../ClientSource/Map/Structure.cs | 16 +- .../ClientSource/Map/StructurePrefab.cs | 8 +- .../ClientSource/Map/SubmarinePreview.cs | 28 +- .../ClientSource/Networking/GameClient.cs | 2 +- .../Networking/ServerList/ServerInfo.cs | 80 +++-- .../ClientSource/Screens/CampaignUI.cs | 5 +- .../ClientSource/Screens/GameScreen.cs | 20 +- .../ClientSource/Screens/LevelEditorScreen.cs | 12 +- .../ClientSource/Screens/MainMenuScreen.cs | 1 + .../ClientSource/Screens/NetLobbyScreen.cs | 1 + .../ServerListScreen/ServerListScreen.cs | 186 +++++++++- .../ClientSource/Screens/SubEditorScreen.cs | 55 ++- .../ClientSource/Settings/SettingsMenu.cs | 16 +- .../ClientSource/Sounds/OggSound.cs | 11 +- .../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 | 19 +- .../StatusEffects/StatusEffect.cs | 2 +- .../ClientSource/Steam/BulkDownloader.cs | 36 +- .../ClientSource/Steam/Workshop.cs | 23 +- .../ClientSource/SubEditorCommands.cs | 83 +++-- .../Utils/{Quad.cs => GraphicsQuad.cs} | 2 +- .../ClientSource/Utils/SpriteRecorder.cs | 11 +- .../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 | 15 +- .../Events/EventActions/HighlightAction.cs | 24 ++ .../ServerSource/Events/Missions/Mission.cs | 8 +- .../GameModes/MultiPlayerCampaign.cs | 48 ++- .../ServerSource/Items/Inventory.cs | 5 +- .../ServerSource/Items/Item.cs | 14 + .../ServerSource/Items/ItemEventData.cs | 21 +- .../ServerSource/Networking/GameServer.cs | 10 +- .../ServerEntityEventManager.cs | 5 +- .../ServerSource/Networking/RespawnManager.cs | 2 +- .../ServerSource/Traitors/TraitorManager.cs | 11 +- .../BarotraumaServer/WindowsServer.csproj | 2 +- .../Characters/AI/AIController.cs | 7 +- .../Characters/AI/EnemyAIController.cs | 15 +- .../Characters/AI/HumanAIController.cs | 98 +++--- .../AI/Objectives/AIObjectiveCombat.cs | 6 +- .../Objectives/AIObjectiveEscapeHandcuffs.cs | 10 +- .../AI/Objectives/AIObjectiveFindThieves.cs | 2 +- .../AI/Objectives/AIObjectiveIdle.cs | 2 +- .../SharedSource/Characters/AI/Order.cs | 14 +- .../Animation/HumanoidAnimController.cs | 50 ++- .../Characters/Animation/Ragdoll.cs | 131 ++++--- .../SharedSource/Characters/Character.cs | 150 ++++---- .../SharedSource/Characters/CharacterInfo.cs | 36 +- .../Health/Afflictions/AfflictionPrefab.cs | 4 + .../Characters/Health/CharacterHealth.cs | 2 + .../Characters/Params/CharacterParams.cs | 14 +- .../SharedSource/Characters/SkillSettings.cs | 7 + .../AbilityConditionCharacter.cs | 54 ++- .../AbilityConditionCharacterNotLooted.cs | 8 +- .../AbilityConditionCharacterUnconcious.cs | 8 +- .../AbilityConditionItemIsStatic.cs | 19 + .../AbilityConditionHasPermanentStat.cs | 13 +- .../AbilityConditionLowestLevel.cs | 13 +- .../CharacterAbilityGiveExperience.cs | 6 +- .../Abilities/CharacterAbilityGiveItemStat.cs | 4 +- .../CharacterAbilityGiveItemStatToTags.cs | 4 +- .../CharacterAbilityGivePermanentStat.cs | 38 +- .../AbilityGroups/CharacterAbilityGroup.cs | 4 + .../CircuitBox/ItemSlotIndexPair.cs | 19 +- .../ContentManagement/ContentFile/TextFile.cs | 17 +- .../ContentPackageManager.cs | 1 + .../SharedSource/DebugConsole.cs | 57 +-- .../BarotraumaShared/SharedSource/Enums.cs | 27 +- .../EventActions/CheckConditionalAction.cs | 103 +++++- .../EventActions/CheckVisibilityAction.cs | 19 +- .../Events/EventActions/EventAction.cs | 32 +- .../EventActions/EventObjectiveAction.cs | 2 +- .../SharedSource/Events/EventActions/GoTo.cs | 18 +- .../Events/EventActions/HighlightAction.cs | 43 +++ .../Events/EventActions/MissionAction.cs | 4 +- .../EventActions/ModifyLocationAction.cs | 6 +- .../EventActions/NPCChangeTeamAction.cs | 2 +- .../Events/EventActions/TagAction.cs | 78 ++++- .../EventActions/TutorialHighlightAction.cs | 34 -- .../EventActions/WaitForItemUsedAction.cs | 31 +- .../SharedSource/Events/EventManager.cs | 14 +- .../Events/Missions/CombatMission.cs | 2 +- .../SharedSource/Events/Missions/Mission.cs | 11 +- .../SharedSource/Events/MonsterEvent.cs | 4 +- .../SharedSource/Events/ScriptedEvent.cs | 96 ++++- .../Extensions/IEnumerableExtensions.cs | 5 + .../SharedSource/GameSession/CrewManager.cs | 9 +- .../GameSession/Data/CampaignMetadata.cs | 9 +- .../GameSession/Data/Reputation.cs | 2 +- .../GameSession/GameModes/CampaignMode.cs | 25 +- .../SharedSource/GameSession/GameSession.cs | 11 +- .../SharedSource/GameSession/HireManager.cs | 20 +- .../SharedSource/Items/CharacterInventory.cs | 13 + .../SharedSource/Items/Components/Door.cs | 9 +- .../Items/Components/Holdable/IdCard.cs | 3 - .../Items/Components/Holdable/MeleeWeapon.cs | 5 +- .../Items/Components/Holdable/Pickable.cs | 2 +- .../Items/Components/Holdable/RepairTool.cs | 12 +- .../Items/Components/ItemComponent.cs | 18 +- .../Items/Components/ItemContainer.cs | 26 +- .../Components/Machines/Deconstructor.cs | 2 +- .../Items/Components/Machines/Engine.cs | 4 +- .../Items/Components/Machines/Fabricator.cs | 113 ++++-- .../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 | 1 + .../Items/Components/Projectile.cs | 5 +- .../Items/Components/Repairable.cs | 6 +- .../Items/Components/Signal/CircuitBox.cs | 2 +- .../Components/Signal/ConnectionPanel.cs | 6 + .../Items/Components/Signal/LightComponent.cs | 36 +- .../Items/Components/TriggerComponent.cs | 9 + .../SharedSource/Items/Components/Turret.cs | 136 +++++++- .../SharedSource/Items/Components/Wearable.cs | 1 + .../SharedSource/Items/Inventory.cs | 10 +- .../SharedSource/Items/Item.cs | 57 ++- .../SharedSource/Items/ItemEventData.cs | 3 +- .../SharedSource/Items/ItemPrefab.cs | 34 +- .../SharedSource/Items/ItemStatManager.cs | 87 ++++- .../BarotraumaShared/SharedSource/Map/Hull.cs | 5 + .../SharedSource/Map/Levels/Level.cs | 25 +- .../SharedSource/Map/Levels/LevelData.cs | 2 +- .../Levels/LevelObjects/LevelObjectManager.cs | 1 + .../SharedSource/Map/Map/Location.cs | 145 ++++++-- .../SharedSource/Map/Map/LocationType.cs | 100 +++++- .../SharedSource/Map/Map/Map.cs | 22 +- .../SharedSource/Map/MapEntity.cs | 10 +- .../SharedSource/Map/Structure.cs | 16 +- .../SharedSource/Map/Submarine.cs | 156 +++++---- .../SharedSource/Map/SubmarineBody.cs | 53 ++- .../SharedSource/Networking/EntitySpawner.cs | 6 +- .../Networking/OrderChatMessage.cs | 2 +- .../SharedSource/Networking/RespawnManager.cs | 7 + .../SharedSource/Networking/ServerSettings.cs | 7 + .../SharedSource/Settings/GameSettings.cs | 2 + .../SharedSource/SteamAchievementManager.cs | 1 + .../BarotraumaShared/SharedSource/Tags.cs | 10 + .../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 +- .../SharedSource/Utils/MathUtils.cs | 54 ++- .../SharedSource/Utils/SaveUtil.cs | 9 +- .../SharedSource/Utils/Shapes/Quad2D.cs | 113 ++++++ .../SharedSource/Utils/Shapes/Triangle2D.cs | 21 ++ Barotrauma/BarotraumaShared/changelog.txt | 117 +++++++ .../FabricatorQualityRollTests.cs | 66 ++++ 210 files changed, 4201 insertions(+), 1283 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/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 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..f7af8779d 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; @@ -521,22 +522,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 de3f40011..d08948b86 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -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); @@ -2234,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; } @@ -3094,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/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 6b408b9a3..d5687b38a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs @@ -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/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/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 270c2e6ec..4ebc75a56 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -2127,11 +2127,10 @@ namespace Barotrauma { var dialog = new GUIMessageBox( TextManager.Get("newsupplies"), - TextManager.GetWithVariable("suppliespurchasedmessage", "[location]", campaignUI?.Campaign?.Map?.CurrentLocation?.Name)); + TextManager.GetWithVariable("suppliespurchasedmessage", "[location]", campaignUI?.Campaign?.Map?.CurrentLocation?.DisplayName)); dialog.Buttons[0].OnClicked += dialog.Close; } } - return false; } 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/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/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index 4c143b2ee..80723ea67 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -192,12 +192,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: @@ -205,7 +205,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; @@ -221,7 +221,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 8c408cf95..ce6972067 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -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/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index b90e5bac4..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; } @@ -761,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 35799ed55..21b3c9c76 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -346,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; } @@ -571,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; } @@ -589,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 f300ae562..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 @@ -825,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) @@ -853,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')); @@ -865,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) { @@ -875,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'); @@ -894,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 = ""; @@ -921,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), @@ -946,7 +1004,6 @@ namespace Barotrauma.Items.Components font: GUIStyle.SmallFont); } - return true; } public void HighlightRecipe(string identifier, Color color) @@ -1056,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 a5d5766f1..1e636ec15 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -991,7 +991,7 @@ 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, @@ -1001,7 +1001,7 @@ namespace Barotrauma.Items.Components 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, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index c3827e5b5..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() 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 5163a7196..054df6051 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -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,19 @@ 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) + { + if (!container.AllowDragAndDrop || !container.DrawInventory) + { + allowCombine = false; + } + } + bool success = selectedInventory.TryPutItem(item, slotIndex, allowSwapping: !anySuccess, allowCombine, Character.Controlled); if (success) { anySuccess = true; @@ -1380,18 +1396,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 +1415,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 +1606,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; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index e2cb908f9..1633cf7bc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -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) @@ -386,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, @@ -401,20 +402,7 @@ namespace Barotrauma textureScale: Vector2.One * Scale, depth: d); } - foreach (var decorativeSprite in Prefab.DecorativeSprites) - { - if (!spriteAnimState[decorativeSprite].IsActive) { continue; } - - Color decorativeSpriteColor = GetSpriteColor(decorativeSprite.Color); - 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: decorativeSpriteColor, - 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,21 +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; } - Color decorativeSpriteColor = GetSpriteColor(decorativeSprite.Color); - 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)), decorativeSpriteColor, - 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) @@ -492,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) @@ -524,7 +485,6 @@ namespace Barotrauma rotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, activeSprite.effects, depth: depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth)); } - } activeSprite.effects = oldEffects; @@ -569,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) { @@ -629,6 +595,55 @@ namespace Barotrauma } } + 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) { if (impact > 1.0f && @@ -804,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(); @@ -861,6 +889,11 @@ namespace Barotrauma CanBeFocused = true }; + 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"), @@ -873,6 +906,7 @@ namespace Barotrauma } if (!SelectedList.Contains(this)) { FlipX(relativeToSub: false); } ColorFlipButton(button, FlippedX); + if (rotationField != null) { rotationField.FloatValue = Rotation; } return true; } }; @@ -889,6 +923,7 @@ namespace Barotrauma } if (!SelectedList.Contains(this)) { FlipY(relativeToSub: false); } ColorFlipButton(button, FlippedY); + if (rotationField != null) { rotationField.FloatValue = Rotation; } return true; } }; @@ -1553,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}"); } @@ -1953,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 995e56880..7a7701b9e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs @@ -371,19 +371,27 @@ namespace Barotrauma { if (!ResizeHorizontal && !ResizeVertical) { - sprite.Draw(spriteBatch, new Vector2(placeRect.Center.X, -(placeRect.Y - placeRect.Height / 2)), SpriteColor * 0.8f, scale: scale, rotate: rotation); + 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 { Vector2 position = placeRect.Location.ToVector2(); Vector2 placeSize = placeRect.Size.ToVector2(); sprite?.DrawTiled( - spriteBatch, - new Vector2(position.X, -position.Y), - placeSize, + spriteBatch: spriteBatch, + position: new Vector2(position.X, -position.Y), + targetSize: placeSize, rotation: rotation, textureScale: Vector2.One * scale, - color: SpriteColor * 0.8f); + color: SpriteColor * 0.8f, + spriteEffects: spriteEffects ^ sprite.effects); } } 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/Level.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs index 5c54904ba..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 { 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..90dcac14c 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; } @@ -259,7 +268,8 @@ namespace Barotrauma.Lights { if (!light.Enabled) { continue; } if ((light.Color.A < 1 || light.Range < 1.0f) && !light.LightSourceParams.OverrideLightSpriteAlpha.HasValue) { continue; } - + //above the top boundary of the level (in an inactive respawn shuttle?) + if (Level.Loaded != null && light.WorldPosition.Y > Level.Loaded.Size.Y) { continue; } if (light.ParentBody != null) { light.ParentBody.UpdateDrawPosition(); @@ -801,6 +811,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 777b0ebd9..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) { @@ -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/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index a9ab8cc23..4e97d4929 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -162,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(); } } @@ -1332,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/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index d1453f665..4fe5ce8e0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -236,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; @@ -312,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))); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs index 06e17348a..587e21dab 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs @@ -96,9 +96,6 @@ namespace Barotrauma 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; @@ -109,9 +106,8 @@ namespace Barotrauma color: Color.White * 0.8f, origin: placeRect.Size.ToVector2() * 0.5f, rotation: rotation, - textureScale: TextureScale * scale); - - Sprite.effects = oldEffects; + textureScale: TextureScale * scale, + spriteEffects: spriteEffects ^ Sprite.effects); } } } 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/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 6fa7ec90f..c6d338147 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -2222,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); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs index ab0cc11ea..4ce4dd2e0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs @@ -181,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())) @@ -263,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) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index 1cefb2a67..a78816c2a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -254,7 +254,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 +598,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 143100d4d..0982130cf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -165,6 +165,7 @@ namespace Barotrauma } } #else + SpamServerFilters.RequestGlobalSpamFilter(); FetchRemoteContent(); #endif diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 7e8b006c9..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) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs index f08ce1d14..d48c4bc39 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs @@ -655,6 +655,7 @@ namespace Barotrauma ScrollBarVisible = true, OnSelected = (btn, obj) => { + if (GUI.MouseOn is GUIButton) { return false; } if (obj is not ServerInfo serverInfo) { return false; } joinButton.Enabled = true; @@ -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 a48cb05d6..63e8af01b 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; @@ -1560,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 @@ -3933,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)); } } @@ -5485,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; } } 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 25ea1f177..d050e09c4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs @@ -1,15 +1,16 @@ -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 @@ -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/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 f76e97b23..5314247ec 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs @@ -284,13 +284,22 @@ namespace Barotrauma } } - 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) + 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; } - bool flipHorizontal = (effects & SpriteEffects.FlipHorizontally) != 0; - bool flipVertical = (effects & SpriteEffects.FlipVertically) != 0; + 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; } @@ -311,7 +320,7 @@ namespace Barotrauma Vector2 transformedPos = slicePos - position; transformedPos = advanceX * transformedPos.X + advanceY * transformedPos.Y; transformedPos += position - transformedOrigin; - spriteBatch.Draw(texture, transformedPos, sliceRect, drawColor, addedRotation, Vector2.Zero, scale, effects, depth ?? this.depth); + spriteBatch.Draw(texture, transformedPos, sliceRect, drawColor, addedRotation, Vector2.Zero, scale, spriteEffects.Value, depth ?? this.depth); } //wrap the drawOffset inside the sourceRect 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/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/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 e31bc063d..6f4b4da41 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.2.1.0 + 1.2.4.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 42698bdfd..38f410733 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.2.1.0 + 1.2.4.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 38e7e8161..2b8cb45e3 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.2.1.0 + 1.2.4.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 093a855f3..d921409b5 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.2.1.0 + 1.2.4.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 04527ab69..9c8837281 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.2.1.0 + 1.2.4.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 532c057e7..aebcc26a5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -1134,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); } } @@ -1416,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 { @@ -1461,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]}."); @@ -1484,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() }; })); @@ -2457,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); } ); 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/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 69a72d1b7..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; @@ -1231,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/Inventory.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs index d2afe45b1..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,8 +107,7 @@ 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"})", item.Prefab.ContentPackage); 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/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index efd98d89e..a790ba686 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -804,7 +804,7 @@ namespace Barotrauma.Networking { using (dosProtection.Pause(connectedClient)) { - MultiPlayerCampaign.LoadCampaign(saveName); + MultiPlayerCampaign.LoadCampaign(saveName, connectedClient); } } } @@ -1230,11 +1230,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 +1479,7 @@ namespace Barotrauma.Networking { using (dosProtection.Pause(sender)) { - MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath); + MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath, sender); } } } @@ -3945,7 +3940,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/Traitors/TraitorManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs index 55eed416d..498108b29 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs @@ -263,7 +263,7 @@ namespace Barotrauma if (amountToChoose > viableTraitors.Count) { DebugConsole.ThrowError( - $"Error in traitor event {traitorEvent.Prefab.Identifier}. Not enough players to choose {amountToChoose} 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; @@ -455,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 be16280af..06bd6d52a 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.2.1.0 + 1.2.4.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 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 adbf7e933..d4188f275 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -262,7 +262,7 @@ 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); @@ -312,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); @@ -322,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 @@ -565,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); @@ -3097,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 95bae3353..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]; @@ -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) 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 index bff65d6b5..f6ac0bec8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindThieves.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindThieves.cs @@ -114,7 +114,7 @@ namespace Barotrauma 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)) + character.CanSeeTarget(target, seeThroughWindows: true)) { AIObjectiveCheckStolenItems? existingObjective = objectiveManager.GetActiveObjectives().FirstOrDefault(o => o.TargetCharacter == target); 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/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index 4c862b4aa..04ee9ccad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -664,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) @@ -713,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/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 4004391cc..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; } @@ -501,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; } @@ -586,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( @@ -764,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); } @@ -1536,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 @@ -1741,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 048d81e17..ffebedae2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -572,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++) @@ -582,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) @@ -668,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; } @@ -710,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) @@ -1335,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 @@ -1593,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 || @@ -1621,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; @@ -1700,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/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index d2f146fa7..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(); @@ -1694,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)); @@ -2303,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) @@ -2365,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; } @@ -2382,39 +2386,37 @@ 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) + if (item.GetComponent() is { HasWindow: true } door && seeThroughWindows) { if (door.IsPositionOnWindow(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition))) { return false; } } @@ -2513,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; } @@ -3584,6 +3598,8 @@ namespace Barotrauma private void Despawn(bool createNetworkEvents = true) { + if (!EnableDespawn) { return; } + Identifier despawnContainerId = IsHuman ? "despawncontainer".ToIdentifier() : @@ -3665,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) @@ -3683,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; @@ -4340,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); } } @@ -4366,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); + } } /// @@ -5005,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() @@ -5021,7 +5037,7 @@ namespace Barotrauma foreach (var hull in adjacentHulls) { if (hull.ConnectedGaps.Any(g => - (g.Open > 0.9f || g.ConnectedDoor is { HasWindow: true }) && + g.Open > 0.9f && g.linkedTo.Contains(CurrentHull) && Vector2.DistanceSquared(g.WorldPosition, WorldPosition) < Math.Pow(maxDistance / 2, 2))) { @@ -5041,7 +5057,7 @@ namespace Barotrauma else { if (h.ConnectedGaps.Any(g => - (g.Open > 0.9f || g.ConnectedDoor is { HasWindow: true }) && + g.Open > 0.9f && Vector2.DistanceSquared(g.WorldPosition, WorldPosition) < Math.Pow(maxDistance / 2, 2) && CanSeeTarget(g))) { @@ -5582,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 0752b6b3f..2b8466406 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -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); @@ -1902,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/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 6f5ac8417..035a8c9ae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -1026,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()); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 3df905b80..7e813b74a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -1313,6 +1313,8 @@ namespace Barotrauma public void Remove() { RemoveProjSpecific(); + afflictionsToRemove.Clear(); + afflictionsToUpdate.Clear(); } partial void RemoveProjSpecific(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 289940df0..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) @@ -603,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); @@ -732,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/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/AbilityConditionData/AbilityConditionCharacter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs index 8b8fe05cb..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,6 +10,12 @@ 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( @@ -25,30 +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.", 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/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/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs index 96ed33dab..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; @@ -21,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/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/CharacterAbilityGiveExperience.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveExperience.cs index 56807217a..3aa2719ea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveExperience.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveExperience.cs @@ -38,7 +38,11 @@ internal sealed class CharacterAbilityGiveExperience : CharacterAbility 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/CharacterAbilityGivePermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs index c046863a6..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; @@ -40,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) @@ -72,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); } } @@ -83,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/AbilityGroups/CharacterAbilityGroup.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs index cb33943db..4d177dd69 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs @@ -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; } } 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/TextFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TextFile.cs index 68d3b4184..ff4493c9c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TextFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TextFile.cs @@ -19,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(); } @@ -33,9 +32,9 @@ 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)) @@ -49,7 +48,11 @@ namespace Barotrauma 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/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/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 2599ebd65..ae30a5412 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -858,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) @@ -877,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() }; })); @@ -1613,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) => @@ -1627,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 @@ -1641,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); } })); @@ -1913,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}"; @@ -2417,11 +2432,7 @@ namespace Barotrauma public static void LogError(string msg, Color? color = null, ContentPackage contentPackage = null) { - if (contentPackage != null) - { - string colorStr = XMLExtensions.ToStringHex(Color.MediumPurple); - msg = $"‖color:{colorStr}‖[{contentPackage.Name}]‖color:end‖ {msg}"; - } + msg = AddContentPackageInfoToMessage(msg, contentPackage); color ??= Color.Red; NewMessage(msg, color.Value, isCommand: false, isError: true); } @@ -2557,11 +2568,7 @@ namespace Barotrauma public static void ThrowError(string error, Exception e = null, ContentPackage contentPackage = null, bool createMessageBox = false, bool appendStackTrace = false) { - if (contentPackage != null) - { - string color = XMLExtensions.ToStringHex(Color.MediumPurple); - error = $"‖color:{color}‖[{contentPackage.Name}]‖color:end‖ {error}"; - } + error = AddContentPackageInfoToMessage(error, contentPackage); if (e != null) { error += " {" + e.Message + "}\n"; @@ -2610,16 +2617,22 @@ namespace Barotrauma public static void AddWarning(string warning, ContentPackage contentPackage = null) { - warning = $"WARNING: {warning}"; - if (contentPackage != null) - { - string color = XMLExtensions.ToStringHex(Color.MediumPurple); - warning = $"‖color:{color}‖[{contentPackage.Name}]‖color:end‖ {warning}"; - } + warning = AddContentPackageInfoToMessage($"WARNING: {warning}", contentPackage); System.Diagnostics.Debug.WriteLine(warning); 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 private static IEnumerable CreateMessageBox(string errorMsg) { 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/EventActions/CheckConditionalAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs index d59add1f5..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,7 +11,16 @@ 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) { @@ -17,14 +29,38 @@ namespace Barotrauma 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()) + { + //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(); + } + + 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 IsNotTargetTagAttribute(XAttribute attribute) => attribute.NameAsIdentifier() != "targettag"; + 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() @@ -34,32 +70,65 @@ namespace Barotrauma protected override bool? DetermineSuccess() { - ISerializableEntity target = null; + IEnumerable targets = null; if (!TargetTag.IsEmpty) { - foreach (var t in ParentEvent.GetTargets(TargetTag)) - { - if (t is ISerializableEntity e) - { - target = e; - break; - } - } + targets = ParentEvent.GetTargets(TargetTag).OfType(); } - if (target == null) + + 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 (target == null || Conditional == null) + + if (targets.None() || Conditionals.None()) { + foreach (var target in targets) + { + ApplyTagsToHulls(target as Entity, ApplyTagToHull, ApplyTagToLinkedHulls); + } 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/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/EventAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs index 4585edbc4..f9be2652e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs @@ -103,7 +103,7 @@ namespace Barotrauma public EventAction(ScriptedEvent parentEvent, ContentXElement element) { - ParentEvent = parentEvent ?? throw new ArgumentNullException(nameof(parentEvent)); + ParentEvent = parentEvent; SerializableProperty.DeserializeProperties(this, element); } @@ -141,7 +141,11 @@ 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(); } @@ -170,6 +174,30 @@ namespace Barotrauma } } + 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/EventObjectiveAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventObjectiveAction.cs index 82cbb6ee8..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; } 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 d87c58be1..4592e6751 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs @@ -123,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), diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ModifyLocationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ModifyLocationAction.cs index cd044361b..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; @@ -77,9 +77,9 @@ namespace Barotrauma 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 239312aff..239b95f60 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs @@ -101,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/TagAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs index 7374e3466..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; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialHighlightAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialHighlightAction.cs deleted file mode 100644 index e227f401d..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialHighlightAction.cs +++ /dev/null @@ -1,34 +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.", - contentPackage: element.ContentPackage); - } - } - - 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/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 183ba0a2b..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; } @@ -905,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/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/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index 717befcb9..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); } @@ -431,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)); @@ -652,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/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index 03f144e2c..187c8f919 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -673,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)) { @@ -700,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 7503af5c5..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; @@ -191,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)) { @@ -229,7 +243,6 @@ namespace Barotrauma } List targetsToReturn = new List(); - if (Targets.ContainsKey(tag)) { foreach (Entity e in Targets[tag]) @@ -240,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); + } } } } @@ -293,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++; @@ -310,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; @@ -369,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/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index 4f6455dba..762807d0d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -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 0e5bfb3f0..3b10f6269 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -558,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" + @@ -570,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" + @@ -582,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") + ", " + @@ -1024,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) @@ -1034,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); @@ -1263,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++) @@ -1271,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); } } @@ -1307,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); 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/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index 1a3e665aa..a8d47043e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -151,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 9f80f04ad..041465193 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -343,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 @@ -404,7 +409,7 @@ namespace Barotrauma.Items.Components position.Y >= item.Rect.Y + Window.Y && position.Y <= item.Rect.Y + Window.Y + Window.Height && position.X >= item.Rect.X - maxPerpendicularDistance && - position.Y <= item.Rect.Right + maxPerpendicularDistance; + position.X <= item.Rect.Right + maxPerpendicularDistance; } } 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/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index a33359230..4f0686cef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -572,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) @@ -660,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/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index 4e1862e59..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; @@ -394,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; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 015807276..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(); @@ -298,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++) { @@ -389,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) @@ -397,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)); @@ -405,7 +417,6 @@ namespace Barotrauma.Items.Components } } - var relatedItem = FindContainableItem(containedItem); var containedItemInfo = new ContainedItem(containedItem, Hide: relatedItem?.Hide ?? false, ItemPos: relatedItem?.ItemPos, @@ -786,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)); } /// @@ -1095,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) @@ -1107,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/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 0bc72b64c..63de9cce6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -469,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; @@ -534,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); } @@ -576,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); @@ -593,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; } @@ -698,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 c0d788133..93d47b5c7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs @@ -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 6e5363695..9b3444649 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -1017,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/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/Signal/CircuitBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs index d2675e51b..69c395e3c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs @@ -157,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()) { 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 d6af54c78..8f1228208 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs @@ -275,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 41b58165d..180487e83 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -559,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..cebc3baad 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. /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index edb9917cc..c44369a96 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -383,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 @@ -393,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) { @@ -1327,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) { @@ -1640,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 /// @@ -2516,7 +2522,7 @@ namespace Barotrauma if (Prefab.AllowRotatingInEditor) { - RotationRad = MathUtils.WrapAngleTwoPi(-RotationRad); + RotationRad = MathUtils.WrapAnglePi(-RotationRad); } #if CLIENT if (Prefab.CanSpriteFlipX) @@ -2543,6 +2549,10 @@ namespace Barotrauma return; } + if (Prefab.AllowRotatingInEditor) + { + RotationRad = MathUtils.WrapAngleTwoPi(-RotationRad); + } #if CLIENT if (Prefab.CanSpriteFlipY) { @@ -3043,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))) { @@ -3068,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; } } } @@ -3526,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; } @@ -3799,6 +3831,11 @@ namespace Barotrauma } break; } + case "itemstats": + { + item.StatManager.Load(subElement); + break; + } default: { ItemComponent component = unloadedComponents.Find(x => x.Name == subElement.Name.ToString()); @@ -3924,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(); } @@ -3994,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 ddedb9091..1b6d9aff5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -871,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)] @@ -1138,10 +1157,13 @@ namespace Barotrauma 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 = GetParentModPackageOrThisPackage(); + 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.", 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/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/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 106c1354b..5db866ad8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -633,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); @@ -4122,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 @@ -4230,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); @@ -4254,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 @@ -4263,7 +4276,7 @@ namespace Barotrauma if (EndLocation != null) { outpost.TeamID = EndLocation.Type.OutpostTeam; - outpost.Info.Name = EndLocation.Name; + outpost.Info.Name = EndLocation.DisplayName.Value; } } } 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/LevelObjectManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs index 29ca85cf5..98b36532c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -639,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 cb8c2faf5..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); @@ -641,7 +658,7 @@ namespace Barotrauma LevelData = new LevelData(element.GetChildElement("Level"), clampDifficultyToBiome: true); - PortraitId = ToolBox.StringToInt(Name); + PortraitId = ToolBox.StringToInt(!rawName.IsNullOrEmpty() ? rawName : nameIdentifier.Value); LoadStores(element); LoadMissions(element); @@ -687,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); @@ -698,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); @@ -735,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) { @@ -1058,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; } @@ -1086,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) @@ -1125,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; @@ -1133,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; @@ -1444,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), @@ -1455,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)); @@ -1491,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/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/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index 10e72f4ca..afbdb5271 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -733,6 +733,12 @@ namespace Barotrauma } } + 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. /// @@ -912,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; @@ -1304,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); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index 459307b73..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); @@ -929,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; @@ -968,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; @@ -1885,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) { @@ -1925,9 +1950,11 @@ namespace Barotrauma Ragdoll.RemoveAll(); PhysicsBody.RemoveAll(); + StatusEffect.StopAll(); GameMain.World = null; Powered.Grids.Clear(); + Powered.ChangedConnections.Clear(); GC.Collect(); @@ -1947,6 +1974,7 @@ namespace Barotrauma outdoorNodes?.Clear(); outdoorNodes = null; + obstructedNodes.Clear(); GameMain.GameSession?.Campaign?.UpgradeManager?.OnUpgradesChanged?.TryDeregister(upgradeEventIdentifier); @@ -1958,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 9976f1f51..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); } } } 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 2a782728c..c105f27de 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -848,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/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index 7d7012dcd..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; 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 53282a021..830c43168 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Tags.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Tags.cs @@ -100,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/Utils/MathUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs index 8deb9ec70..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); } @@ -1091,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; } @@ -1106,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 2adfd257b..4eec61623 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,120 @@ +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.2.4.0 +------------------------------------------------------------------------------------------------------------------------------------------------- + +Unstable only: +- Fixed captain tutorial's drone steering objective completing automatically. +- Fixed items inside held items (like tanks in welding tools) being mirrored vertically when you're facing left. +- Fixed lights sometimes being visible even if the item emitting the light is hidden inside an inventory. +- Fixed items sometimes not emitting light when brought outside. +- Fixed looping sounds occasionally not stopping when they should. Happened in situations where there was no streaming audio active (i.e. no music or background ambience). +- Fixed items with an inaccessible/hidden inventory (like magazines) no longer stacking. +- Fixed inability to edit components' values in circuit boxes in multiplayer. + +Fixes: +- Fixed turret lights always being forced on at the start of the round. + +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.2.3.0 +------------------------------------------------------------------------------------------------------------------------------------------------- + +Unstable only: +- Fixed crashing when a bot tries to fire a turret at an ice spire. +- Fixed bots trying to shoot through windowed doors and glass walls. +- Adjusted indentured servitude talent: you only get a discount of 20% from assistants (the effects do still stack though, so you if you have lots of assistants with the talent in the crew, you can get a 100% discount). We're still not completely sure about this: is it too powerful/balance-breaking to get free crewmembers? +- Fixed "starter quest" talent crashing the game when you kill a crawler. + +Changes and additions: +- 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". + +Optimization: +- 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. + +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. + +Talents: +- Fixed "junction junkie" not working on sonar monitors. + +Fixes: +- 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. + +Modding: +- 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. + +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.2.2.0 +------------------------------------------------------------------------------------------------------------------------------------------------- + +Unstable only: +- Fixed rotated structures sometimes getting culled even though they're still visible on the screen. +- Fixed area select not working correctly on rotated structures in the sub editor. +- Fixed rotated structures not appearing rotated on the submarine preview. + +Changes and additions: +- 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. +- 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. +- Added new variants of the Monsters Nearby and Submarine Flooded tracks. +- Location names can now be translated. In the vanilla game the names are only translated if you're playing in Chinese. +- Minor adjustments to level layouts: all the levels now slope down a bit to give the impression you're actually heading deeper. + +Talents: +- Five new assistant talents: Starter Quest, Mule, Jenga Master, Indentured Servitude, Tasty Target. + +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. +- Miscellaneous small optimizations. + +Traitors: +- 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). + +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 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. + +Modding: +- 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. + ------------------------------------------------------------------------------------------------------------------------------------------------- v1.2.1.0 ------------------------------------------------------------------------------------------------------------------------------------------------- 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